├── .gitignore ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── build_database.yml ├── tasks └── build_database.js ├── cmds ├── owner │ ├── echo.js │ ├── killall.js │ ├── bash.js │ └── eval.js ├── information │ ├── ping.js │ ├── uptime.js │ ├── about.js │ ├── invite.js │ └── source.js ├── setup │ ├── setup.js │ └── helper │ │ ├── misc │ │ └── embeds.js │ │ ├── roles.js │ │ └── updates.js ├── utilities │ └── help.js └── apple │ ├── macos_installers.js │ ├── ipsw.js │ └── update_info.js ├── assets ├── devices.json ├── catalogs.json └── audiences.json ├── core ├── error.js ├── apple │ ├── info.js │ ├── xml.js │ ├── doc.js │ ├── gdmf.js │ └── manager.js ├── misc.js ├── updates.js ├── embed.js └── send.js ├── package.json ├── README.md ├── index.js └── LICENSE.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .env 4 | database.json -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /tasks/build_database.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; 2 | 3 | global.SAVE_MODE = true; 4 | 5 | const firebase = require("firebase-admin"); 6 | 7 | firebase.initializeApp({ 8 | credential: firebase.credential.cert(JSON.parse(process.env.firebase)) 9 | }); 10 | 11 | require("../core/updates.js")(); 12 | 13 | (async() => { 14 | await fetch_gdmf(true, true, true, true, true, true); 15 | await fetch_xml(); 16 | })(); 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/build_database.yml: -------------------------------------------------------------------------------- 1 | name: Build Database 2 | 3 | on: 4 | schedule: 5 | - cron: '0 */24 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repo 14 | uses: actions/checkout@v2 15 | 16 | - name: Use Node.js 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 22 20 | - run: npm install 21 | 22 | - name: Build database 23 | run: node tasks/build_database.js 24 | env: 25 | firebase: ${{ secrets.FIREBASE }} 26 | error_id: ${{ secrets.WEBHOOK_ID }} 27 | error_token: ${{ secrets.WEBHOOK_TOKEN }} 28 | -------------------------------------------------------------------------------- /cmds/owner/echo.js: -------------------------------------------------------------------------------- 1 | // Send bot announcements to servers 2 | 3 | require('../../core/embed.js')(); 4 | 5 | module.exports = { 6 | name: 'echo', 7 | command: 'echo <"title"> <"announcement">', 8 | category: 'Owner', 9 | description: '**[Owner Only]** Sends bot\'s announcements.', 10 | async execute(message, args) { 11 | let isBotOwner = message.author.id == process.env.owner_id; 12 | if (!isBotOwner) return message.channel.send(error_alert('Dreaming of sending an announcement to thousands of people? Well you can\'t!')); 13 | 14 | send_announcements(message.content.match(/[^"]+(?=(" ")|"$)/g)[0], message.content.match(/[^"]+(?=(" ")|"$)/g)[1]); 15 | }, 16 | }; -------------------------------------------------------------------------------- /cmds/owner/killall.js: -------------------------------------------------------------------------------- 1 | // Crash the bot 2 | 3 | const { EmbedBuilder } = require('discord.js'); 4 | 5 | require('../../core/embed.js')(); 6 | 7 | module.exports = { 8 | name: 'killall', 9 | command: 'killall', 10 | category: 'Owner', 11 | description: '**[Owner Only]** Restarts the bot.', 12 | async execute(message, args) { 13 | let isBotOwner = message.author.id == process.env.owner_id; 14 | if (!isBotOwner) return message.channel.send(error_alert('You can only cr4sh me unless you have special powers ¯\\_(ツ)_/¯')); 15 | 16 | global.bot.user.setStatus("invisible"); 17 | 18 | const embed = new EmbedBuilder() 19 | .setDescription(':skull: **Cr4shed successfully!**') 20 | .setTimestamp(); 21 | 22 | message.channel.send({ embeds: [embed] }).then(() => { 23 | process.exit(1); 24 | }); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /cmds/information/ping.js: -------------------------------------------------------------------------------- 1 | // Check bot latency 2 | 3 | const { EmbedBuilder } = require("discord.js"); 4 | const { SlashCommandBuilder } = require('@discordjs/builders'); 5 | 6 | require('../../core/misc.js')(); 7 | 8 | module.exports = { 9 | name: 'ping', 10 | command: 'ping', 11 | category: 'Information', 12 | description: 'Checks the bot\'s connection.', 13 | data: new SlashCommandBuilder().setName("ping").setDescription("Check the bot's connection."), 14 | async execute(interaction) { 15 | const embed = new EmbedBuilder().setColor(randomColor()); 16 | const time_past = new Date().getTime(); 17 | await interaction.editReply({ embeds: [embed.setDescription("Ping?")] }); 18 | const time_now = new Date().getTime(); 19 | await interaction.editReply({ embeds: [embed.setDescription(`:bell: **Pong!** It took \`${time_now - time_past}ms\` for signals to reach me. My current heartbeat is \`${Math.round(global.bot.ws.ping)}ms\`.`)] }); 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /assets/devices.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "version": "18.5", 4 | "build": "22F76", 5 | "prodtype": "iPhone16,2", 6 | "model": "D84AP" 7 | }, 8 | 9 | "ipados": { 10 | "version": "18.5", 11 | "build": "22F76", 12 | "prodtype": "iPad16,5", 13 | "model": "J720AP" 14 | }, 15 | 16 | "watchos": { 17 | "version": "11.5", 18 | "build": "22T572", 19 | "prodtype": "Watch6,18", 20 | "model": "N199AP" 21 | }, 22 | 23 | "audioos": { 24 | "version": "18.5", 25 | "build": "22L572", 26 | "prodtype": "AudioAccessory5,1", 27 | "model": "B520AP" 28 | }, 29 | 30 | "tvos": { 31 | "version": "18.5", 32 | "build": "22L572", 33 | "prodtype": "AppleTV14,1", 34 | "model": "J255AP" 35 | }, 36 | 37 | "macos": { 38 | "version": "13.7.2", 39 | "build": "22H313", 40 | "prodtype": "MacBookPro18,3", 41 | "model": "J314sAP" 42 | } 43 | } -------------------------------------------------------------------------------- /core/error.js: -------------------------------------------------------------------------------- 1 | // Send log and error messages to the log/error channel in a monitor discord server 2 | 3 | const { EmbedBuilder, WebhookClient } = require('discord.js'); 4 | const error_reporter = new WebhookClient({ id: process.env.error_id, token: process.env.error_token }); 5 | 6 | require('./misc.js')(); 7 | 8 | module.exports = function () { 9 | this.send_error = function (message, location, process, task) { 10 | let time = getCurrentTime("Asia/Ho_Chi_Minh"); 11 | let color = (global.BETA_RELEASE) ? "#f07800" : "#f52b32"; 12 | const embed = new EmbedBuilder() 13 | .setColor(color) 14 | .setDescription(`**Error message**:\n> ${message}`) 15 | .addFields( 16 | { name: `File`, value: location, inline: true }, 17 | { name: `Process`, value: process, inline: true }, 18 | { name: `Task`, value: task, inline: true } 19 | ) 20 | .setFooter({ text: time }); 21 | error_reporter.send({ embeds: [embed] }); 22 | }; 23 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apple-updates", 3 | "version": "4.0.0", 4 | "description": "A Discord bot for pushing notifications about new Apple OS updates.", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "node index.js", 8 | "start": "NODE_ENV=production node index.js" 9 | }, 10 | "engines": { 11 | "node": ">=17.0.1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Minh-Ton/apple-updates.git" 16 | }, 17 | "keywords": [ 18 | "apple", 19 | "updates", 20 | "os", 21 | "discord-bot" 22 | ], 23 | "author": "MinhTon", 24 | "license": "GPL-3.0", 25 | "bugs": { 26 | "url": "https://github.com/Minh-Ton/apple-updates/issues" 27 | }, 28 | "homepage": "https://github.com/Minh-Ton/apple-updates", 29 | "dependencies": { 30 | "adm-zip": "^0.5.10", 31 | "axios": "^1.9.0", 32 | "discord.js": "^14.19.3", 33 | "dotenv": "^16.5.0", 34 | "firebase-admin": "^13.4.0", 35 | "minisearch": "^7.1.2", 36 | "plist": "^3.1.0", 37 | "pretty-bytes": "^5.6.0", 38 | "pretty-ms": "^7.0.1", 39 | "randomcolor": "^0.6.2", 40 | "sanitize-html": "^2.17.0", 41 | "uniqid": "^5.4.0", 42 | "url-status-code": "^2.0.0", 43 | "xml-js": "^1.6.11" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cmds/information/uptime.js: -------------------------------------------------------------------------------- 1 | // Show bot uptime 2 | 3 | const { EmbedBuilder } = require('discord.js'); 4 | const ms = require("pretty-ms"); 5 | const { SlashCommandBuilder } = require('@discordjs/builders'); 6 | 7 | require('../../core/misc.js')(); 8 | 9 | module.exports = { 10 | name: 'uptime', 11 | command: 'uptime', 12 | category: 'Information', 13 | description: 'Shows the bot uptime.', 14 | data: new SlashCommandBuilder().setName("uptime").setDescription("Shows the bot uptime."), 15 | async execute(interaction) { 16 | const embed = new EmbedBuilder() 17 | .setColor(randomColor()) 18 | .setTitle(`${global.bot.user.tag} - Uptime`) 19 | .setThumbnail(global.bot.user.displayAvatarURL({ format: "png", dynamic: true })) 20 | .addFields( 21 | { name: `Bot Uptime`, value: ms(global.bot.uptime).toString(), inline: true }, 22 | { name: `Bot Age`, value: ms(Math.abs(new Date() - new Date(global.bot.user.createdAt))), inline: true }, 23 | { name: `Status`, value: (!global.BOT_STATUS) ? "Starting" : global.BOT_STATUS, inline: true }, 24 | { name: `Memory Usage`, value: `${formatBytes(process.memoryUsage.rss())} / ${formatBytes(require('os').totalmem())}`, inline: true }, 25 | { name: `CPU Usage`, value: `User: ${((global.CPU_USAGE.user / process.cpuUsage().user) * 100).toFixed(1)}% - System: ${((global.CPU_USAGE.system / process.cpuUsage().system) * 100).toFixed(1)}%`, inline: true } 26 | ) 27 | await interaction.editReply({ embeds: [embed] }); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /cmds/information/about.js: -------------------------------------------------------------------------------- 1 | // Show bot info 2 | 3 | const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); 4 | const path = require('path'); 5 | const axios = require('axios'); 6 | const { SlashCommandBuilder } = require('@discordjs/builders'); 7 | 8 | require('../../core/misc.js')(); 9 | 10 | module.exports = { 11 | name: 'about', 12 | command: 'about', 13 | category: 'Information', 14 | description: 'Displays bot information.', 15 | data: new SlashCommandBuilder().setName("about").setDescription("Displays bot information."), 16 | async execute(interaction) { 17 | let updated = (await axios.get("https://api.github.com/repos/minh-ton/apple-updates", { 18 | headers: { 'Authorization': `token ${process.env.github_token}` } 19 | })).data.pushed_at; 20 | const button = new ActionRowBuilder().addComponents( 21 | new ButtonBuilder() 22 | .setURL("https://discord.gg/ktHmcbpMNU") 23 | .setLabel('Join support server') 24 | .setStyle(ButtonStyle.Link)); 25 | let about_embed = new EmbedBuilder() 26 | .setColor(randomColor()) 27 | .setTitle(`${global.bot.user.tag} - About`) 28 | .setThumbnail(global.bot.user.displayAvatarURL({ format: "png", dynamic: true })) 29 | .addFields( 30 | { name: `Version`, value: require(path.join(__dirname, '../../package.json')).version, inline: true }, 31 | { name: `Last Updated`, value: ``, inline: true }, 32 | { name: `Servers`, value: `${global.bot.guilds.cache.size}`, inline: true } 33 | ) 34 | .setFooter({ text: "Join our support server: https://discord.gg/ktHmcbpMNU" }); 35 | await interaction.editReply({ embeds: [about_embed], components: [button] }); 36 | }, 37 | } -------------------------------------------------------------------------------- /core/apple/info.js: -------------------------------------------------------------------------------- 1 | // Save update data for searching 2 | 3 | const firebase = require("firebase-admin"); 4 | 5 | require('../error.js')(); 6 | 7 | let db = firebase.firestore(); 8 | 9 | const database = db.collection('other').doc('information'); 10 | 11 | module.exports = function() { 12 | this.save_update = async function(cname, version, size, build, updateid, changelog, postdate, raw_response, beta) { 13 | let information = database.collection(`${cname.toLowerCase()}${(beta) ? "_beta" : "_public"}`); 14 | 15 | if (!build) return; 16 | 17 | let cache = await information.doc(build).get(); 18 | let cache_data = cache.data(); 19 | 20 | let data = { 21 | "version": version, 22 | "size": size, 23 | "build": build, 24 | "updateid": updateid, 25 | "changelog": changelog, 26 | "postdate": postdate, 27 | "raw_response": raw_response, 28 | "beta": beta 29 | }; 30 | 31 | try { 32 | if (cache_data == undefined) await information.doc(build).set(data); 33 | else await information.doc(build).update(data); 34 | } catch(error) { 35 | return send_error(`Cannot update information`, "info.js", `save_update`, `uploading for ${cname}.`); 36 | } 37 | } 38 | 39 | this.save_package = async function(cname, build, version, size, package, postdate, beta) { 40 | let information = database.collection(`${cname.toLowerCase()}${(beta) ? "_beta" : "_public"}`); 41 | 42 | if (!build) return; 43 | 44 | let cache = await information.doc(build).get(); 45 | let cache_data = cache.data(); 46 | 47 | let data = { 48 | "version": version, 49 | "build": build, 50 | "package": package, 51 | "packagesize": size, 52 | "postdate": postdate, 53 | "beta": beta 54 | }; 55 | 56 | try { 57 | if (cache_data == undefined) await information.doc(build).set(data); 58 | else await information.doc(build).update(data); 59 | } catch(error) { 60 | return send_error(`Cannot update information`, "info.js", `save_package`, `uploading for ${cname}.`); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /cmds/owner/bash.js: -------------------------------------------------------------------------------- 1 | // Run shell commands on the host machine 2 | 3 | const { exec } = require('child_process'); 4 | const { EmbedBuilder } = require('discord.js'); 5 | 6 | require('../../core/embed.js')(); 7 | 8 | module.exports = { 9 | name: 'bash', 10 | command: 'bash ', 11 | category: 'Owner', 12 | description: '**[Owner Only]** Executes a bash command.', 13 | async execute(message, args) { 14 | let isBotOwner = message.author.id == process.env.owner_id; 15 | if (!isBotOwner) return message.channel.send(error_alert('You know what, it\'s my job to prevent strangers on Discord from damaging my own house with bash commands!')); 16 | 17 | if (!args.join(" ")) return message.channel.send(error_alert('Empty bash command, interesting!')); 18 | 19 | const m_embed = new EmbedBuilder().setDescription(`Executing \`${args.join(" ")}\`...`); 20 | const m = await message.channel.send({ embeds: [m_embed] }); 21 | exec(args.join(" "), (err, stdout, stderr) => { 22 | if (err) { 23 | const embed = new EmbedBuilder() 24 | .setDescription(`**Command exited with error:** \n \`\`\`${err}\`\`\``) 25 | .setTimestamp(); 26 | m.edit({ embeds: [embed] }); 27 | return; 28 | } 29 | 30 | var error = (stderr) ? stderr : "No Error"; 31 | var output = (stdout) ? stdout : "No Output" 32 | 33 | if (output.length > 1000) output = output.substring(0, 1000) + '...'; 34 | if (error.length > 1000) error = error.substring(0, 1000) + '...'; 35 | 36 | const embed = new EmbedBuilder() 37 | .addFields( 38 | { name: `Output`, value: `\`\`\`${output}\`\`\`` }, 39 | { name: `Error`, value: `\`\`\`${error}\`\`\`` } 40 | ) 41 | .setTimestamp(); 42 | m.edit({ embeds: [embed] }); 43 | }); 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /cmds/owner/eval.js: -------------------------------------------------------------------------------- 1 | // Run arbitrary JS code 2 | 3 | const { EmbedBuilder } = require('discord.js'); 4 | 5 | require('../../core/embed.js')(); 6 | 7 | const clean = text => { 8 | if (typeof(text) === "string") 9 | return text.replace(/`/g, "`" + String.fromCharCode(8203)).replace(/@/g, "@" + String.fromCharCode(8203)); 10 | else 11 | return text; 12 | } 13 | 14 | module.exports = { 15 | name: 'eval', 16 | command: 'eval ', 17 | category: 'Owner', 18 | description: '**[Owner Only]** Evaluates JavaScript code.', 19 | async execute(message, args) { 20 | let isBotOwner = message.author.id == process.env.owner_id; 21 | if (!isBotOwner) return message.channel.send(error_alert('Want to take control of me? NO!')); 22 | 23 | try { 24 | const code = args.join(" "); 25 | 26 | if (!code) return message.channel.send(error_alert('How could I execute your code if you didn\'t provide any?')); 27 | 28 | let evaled = eval(code); 29 | 30 | if (typeof evaled !== "string") evaled = require("util").inspect(evaled); 31 | 32 | const success = new EmbedBuilder() 33 | .setDescription(`\`SUCCESS\` \`\`\`xl\n${clean(evaled)}\n\`\`\``) 34 | .setColor("#00d768") 35 | .setTimestamp(); 36 | try { 37 | if (message.channel) message.channel.send({ embeds: [success] }); 38 | } catch(e) { 39 | return console.log(e); 40 | } 41 | 42 | } catch (err) { 43 | const error = new EmbedBuilder() 44 | .setDescription(`\`ERROR\` \`\`\`xl\n${clean(err)}\n\`\`\``) 45 | .setColor("#c2002a") 46 | .setTimestamp(); 47 | try { 48 | if (message.channel) message.channel.send({ embeds: [error] }); 49 | } catch(e) { 50 | return console.log(e); 51 | } 52 | } 53 | 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /cmds/information/invite.js: -------------------------------------------------------------------------------- 1 | // Show bot info 2 | 3 | const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); 4 | const { SlashCommandBuilder } = require('@discordjs/builders'); 5 | 6 | require('../../core/misc.js')(); 7 | 8 | module.exports = { 9 | name: 'invite', 10 | command: 'invite', 11 | category: 'Information', 12 | description: 'Shows the bot invite link.', 13 | data: new SlashCommandBuilder().setName("invite").setDescription("Shows the bot invite link."), 14 | async execute(interaction) { 15 | const button = new ActionRowBuilder().addComponents( 16 | new ButtonBuilder() 17 | .setURL("https://discord.com/api/oauth2/authorize?client_id=852378577063116820&permissions=8&scope=applications.commands%20bot") 18 | .setLabel('Invite Software Updates') 19 | .setStyle(ButtonStyle.Link), 20 | new ButtonBuilder() 21 | .setURL("https://discord.gg/ktHmcbpMNU") 22 | .setLabel('Join support server') 23 | .setStyle(ButtonStyle.Link)); 24 | let invite_embed = new EmbedBuilder() 25 | .setColor(randomColor()) 26 | .setThumbnail(global.bot.user.displayAvatarURL({ format: "png", dynamic: true })) 27 | .setTitle(`${global.bot.user.tag} - Invite`) 28 | .setURL("https://discord.com/api/oauth2/authorize?client_id=852378577063116820&permissions=8&scope=applications.commands%20bot") 29 | .setDescription(`Click [here](https://discord.com/api/oauth2/authorize?client_id=852378577063116820&permissions=8&scope=applications.commands%20bot) to invite the bot to your server.\n\nAfter inviting the bot to your server, make sure to run the \`setup\` command to configure it to your liking!`) 30 | .setFooter({ text: "Join our support server: https://discord.gg/ktHmcbpMNU" }); 31 | await interaction.editReply({ embeds: [invite_embed], components: [button] }); 32 | }, 33 | } -------------------------------------------------------------------------------- /assets/catalogs.json: -------------------------------------------------------------------------------- 1 | { 2 | "macos_public": "https://swscan.apple.com/content/catalogs/others/index-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog", 3 | "macos_beta": "https://swscan.apple.com/content/catalogs/others/index-16seed-16-15-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog", 4 | 5 | "macos_beta_15": "https://swscan.apple.com/content/catalogs/others/index-15seed-15-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog", 6 | "macos_beta_14": "https://swscan.apple.com/content/catalogs/others/index-14seed-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog", 7 | "macos_beta_13": "https://swscan.apple.com/content/catalogs/others/index-13seed-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog", 8 | "macos_beta_12": "https://swscan.apple.com/content/catalogs/others/index-12seed-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog", 9 | "macos_beta_11": "https://swscan.apple.com/content/catalogs/others/index-10.16seed-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog", 10 | 11 | "airtags": "https://mesu.apple.com/assets/com_apple_MobileAsset_MobileAccessoryUpdate_DurianFirmware/com_apple_MobileAsset_MobileAccessoryUpdate_DurianFirmware.xml", 12 | "airpods": "https://mesu.apple.com/assets/com_apple_MobileAsset_MobileAccessoryUpdate_A2032_EA/com_apple_MobileAsset_MobileAccessoryUpdate_A2032_EA.xml", 13 | "airpods_3": "https://mesu.apple.com/assets/com_apple_MobileAsset_MobileAccessoryUpdate_A2564_EA/com_apple_MobileAsset_MobileAccessoryUpdate_A2564_EA.xml", 14 | "airpods_pro": "https://mesu.apple.com/assets/com_apple_MobileAsset_MobileAccessoryUpdate_A2084_EA/com_apple_MobileAsset_MobileAccessoryUpdate_A2084_EA.xml" 15 | } -------------------------------------------------------------------------------- /core/apple/xml.js: -------------------------------------------------------------------------------- 1 | // Fetch updates from Apple's XML Catalog 2 | 3 | const axios = require('axios'); 4 | const xmljs = require('xml-js'); 5 | const plist = require('plist'); 6 | 7 | require('../error.js')(); 8 | 9 | module.exports = async function () { 10 | this.fetch_macos_xml = async function (url, dname) { 11 | let response = await axios.get(url).catch(function (error) { 12 | return send_error(error, "xml.js", `fetch_macos_pkg - ${dname}`, `fetching the catalog for new updates`); 13 | }); 14 | 15 | if (!response) return send_error("No XML response availble.", "xml.js", `fetch_macos_pkg - ${dname}`, `fetching the catalog for new updates`); 16 | 17 | let catalog_content = plist.parse(response.data); 18 | 19 | let packages = []; 20 | 21 | for (let product in catalog_content.Products) for (let package in catalog_content.Products[product].Packages) if (catalog_content.Products[product].Packages[package].URL.endsWith('InstallAssistant.pkg')) { 22 | 23 | let pkg_info = catalog_content.Products[product].Distributions.English; 24 | 25 | let info = await axios.get(pkg_info).catch(function (error) { 26 | return send_error(error, "xml.js", `fetch_macos_pkg - ${dname}`, `getting InstallAssistant.pkg info from the distribution file`); 27 | }); 28 | 29 | if (!info) return send_error("No product info available.", "xml.js", `fetch_macos_pkg - ${dname}`, `getting InstallAssistant.pkg info from the distribution file`); 30 | 31 | const packageinfo = JSON.parse(xmljs.xml2json(info.data, { compact: true, spaces: 2 })); 32 | 33 | let xml_update = { 34 | xml_pkg: catalog_content.Products[product].Packages[package].URL, 35 | xml_version: packageinfo['installer-gui-script'].auxinfo.dict.string[1]._text.replace('9.9.', ''), 36 | xml_build: packageinfo['installer-gui-script'].auxinfo.dict.string[0]._text, 37 | xml_size: catalog_content.Products[product].Packages[package].Size, 38 | xml_postdate: catalog_content.Products[product].PostDate 39 | } 40 | 41 | packages.push(xml_update); 42 | } 43 | 44 | return packages; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cmds/information/source.js: -------------------------------------------------------------------------------- 1 | // View bot's source code 2 | 3 | const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); 4 | const axios = require('axios'); 5 | const { SlashCommandBuilder } = require('@discordjs/builders'); 6 | 7 | require('../../core/misc.js')(); 8 | 9 | module.exports = { 10 | name: 'source', 11 | command: 'source', 12 | category: 'Information', 13 | description: 'Shows the bot\'s source code link.', 14 | data: new SlashCommandBuilder().setName("source").setDescription("Shows the bot's source code link."), 15 | async execute(interaction) { 16 | let repo_data = (await axios.get("https://api.github.com/repos/minh-ton/apple-updates", { 17 | headers: { 'Authorization': `token ${process.env.github_token}` } 18 | })).data; 19 | const button = new ActionRowBuilder().addComponents( 20 | new ButtonBuilder() 21 | .setURL("https://github.com/Minh-Ton/apple-updates") 22 | .setLabel('Source Code') 23 | .setStyle(ButtonStyle.Link), 24 | new ButtonBuilder() 25 | .setURL("https://discord.gg/ktHmcbpMNU") 26 | .setLabel('Join support server') 27 | .setStyle(ButtonStyle.Link)); 28 | const source_embed = new EmbedBuilder() 29 | .setColor(randomColor()) 30 | .setTitle(`${global.bot.user.tag} - Source Code`) 31 | .setURL("https://github.com/Minh-Ton/apple-updates") 32 | .setThumbnail(global.bot.user.displayAvatarURL({ format: "png", dynamic: true })) 33 | .setDescription(`Curious about how I work?\nClick [here](https://github.com/Minh-Ton/apple-updates) to view my source code on GitHub!`) 34 | .addFields( 35 | { name: "Stargazers", value: repo_data.stargazers_count.toString(), inline: true }, 36 | { name: "Watchers", value: repo_data.subscribers_count.toString(), inline: true }, 37 | { name: "Forks", value: repo_data.forks_count.toString(), inline: true } 38 | ) 39 | .setFooter({ text: "Join our support server: https://discord.gg/ktHmcbpMNU" }); 40 | await interaction.editReply({ embeds: [source_embed], components: [button] }); 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /cmds/setup/setup.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, PermissionFlagsBits } = require('discord.js'); 2 | const { SlashCommandBuilder } = require('@discordjs/builders'); 3 | 4 | require("./helper/roles.js")(); 5 | require("./helper/updates.js")(); 6 | require("../../core/embed.js")(); 7 | 8 | module.exports = { 9 | name: 'setup', 10 | command: 'setup', 11 | category: 'Utilities', 12 | cooldown: 5, 13 | ephemeral: false, 14 | description: 'Configures the bot to your liking!', 15 | usage: '`/setup`: Configures the bot.\n`/setup role add`: Adds a notification role.\n`/setup role remove`: Removes a notification role.\n`/setup role list`: Lists configures notification roles.', 16 | data: new SlashCommandBuilder().setName("setup").setDescription("Configures the bot to your liking!") 17 | .addStringOption(option => option.setName("option").setDescription("Configures notification roles").setRequired(false) 18 | .addChoices( 19 | { name: 'role add', value: 'role add' }, 20 | { name: 'role remove', value: 'role remove' }, 21 | { name: 'role list', value: 'role list' }, 22 | )), 23 | async execute(interaction) { 24 | if (!interaction.member.permissions.has(PermissionFlagsBits.ManageGuild)) return interaction.editReply(error_alert("You do not have the `MANAGE SERVER` permission to use this command!")); 25 | if (!interaction.guild.members.me.permissions.has([ 26 | PermissionFlagsBits.ViewChannel, 27 | PermissionFlagsBits.AddReactions, 28 | PermissionFlagsBits.UseExternalEmojis, 29 | PermissionFlagsBits.ManageMessages 30 | ])) return interaction.editReply(error_alert("I do not have the necessary permissions to work properly! \n\n ***Please make sure I have the following permissions:*** \n- View Channels\n- Add Reactions\n- Use External Emojis\n- Manage Messages")); 31 | 32 | try { 33 | if (interaction.options.getString('option') != undefined && interaction.options.getString('option').includes("role")) await setup_roles(interaction); 34 | else await setup_updates(interaction); 35 | } catch (e) { 36 | console.error(e); 37 | return interaction.editReply(error_alert("An unknown error occurred while running **setup** command.", e)); 38 | } 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /assets/audiences.json: -------------------------------------------------------------------------------- 1 | { 2 | "macos_release": "60b55e25-a8ed-4f45-826c-c1495a4ccc65", 3 | "macos_11_beta": "ca60afc6-5954-46fd-8cb9-60dde6ac39fd", 4 | "macos_12_beta": "298e518d-b45e-4d36-94be-34a63d6777ec", 5 | "macos_13_beta": "683e9586-8a82-4e5f-b0e7-767541864b8b", 6 | "macos_14_beta": "77c3bd36-d384-44e8-b550-05122d7da438", 7 | "macos_15_beta": "98df7800-8378-4469-93bf-5912da21a1e1", 8 | "macos_26_beta": "832afda4-7283-41da-a95b-75f4a151e473", 9 | 10 | "display_release": "60b55e25-a8ed-4f45-826c-c1495a4ccc65", 11 | "display_15_beta": "298e518d-b45e-4d36-94be-34a63d6777ec", 12 | "display_16_beta": "683e9586-8a82-4e5f-b0e7-767541864b8b", 13 | "display_17_beta": "77c3bd36-d384-44e8-b550-05122d7da438", 14 | "display_18_beta": "98df7800-8378-4469-93bf-5912da21a1e1", 15 | 16 | "ios_release": "01c1d682-6e8f-4908-b724-5501fe3f5e5c", 17 | "ios_14_security": "c724cb61-e974-42d3-a911-ffd4dce11eda", 18 | "ios_15_beta": "ce48f60c-f590-4157-a96f-41179ca08278", 19 | "ios_16_beta": "a6050bca-50d8-4e45-adc2-f7333396a42c", 20 | "ios_17_beta": "9dcdaf87-801d-42f6-8ec6-307bd2ab9955", 21 | "ios_18_beta": "41651cee-d0e2-442f-b786-85682ff6db86", 22 | "ios_26_beta": "da1941f6-9822-4347-b771-fb09c3509052", 23 | 24 | "watchos_release": "b82fcf9c-c284-41c9-8eb2-e69bf5a5269f", 25 | "watchos_8_beta": "b407c130-d8af-42fc-ad7a-171efea5a3d0", 26 | "watchos_9_beta": "341f2a17-0024-46cd-968d-b4444ec3699f", 27 | "watchos_10_beta": "7ae7f3b9-886a-437f-9b22-e9f017431b0e", 28 | "watchos_11_beta": "23d7265b-1000-47cf-8d0a-07144942db9e", 29 | "watchos_26_beta": "e73d2741-8003-45cd-b909-86b9840f2ea2", 30 | 31 | "tvos_release": "356d9da0-eee4-4c6c-bbe5-99b60eadddf0", 32 | "tvos_15_beta": "4d0dcdf7-12f2-4ebf-9672-ac4a4459a8bc", 33 | "tvos_16_beta": "d6bac98b-9e2a-4f87-9aba-22c898b25d84", 34 | "tvos_17_beta": "61693fed-ab18-49f3-8983-7c3adf843913", 35 | "tvos_18_beta": "98847ed4-1c37-445c-9e7b-5b95d29281f2", 36 | "tvos_26_beta": "69cc7bd5-9ff2-4f5e-8b4f-30955542a81d", 37 | 38 | "audioos_release": "0322d49d-d558-4ddf-bdff-c0443d0e6fac", 39 | "audioos_15_beta": "58ff8d56-1d77-4473-ba88-ee1690475e40", 40 | "audioos_16_beta": "59377047-7b3f-45b9-8e99-294c0daf3c85", 41 | "audioos_17_beta": "17536d4c-1a9d-4169-bc62-920a3873f7a5", 42 | "audioos_18_beta": "bedbd9c7-738a-4060-958b-79da54a1f7ad", 43 | "audioos_26_beta": "47ed08e9-bd89-454e-938c-664029863ee8" 44 | } -------------------------------------------------------------------------------- /core/misc.js: -------------------------------------------------------------------------------- 1 | // Some miscellaneous functions 2 | 3 | const prettyBytes = require('pretty-bytes'); 4 | 5 | module.exports = function () { 6 | // turn bytes into human-readable 7 | this.formatBytes = function (bytes) { 8 | return prettyBytes(bytes); 9 | }; 10 | 11 | this.formatUpdatesName = function (updateid, version, cname) { 12 | // tvOS SUDocumentationID is always "PreRelease"... 13 | if (cname.toLowerCase() === "tvos") { 14 | return "Beta"; 15 | } 16 | 17 | if (!updateid.includes('Long') && !updateid.includes('Short') && !updateid.includes('RC')) { 18 | // Format & sanitize SUDocumentationID 19 | if (version.endsWith(".0")) version = version.substring(0, version.length - 2); 20 | let name_prefix = cname + version.replace('.', ''); 21 | var document_id = (updateid.includes(name_prefix)) ? updateid.replace(name_prefix, '') : updateid.replace(cname + version.split('.')[0], ''); 22 | 23 | document_id = document_id.replace(version.replace('.', ''), ''); // workaround for audioOS, weird 24 | 25 | // Get beta number from SUDocumentationID 26 | let beta_name = `Beta ${parseInt(document_id.replace(/[^0-9]/g, ""))}`; 27 | return beta_name; 28 | } else { 29 | // Release Candidate Updates 30 | return "Release Candidate"; 31 | } 32 | } 33 | 34 | // generate random colors for the embeds 35 | this.randomColor = function () { 36 | var color = Math.floor(Math.random() * 16777215).toString(16); 37 | return color; 38 | }; 39 | 40 | // device icons for os updates embeds 41 | this.getThumbnail = function (os) { 42 | let link = 'https://minh-ton.github.io/apple-updates/icons/'; 43 | let extension = '.png'; 44 | 45 | var thumb = link + os.toLowerCase() + extension; 46 | return thumb; 47 | }; 48 | 49 | // get time in a timezone 50 | this.getCurrentTime = function (timezone) { 51 | let nz_date_string = new Date().toLocaleString("en-US", { timeZone: timezone }); 52 | let date_nz = new Date(nz_date_string); 53 | let year = date_nz.getFullYear(); 54 | let month = ("0" + (date_nz.getMonth() + 1)).slice(-2); 55 | let date = ("0" + date_nz.getDate()).slice(-2); 56 | let hours = ("0" + date_nz.getHours()).slice(-2); 57 | let minutes = ("0" + date_nz.getMinutes()).slice(-2); 58 | let seconds = ("0" + date_nz.getSeconds()).slice(-2); 59 | let date_time = date + "/" + month + "/" + year + " " + hours + ":" + minutes + ":" + seconds; 60 | return date_time; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/apple/doc.js: -------------------------------------------------------------------------------- 1 | // Get update documentation/changelog 2 | 3 | const axios = require('axios'); 4 | const admzip = require('adm-zip'); 5 | const sanitizeHtml = require('sanitize-html'); 6 | 7 | require('../error.js')(); 8 | 9 | module.exports = function () { 10 | this.get_changelog = async function (audience, hw, sudocumentationid, device, assettype) { 11 | const res = await axios.post('https://gdmf.apple.com/v2/assets', { 12 | AssetAudience: audience, 13 | HWModelStr: hw, 14 | SUDocumentationID: sudocumentationid, 15 | CertIssuanceDay: "2019-09-06", 16 | ClientVersion: 2, 17 | DeviceName: device, 18 | AssetType: assettype, 19 | }).catch(function (error) { 20 | return send_error(error, "doc.js", `${device} changelog`, `getting changelog from apple server.`); 21 | }); 22 | 23 | if (!res) return send_error("No data available", "doc.js", `${device} changelog`, `getting changelog from apple server.`); 24 | 25 | var arr = res.data.split("."); 26 | let buff = new Buffer.from(arr[1], 'base64'); 27 | let text = JSON.parse(buff.toString('utf8')); 28 | 29 | if (!text.Assets[0]) return "Release note is not available." 30 | 31 | var file_url = `${text.Assets[0].__BaseURL}${text.Assets[0].__RelativePath}`; 32 | 33 | const file = await axios.request({ 34 | method: 'GET', 35 | url: file_url, 36 | responseType: 'arraybuffer', 37 | responseEncoding: null, 38 | }).catch(function (error) { 39 | return send_error(error, "doc.js", `extract changelog files`, `url: ${file_url}`); 40 | }); 41 | 42 | if (!file) return send_error("No changelog file available", "doc.js", `${device} changelog`, `cannot download changelog zip file.`); 43 | 44 | var zip = new admzip(file.data); 45 | var zipEntries = zip.getEntries(); 46 | 47 | let changelog = zipEntries.map(function (entry) { 48 | if (entry.entryName == "AssetData/en.lproj/ReadMeSummary.html") return entry; 49 | }).filter(function (entry) { return entry; })[0]; 50 | 51 | const clean = sanitizeHtml(zip.readAsText(changelog).replace(/(.|\n)*/, '').replace(/<\/body(.|\n)*/g, ''), { 52 | allowedTags: ['li'], 53 | }); 54 | 55 | var arr = clean.split("\r\n"); 56 | 57 | for (var i = 0; i < arr.length; i++) arr[i] = arr[i].replace(/\t/g, "").replace(/
  • /g, "- ").replace(/<[^>]+>/g, '').replace(/\&/g,'&').trimStart(); 58 | 59 | let notes = arr.join('\n').replace(/\n\s*\n\s*\n/g, '\n\n'); 60 | 61 | if (notes.length > 4000) notes = notes.substring(0, 4000) + '...\n\n*[Release notes have been truncated]*'; 62 | 63 | return notes; 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Software Updates # 5023 4 | 5 | A Discord bot for pushing notifications about new Apple OS updates. 6 | 7 | [](https://discord.com/api/oauth2/authorize?client_id=852378577063116820&permissions=8&scope=applications.commands%20bot) 8 | 9 |

    10 |
    11 |
    12 | 13 | ## 🎉 Getting Started 14 | 15 | After adding the bot to your servers, use the `/setup` command to set up the bot to your liking. 16 | 17 | **Note**: _You can also configure the bot to ping a specific role when a new Apple update comes out by using `/setup role add`_. 18 | 19 | 20 | 21 | 22 | ## 🔔 Commands 23 | 24 | ### Utilities 25 | 26 | | Command | Description | Usage | 27 | | :------------- | :---------- | :----------- | 28 | | `setup` | Configures the bot to your liking! | `/setup`: Set up the bot for the first time.
    `/setup role add`: Add a notifications role.
    `/setup role remove`: Remove a notifications role.
    `/setup role list`: List configured notifications roles. | 29 | | `help` | Shows the help menu or get help on a command. | `/help`: Shows the help menu.
    `/help [command]`: Get help on a command. | 30 | 31 | ### Apple 32 | `latest` `ipsw` `info` 33 | 34 | ### Other 35 | `about` `ping` `uptime` `invite` `source` 36 | 37 | ## 👏🏼 Contributing 38 | If you have a question, get help by joining the [Discord server](https://discord.gg/ktHmcbpMNU) or opening an issue. 39 | If you wish to contribute, feel free to fork the repository and submit a pull request! 40 | 41 | 42 | ## 🔎 Credits 43 | - [ASentientBot](https://asentientbot.github.io) and [DhinakG](https://github.com/dhinakg) for many insights on Apple's Pallas server. 44 | - Several other contributors and testers on Discord for helping me with various parts of the bot. 45 | 46 | ## 📝 License 47 | This project is licensed under the GPL-3.0 License. View [LICENSE.md](https://github.com/Minh-Ton/apple-updates/blob/bot/LICENSE.md) for more detail. 48 | -------------------------------------------------------------------------------- /cmds/utilities/help.js: -------------------------------------------------------------------------------- 1 | // Get bot help / command help 2 | 3 | const { EmbedBuilder, PermissionFlagsBits } = require('discord.js'); 4 | const { SlashCommandBuilder } = require('@discordjs/builders'); 5 | 6 | require('../../core/embed.js')(); 7 | require('../../core/misc.js')(); 8 | 9 | function help_embed(author) { 10 | const embed = new EmbedBuilder() 11 | .setTitle(`Software Updates - Help`) 12 | .setDescription(`To view more information on a command, use \`/help \`. \nNeed more help? Join our support server: https://discord.gg/ktHmcbpMNU`) 13 | .addFields( 14 | { name: `Information`, value: '`about` `ping` `uptime` `invite` `source`' }, 15 | { name: `Apple`, value: '`macos_installers` `ipsw` `update_information`' }, 16 | { name: `Utilities`, value: '`help` `setup`' } 17 | ) 18 | .setColor(randomColor()); 19 | if (author == process.env.owner_id) embed.addFields({ name: `Owner`, value: '`killall` `bash` `echo` `eval`' }); 20 | return embed; 21 | } 22 | 23 | module.exports = { 24 | name: 'help', 25 | command: 'help', 26 | category: 'Utilities', 27 | description: 'Gets help on a command.', 28 | usage: '`/help`: Shows a list of bot commands.\n`/help `: Gets help on a command.', 29 | data: new SlashCommandBuilder().setName("help").setDescription("Gets help on a command.") 30 | .addStringOption(option => option.setName('command').setDescription('View more information on a command.').setRequired(false) 31 | .addChoices( 32 | { name: "about", value: "about" }, 33 | { name: "ping", value: "ping" }, 34 | { name: "uptime", value: "uptime" }, 35 | { name: "invite", value: "invite" }, 36 | { name: "source", value: "source" }, 37 | { name: "macos_installers", value: "macos_installers" }, 38 | { name: "ipsw", value: "ipsw" }, 39 | { name: "update_information", value: "update_information" }, 40 | { name: "help", value: "help" }, 41 | { name: "setup", value: "setup" }, 42 | )), 43 | async execute(interaction) { 44 | let color = randomColor(); 45 | 46 | if (!interaction.options.getString('command')) { 47 | const embed = new EmbedBuilder().setDescription(':wave: **Hey there! If you haven\'t configured this bot, just use the `setup` command to set it up! Don\'t worry, the process is very simple and user-friendly!**').setColor(color); 48 | if (!interaction.member.permissions.has(PermissionFlagsBits.ManageGuild)) await interaction.editReply({ embeds: [help_embed(interaction.member.id)] }); 49 | else await interaction.editReply({ embeds: [help_embed(interaction.member.id), embed] }); 50 | return; 51 | } 52 | 53 | const cmd = global.bot.commands.get(interaction.options.getString('command')); 54 | 55 | const embed = new EmbedBuilder() 56 | .setTitle(`\`${cmd.command}\``) 57 | .addFields( 58 | { name: 'Description', value: cmd.description }, 59 | { name: `Category`, value: cmd.category } 60 | ) 61 | .setColor(color); 62 | if (cmd.usage) embed.addFields({ name: 'Usage', value: cmd.usage }); 63 | await interaction.editReply({ embeds: [embed] }); 64 | }, 65 | }; -------------------------------------------------------------------------------- /cmds/apple/macos_installers.js: -------------------------------------------------------------------------------- 1 | // Send macOS Installers 2 | 3 | const { EmbedBuilder } = require("discord.js"); 4 | const catalogs = require("../../assets/catalogs.json"); 5 | const { SlashCommandBuilder } = require('@discordjs/builders'); 6 | 7 | require('../../core/apple/manager.js')(); 8 | require('../../core/embed.js')(); 9 | require('../../core/misc.js')(); 10 | 11 | function isBeta(build) { 12 | if (build.length > 6 && build.toUpperCase() != build) return true; // May break in the future 13 | return false; 14 | } 15 | 16 | function get_links(xml_update) { 17 | let beta = []; let public = []; 18 | for (let update in xml_update) { 19 | let pkgurl = xml_update[update]['xml_pkg']; 20 | let version = xml_update[update]['xml_version']; 21 | let build = xml_update[update]['xml_build']; 22 | let size = xml_update[update]['xml_size']; 23 | 24 | if (isBeta(build)) beta.push(`macOS ${version} (Build ${build} - Size ${formatBytes(size)}): [InstallAssistant.pkg](${pkgurl})`); 25 | else public.push(`macOS ${version} (Build ${build} - Size ${formatBytes(size)}): [InstallAssistant.pkg](${pkgurl})`); 26 | } 27 | return [beta, public]; 28 | } 29 | 30 | module.exports = { 31 | name: 'macos_installers', 32 | command: 'macos_installers', 33 | category: 'Apple', 34 | cooldown: 60, 35 | ephemeral: false, 36 | description: 'Gets the latest macOS Full Installer Packages.', 37 | data: new SlashCommandBuilder().setName("macos_installers").setDescription("Gets the latest macOS Full Installer Packages."), 38 | async execute(interaction) { 39 | try { 40 | const processing = new EmbedBuilder().setColor(randomColor()); 41 | await interaction.editReply({ embeds: [processing.setDescription("Hang on, I'm fetching data from Apple...")] }); 42 | 43 | let installers = get_links(await get_pkg_assets(catalogs.macos_beta, 'beta_pkg')); 44 | let installers13 = get_links(await get_pkg_assets(catalogs.macos_beta_13, 'beta_pkg')); 45 | let installers12 = get_links(await get_pkg_assets(catalogs.macos_beta_12, 'beta_pkg')); 46 | let installers11 = get_links(await get_pkg_assets(catalogs.macos_beta_11, 'beta_pkg')); 47 | 48 | let installers_beta = `${installers11[0].sort().join('\n')} 49 | ${installers12[0].sort().join('\n')} 50 | ${installers13[0].sort().join('\n')} 51 | ${installers[0].sort()}`; 52 | 53 | let embed_color = randomColor(); 54 | 55 | let items_count = Math.floor(installers[1].length / 2); 56 | 57 | const public1 = new EmbedBuilder() 58 | .setTitle("macOS Full Installer Packages") 59 | .setDescription(`**Public Release Installers:**\n${installers[1].sort().slice(0, items_count).join('\n')}`) 60 | .setColor(embed_color); 61 | 62 | const public2 = new EmbedBuilder() 63 | .setDescription(`${installers[1].sort().slice(items_count, installers[1].length).join('\n')}`) 64 | .setColor(embed_color); 65 | 66 | const beta = new EmbedBuilder() 67 | .setDescription(`**Beta Release Installers:**\n${installers_beta}`) 68 | .setColor(embed_color) 69 | .setFooter({ text: interaction.user.username, iconURL: interaction.user.displayAvatarURL() }); 70 | 71 | interaction.editReply({ embeds: [public1, public2, beta] }); 72 | } catch (error) { 73 | return interaction.editReply(error_alert("Ugh, an unknown error occurred.", error)); 74 | } 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /core/apple/gdmf.js: -------------------------------------------------------------------------------- 1 | // Fetch updates from Apple's Pallas server (gdmf.apple.com) 2 | 3 | const axios = require('axios'); 4 | 5 | require('../error.js')(); 6 | require('./doc.js')(); 7 | 8 | const doc_asset_type = { 9 | ios: "com.apple.MobileAsset.SoftwareUpdateDocumentation", 10 | ipados: "com.apple.MobileAsset.SoftwareUpdateDocumentation", 11 | audioos: "com.apple.MobileAsset.SoftwareUpdateDocumentation", 12 | watchos: "com.apple.MobileAsset.WatchSoftwareUpdateDocumentation" 13 | } 14 | 15 | const device_name = { 16 | ios: "iPhone", 17 | ipados: "iPad", 18 | audioos: "AudioAccessory", 19 | watchos: "Watch" 20 | } 21 | 22 | module.exports = function () { 23 | this.gdmf_macos = async function (assetaud, build, hwm, prodtype, prodversion, beta) { 24 | const res = await axios.post('https://gdmf.apple.com/v2/assets', { 25 | AssetAudience: assetaud, 26 | CertIssuanceDay: "2020-09-29", 27 | ClientVersion: 2, 28 | AssetType: "com.apple.MobileAsset.MacSoftwareUpdate", 29 | BuildVersion: build, 30 | HWModelStr: hwm, 31 | ProductType: prodtype, 32 | ProductVersion: prodversion, 33 | }).catch(function (error) { 34 | return send_error(error, "gdmf.js", `gdmf_macos`, `politely asking gdmf.apple.com for updates`); 35 | }); 36 | 37 | if (!res) return send_error("No data available.", "gdmf.js", `gdmf_macos`, `politely asking gdmf.apple.com for updates`); 38 | 39 | var arr = res.data.split("."); 40 | let buff = new Buffer.from(arr[1], 'base64'); 41 | let text = JSON.parse(buff.toString('utf8')); 42 | 43 | let data = []; 44 | 45 | for (let asset in text.Assets) { 46 | if (!text.Assets[asset]) { 47 | return send_error(`Missing text.Asset[${asset}]`, "gdmf.js", `gdmf_macos`, `update not available for ${assetaud}.`); 48 | } 49 | 50 | var changelog; 51 | 52 | if (!beta) changelog = await get_changelog(assetaud, hwm, text.Assets[asset].SUDocumentationID, "Mac", "com.apple.MobileAsset.SoftwareUpdateDocumentation"); 53 | 54 | if (changelog == undefined) changelog = "Release note is not available."; 55 | 56 | let mac_update = { 57 | mac_pkg: `${text.Assets[asset].__BaseURL}${text.Assets[asset].__RelativePath}`, 58 | mac_version: text.Assets[asset].OSVersion, 59 | mac_build: text.Assets[asset].Build, 60 | mac_size: text.Assets[asset]._DownloadSize, 61 | mac_updateid: text.Assets[asset].SUDocumentationID, 62 | mac_changelog: changelog, 63 | mac_postdate: text.PostingDate, 64 | mac_raw_response: JSON.stringify(text.Assets[asset]) 65 | } 66 | 67 | data.push(mac_update); 68 | } 69 | 70 | return data; 71 | }; 72 | 73 | this.gdmf_other = async function (assetaud, build, hwm, prodtype, prodversion, cname, dname, beta) { 74 | const res = await axios.post('https://gdmf.apple.com/v2/assets', { 75 | AssetAudience: assetaud, 76 | CertIssuanceDay: "2020-09-29", 77 | ClientVersion: 2, 78 | AssetType: "com.apple.MobileAsset.SoftwareUpdate", 79 | BuildVersion: build, 80 | HWModelStr: hwm, 81 | ProductType: prodtype, 82 | ProductVersion: prodversion, 83 | }).catch(function (error) { 84 | return send_error(error, "gdmf.js", `fetch_other_updates - ${cname} ${dname}`, `politely asking gdmf.apple.com for updates`); 85 | }); 86 | 87 | if (!res) return send_error("No data available.", "gdmf.js", `fetch_other_updates - ${cname} ${dname}`, `politely asking gdmf.apple.com for updates`);; 88 | 89 | var arr = res.data.split("."); 90 | let buff = new Buffer.from(arr[1], 'base64'); 91 | let text = JSON.parse(buff.toString('utf8')); 92 | 93 | if (!text.Assets[0]) { 94 | return send_error(`Missing text.Asset[0]`, "gdmf.js", `gdmf_other`, `update not available for ${assetaud}.`); 95 | } 96 | 97 | var changelog; 98 | 99 | if (!beta) { 100 | (cname.toLowerCase() == "tvos") ? changelog = undefined : changelog = await get_changelog(assetaud, hwm, text.Assets[0].SUDocumentationID, device_name[cname.toLowerCase()], doc_asset_type[cname.toLowerCase()]); 101 | } 102 | 103 | if (changelog == undefined) changelog = "Release note is not available."; 104 | 105 | let os_update = { 106 | os_version: text.Assets[0].OSVersion.replace('9.9.', ''), 107 | os_build: text.Assets[0].Build, 108 | os_size: text.Assets[0]._DownloadSize, 109 | os_updateid: text.Assets[0].SUDocumentationID, 110 | os_changelog: changelog, 111 | os_postdate: text.PostingDate, 112 | os_raw_response: JSON.stringify(text.Assets[0]) 113 | } 114 | 115 | return os_update; 116 | }; 117 | }; -------------------------------------------------------------------------------- /cmds/apple/ipsw.js: -------------------------------------------------------------------------------- 1 | // Get device ipsw files 2 | 3 | const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); 4 | const axios = require('axios'); 5 | const MiniSearch = require('minisearch') 6 | const uniqid = require('uniqid'); 7 | const wait = require('util').promisify(setTimeout); 8 | const { SlashCommandBuilder } = require('@discordjs/builders'); 9 | 10 | require('../../core/misc.js')(); 11 | require('../../core/embed.js')(); 12 | 13 | async function display_results(results, index, interaction) { 14 | let ipsw = await axios.get(`https://api.ipsw.me/v4/device/${results[index].identifier}?type=ipsw`); 15 | 16 | const data = []; 17 | 18 | for (let item in ipsw.data.firmwares) { 19 | if (ipsw.data.firmwares[item].signed == true) { 20 | const url = ipsw.data.firmwares[item].url; 21 | const filename = new URL(url).pathname.split('/').pop(); 22 | const build = ipsw.data.firmwares[item].buildid; 23 | const version = ipsw.data.firmwares[item].version; 24 | 25 | data.push(`[${filename}](${url}) (Version ${version} - Build ${build})`); 26 | } 27 | } 28 | 29 | let embed = new EmbedBuilder() 30 | .setTitle(`Signed IPSW files for ${ipsw.data.name}`) 31 | .setDescription(`${data.join('\n')}`) 32 | .setColor(randomColor()) 33 | .setFooter({ text: interaction.user.username, iconURL: interaction.user.displayAvatarURL() }); 34 | 35 | return embed; 36 | } 37 | 38 | module.exports = { 39 | name: 'ipsw', 40 | command: 'ipsw', 41 | category: 'Apple', 42 | ephemeral: false, 43 | description: 'Gets the latest signed ipsw files for a device.', 44 | usage: '`/ipsw `', 45 | data: new SlashCommandBuilder().setName("ipsw").setDescription("Gets the latest signed ipsw files for a device.") 46 | .addStringOption(option => option.setName("model").setDescription("Specify device model, e.g. iPhone 13 Pro Max").setRequired(true)), 47 | async execute(interaction) { 48 | let identifier = interaction.options.getString('model'); 49 | 50 | try { 51 | const devices = `https://api.ipsw.me/v4/devices`; 52 | const device_table = []; 53 | 54 | let device_data = await axios.get(devices); 55 | 56 | for (let query in device_data.data) { 57 | let device = device_data.data[query]; 58 | device_table.push({ id: parseInt(query) + 1, name: device["name"], identifier: device["identifier"] }); 59 | } 60 | 61 | let search = new MiniSearch({ 62 | fields: ['name', 'identifier'], 63 | storeFields: ['name', 'identifier'], 64 | searchOptions: { boost: { name: 5 } } 65 | }); 66 | 67 | search.addAll(device_table); 68 | 69 | let results = search.search(identifier); 70 | 71 | var index = 0, embed = undefined; 72 | 73 | const next_id = uniqid('next-'); 74 | const prev_id = uniqid('prev-'); 75 | const cancel_id = uniqid('cancel-'); 76 | const ids = [next_id, prev_id, cancel_id]; 77 | 78 | const filter = ch => { 79 | ch.deferUpdate(); 80 | return ch.member.id == interaction.member.id && ids.includes(ch.customId); 81 | } 82 | 83 | const row = new ActionRowBuilder().addComponents( 84 | new ButtonBuilder() 85 | .setCustomId(prev_id) 86 | .setLabel('Previous') 87 | .setStyle(ButtonStyle.Primary), 88 | new ButtonBuilder() 89 | .setCustomId(cancel_id) 90 | .setLabel('Done') 91 | .setStyle(ButtonStyle.Success), 92 | new ButtonBuilder() 93 | .setCustomId(next_id) 94 | .setLabel('Next') 95 | .setStyle(ButtonStyle.Primary), 96 | ); 97 | 98 | await interaction.editReply({ embeds: [embed = await display_results(results, index, interaction)], components: [row] }); 99 | const collector = interaction.channel.createMessageComponentCollector({ filter, time: 180000 }); 100 | 101 | collector.on('collect', async action => { 102 | if (action.customId == next_id && index < results.length - 1) index++; 103 | if (action.customId == prev_id && index > 0) index--; 104 | if (action.customId == cancel_id) return collector.stop(); 105 | if (index >= 0) { 106 | embed = await display_results(results, index, interaction).catch(() => { collector.stop() }); 107 | await interaction.editReply({ embeds: [embed], components: [] }); 108 | await wait(1000); 109 | await interaction.editReply({ embeds: [embed], components: [row] }); 110 | } 111 | }); 112 | 113 | collector.on('end', async action => { 114 | await interaction.editReply({ embeds: [embed], components: [] }); 115 | }); 116 | 117 | } catch (error) { 118 | return await interaction.editReply(error_alert('Ugh, an unknown error occurred.', error)); 119 | } 120 | }, 121 | }; 122 | -------------------------------------------------------------------------------- /core/updates.js: -------------------------------------------------------------------------------- 1 | // Fetch updates for each Apple OS 2 | 3 | const devices = require("../assets/devices.json"); 4 | const catalogs = require("../assets/catalogs.json"); 5 | const audiences = require("../assets/audiences.json"); 6 | 7 | require('./apple/manager.js')(); 8 | require('./error.js')(); 9 | 10 | module.exports = function () { 11 | this.fetch_gdmf = async function (macos, ios, ipados, watchos, audioos, tvos) { // for debugging purposes 12 | global.BOT_STATUS = "Working"; 13 | 14 | // Beta macOS 15 | if (macos) await fetch_macos_updates(audiences.macos_13_beta, devices.macos.build, devices.macos.model, devices.macos.prodtype, devices.macos.version, 'beta', true); // macOS Ventura Beta 16 | if (macos) await fetch_macos_updates(audiences.macos_14_beta, devices.macos.build, devices.macos.model, devices.macos.prodtype, devices.macos.version, 'beta', true); // macOS Sonoma Beta 17 | if (macos) await fetch_macos_updates(audiences.macos_15_beta, devices.macos.build, devices.macos.model, devices.macos.prodtype, devices.macos.version, 'beta', true); // macOS Squoia Beta 18 | if (macos) await fetch_macos_updates(audiences.macos_26_beta, devices.macos.build, devices.macos.model, devices.macos.prodtype, devices.macos.version, 'beta', true); // macOS Tahoe Beta 19 | 20 | // Public macOS 21 | if (macos) await fetch_macos_updates(audiences.macos_release, devices.macos.build, devices.macos.model, devices.macos.prodtype, devices.macos.version, 'public', false); // macOS Release 22 | 23 | // Beta iOS 24 | if (ios) await fetch_other_updates(audiences.ios_18_beta, devices.ios.build, devices.ios.model, devices.ios.prodtype, devices.ios.version, "iOS", "beta", true); // iOS 18 Beta 25 | if (ios) await fetch_other_updates(audiences.ios_26_beta, devices.ios.build, devices.ios.model, devices.ios.prodtype, devices.ios.version, "iOS", "beta", true); // iOS 26 Beta 26 | 27 | // Public iOS 28 | if (ios) await fetch_other_updates(audiences.ios_release, "19G71", "D101AP", "iPhone9,3", "15.6", "iOS", "public", false); // iOS 15 29 | if (ios) await fetch_other_updates(audiences.ios_release, devices.ios.build, devices.ios.model, devices.ios.prodtype, devices.ios.version, "iOS", "public", false); // iOS Release 30 | 31 | // Beta iPadOS 32 | if (ipados) await fetch_other_updates(audiences.ios_18_beta, devices.ipados.build, devices.ipados.model, devices.ipados.prodtype, devices.ipados.version, "iPadOS", "beta", true); // iPadOS 18 Beta 33 | if (ipados) await fetch_other_updates(audiences.ios_26_beta, devices.ipados.build, devices.ipados.model, devices.ipados.prodtype, devices.ipados.version, "iPadOS", "beta", true); // iPadOS 26 Beta 34 | 35 | // Public iPadOS 36 | if (ipados) await fetch_other_updates(audiences.ios_release, "19G71", "J81AP", "iPad5,3", "15.6", "iPadOS", "public", false); // iPadOS 15 37 | if (ipados) await fetch_other_updates(audiences.ios_release, devices.ipados.build, devices.ipados.model, devices.ipados.prodtype, devices.ipados.version, "iPadOS", "public", false); // iPadOS Release 38 | 39 | // Beta watchOS 40 | if (watchos) await fetch_other_updates(audiences.watchos_11_beta, devices.watchos.build, devices.watchos.model, devices.watchos.prodtype, devices.watchos.version, "watchOS", "beta", true); // watchOS 11 Beta 41 | if (watchos) await fetch_other_updates(audiences.watchos_26_beta, devices.watchos.build, devices.watchos.model, devices.watchos.prodtype, devices.watchos.version, "watchOS", "beta", true); // watchOS 26 Beta 42 | 43 | // Public watchOS 44 | if (watchos) await fetch_other_updates(audiences.watchos_release, devices.watchos.build, devices.watchos.model, devices.watchos.prodtype, devices.watchos.version, "watchOS", "public", false); // watchOS Release 45 | 46 | // Beta audioOS 47 | if (audioos) await fetch_other_updates(audiences.audioos_18_beta, devices.audioos.build, devices.audioos.model, devices.audioos.prodtype, devices.audioos.version, "audioOS", "beta", true); // audioOS 18 Beta 48 | if (audioos) await fetch_other_updates(audiences.audioos_26_beta, devices.audioos.build, devices.audioos.model, devices.audioos.prodtype, devices.audioos.version, "audioOS", "beta", true); // audioOS 26 Beta 49 | 50 | // Public audioOS 51 | if (audioos) await fetch_other_updates(audiences.audioos_release, devices.audioos.build, devices.audioos.model, devices.audioos.prodtype, devices.audioos.version, "audioOS", "public", false); // audioOS Release 52 | 53 | // Beta tvOS 54 | if (tvos) await fetch_other_updates(audiences.tvos_18_beta, devices.tvos.build, devices.tvos.model, devices.tvos.prodtype, devices.tvos.version, "tvOS", "beta", true); // tvOS 18 Beta 55 | if (tvos) await fetch_other_updates(audiences.tvos_26_beta, devices.tvos.build, devices.tvos.model, devices.tvos.prodtype, devices.tvos.version, "tvOS", "beta", true); // tvOS 26 Beta 56 | 57 | // Public tvOS 58 | if (tvos) await fetch_other_updates(audiences.tvos_release, devices.tvos.build, devices.tvos.model, devices.tvos.prodtype, devices.tvos.version, "tvOS", "public", false); // tvOS Release 59 | 60 | global.BOT_STATUS = "Idling"; 61 | }; 62 | 63 | this.fetch_xml = async function () { 64 | global.BOT_STATUS = "Working"; 65 | 66 | await fetch_macos_pkg(catalogs.macos_beta, true, 'beta_pkg'); 67 | await fetch_macos_pkg(catalogs.macos_public, false, 'public_pkg'); 68 | 69 | global.BOT_STATUS = "Idling"; 70 | }; 71 | } -------------------------------------------------------------------------------- /cmds/setup/helper/misc/embeds.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); 2 | 3 | module.exports = function () { 4 | this.updates_part_1 = function(existing_setup) { 5 | const setup = (existing_setup.length > 0) ? `\n:warning: Your server has already set up receiving _${ existing_setup.join(", ") }_.\n***The previous configuration will be deleted after running this command.***\n\n` : ""; 6 | const part1 = new EmbedBuilder() 7 | .setTitle(`Software Updates - Updates Setup Part 1`) 8 | .setColor("#00d768") 9 | .setDescription(`\n**Please select the channel that you want me to send new Apple updates to.**\n${setup}*If you don't select within 1 minute, the command will time out.*`); 10 | return part1; 11 | } 12 | 13 | this.updates_part_2 = function(selected_channel, selected_options) { 14 | const part2 = new EmbedBuilder() 15 | .setTitle(`Software Updates - Updates Setup Part 2`) 16 | .setDescription(`\n ** React to receive updates notifications to <#${selected_channel.id}>.** 17 | *If you don't react to this message within 1 minute, the command will time out. Your options will be recorded automatically after 1 minute.*`) 18 | .setColor("#00d768") 19 | .addFields( 20 | { name: `OS Updates`, value: `<:iphone:852824816092315678> iOS Updates\n<:ipad:852824860089516033> iPadOS Updates\n<:macbook:852826607286878228> macOS Updates\n`, inline: true }, 21 | { name: `OS Updates`, value: `<:apple_watch:852824628921499688> watchOS Updates\n<:homepod:852824690166333440> audioOS Updates\n<:apple_tv:852826560725778442> tvOS Updates\n`, inline: true }, 22 | { name: `Package Links`, value: `<:installassistant:852824497202659348> macOS InstallAssistant.pkg Links\n`, inline: true }, 23 | { name: `Bot Updates`, value: `<:software_updates:852825269705113610> <@${global.bot.user.id}>'s new features and bug fixes announcements\n` }, 24 | { name: "Selected Options", value: selected_options + "." } 25 | ); 26 | return part2; 27 | } 28 | 29 | this.updates_overall = function(selected_channel, selected_options) { 30 | const overall = new EmbedBuilder() 31 | .setTitle(`Software Updates - Setup Overview`) 32 | .setDescription(`**Your setup data has been saved successfully!**`) 33 | .addFields( 34 | { name: `Selected channel`, value: selected_channel, inline: true }, 35 | { name: `Selected updates`, value: selected_options + ".", inline: true } 36 | ) 37 | .setColor("#1c95e0") 38 | .setTimestamp(); 39 | return overall; 40 | } 41 | 42 | 43 | this.roles_part_1 = function () { 44 | const embed = new EmbedBuilder() 45 | .setTitle(`Software Updates - Notification Roles Setup Part 1`) 46 | .setColor("#00d768") 47 | .setDescription(`\n**Please select an update name that you would like to get ping notifications for.**\n*If you don't select within 1 minute, the command will time out.*`); 48 | return embed; 49 | } 50 | 51 | this.roles_part_2 = function (os) { 52 | const embed = new EmbedBuilder() 53 | .setTitle(`Software Updates - Notification Roles Setup Part 2`) 54 | .setColor("#00d768") 55 | .setDescription(`\n**Please select the role that you would like me to ping when a new ${os} is available.**\n*If you don't select within 1 minute, the command will time out.*`); 56 | return embed; 57 | } 58 | 59 | this.roles_overall = function (selected_role, selected_update, option) { 60 | const button = new ActionRowBuilder().addComponents( 61 | new ButtonBuilder() 62 | .setURL("https://discord.gg/ktHmcbpMNU") 63 | .setLabel('Join support server') 64 | .setStyle(ButtonStyle.Link)); 65 | choice = (option) ? "will ping" : "will no longer ping"; 66 | const overall = new EmbedBuilder() 67 | .setTitle(`Software Updates - Setup Overview`) 68 | .setColor("#1c95e0") 69 | .setDescription(`**Your setup data has been saved successfully!**\nFrom now on, I ${choice} ${selected_role} when a new ${selected_update} is available!`) 70 | .setTimestamp(); 71 | return { embeds: [overall], components: [button] }; 72 | } 73 | 74 | this.roles_remove = function (roles) { 75 | const embed = new EmbedBuilder() 76 | .setTitle(`Software Updates - Notification Roles Removal`) 77 | .setColor("#00d768") 78 | .setDescription(`\n**Please select an update name that you would like to remove ping notifications for.**\nYour server has these notification roles configured: 79 | - ${roles.join(`\n - `)}\n*If you don't select within 1 minute, the command will time out.*`); 80 | return embed; 81 | } 82 | 83 | this.roles_list = function (roles) { 84 | const embed = new EmbedBuilder() 85 | .setTitle(`Software Updates - Configured Notification Roles`) 86 | .setColor("#00d768") 87 | .setDescription(`\n**Your server has these notification roles configured:** 88 | - ${roles.join(`\n - `)}`); 89 | return { embeds: [embed] }; 90 | } 91 | 92 | this.error_embed = function(content) { 93 | const button = new ActionRowBuilder().addComponents( 94 | new ButtonBuilder() 95 | .setURL("https://discord.gg/ktHmcbpMNU") 96 | .setLabel('Join support server') 97 | .setStyle(ButtonStyle.Link)); 98 | const error = new EmbedBuilder() 99 | .setTitle("An issue has occured!") 100 | .setColor("#c2002a") 101 | .setDescription(`${content} \n\n *Please try again later.\nIf you need help, join our support server: https://discord.gg/ktHmcbpMNU*`); 102 | return { embeds: [error], components: [button] }; 103 | } 104 | } -------------------------------------------------------------------------------- /cmds/setup/helper/roles.js: -------------------------------------------------------------------------------- 1 | const { ActionRowBuilder, StringSelectMenuBuilder, PermissionFlagsBits, ComponentType } = require('discord.js'); 2 | const firebase = require("firebase-admin"); 3 | const uniqid = require('uniqid'); 4 | 5 | const db = firebase.firestore(); 6 | 7 | require("./misc/embeds.js")(); 8 | 9 | const os_updates = { 10 | ios: "iOS Updates", 11 | ipados: "iPadOS Updates", 12 | watchos: "watchOS Updates", 13 | macos: "macOS Updates", 14 | tvos: "tvOS Updates", 15 | audioos: "audioOS Updates", 16 | bot: `Bot Announcements`, 17 | pkg: "macOS InstallAssistant.pkg Links", 18 | } 19 | 20 | const database = db.collection('discord').doc('roles').collection('servers'); 21 | 22 | module.exports = function () { 23 | this.setup_roles = async function (interaction) { 24 | const sessionIDs = []; 25 | 26 | var sessionID = uniqid(); 27 | sessionIDs.push(sessionID); 28 | 29 | if (interaction.options.getString('option').includes("add")) { 30 | const os_components = []; 31 | for (let os in os_updates) { os_components.push({ "label": os_updates[os], "value": os}); } 32 | const os_input = new ActionRowBuilder().addComponents(new StringSelectMenuBuilder().setCustomId(sessionID).setPlaceholder('Nothing selected').addOptions(os_components)); 33 | 34 | await interaction.editReply({ embeds: [roles_part_1()], components: [os_input] }); 35 | 36 | } else if (interaction.options.getString('option').includes("remove")) { 37 | const os_components = []; 38 | let roles = await database.doc(interaction.member.guild.id).get(); 39 | let data = roles.data(); 40 | let arr = []; 41 | for (let os in data) { 42 | arr.push(`**${os_updates[os]}**: <@&${data[os]}>`); 43 | os_components.push({ "label": os_updates[os], "value": os, "description": "@" + interaction.member.guild.roles.cache.get(data[os]).name }); 44 | } 45 | 46 | if (arr.length < 1) return interaction.editReply(error_embed(`Your server has no notification roles configured!`)); 47 | const os_input = new ActionRowBuilder().addComponents(new StringSelectMenuBuilder().setCustomId(sessionID).setPlaceholder('Nothing selected').addOptions(os_components)); 48 | 49 | await interaction.editReply({ embeds: [roles_remove(arr)], components: [os_input] }); 50 | 51 | } else if (interaction.options.getString('option').includes("list")) { 52 | let roles = await database.doc(interaction.member.guild.id).get(); 53 | let data = roles.data(); 54 | let arr = []; 55 | for (let os in data) arr.push(`**${os_updates[os]}**: <@&${data[os]}> (${os_updates[os]})`); 56 | if (arr.length < 1) return interaction.editReply(error_embed(`Your server has no notification roles configured!`)); 57 | return interaction.editReply(roles_list(arr)); 58 | } 59 | 60 | const filter = ch => { 61 | ch.deferUpdate(); 62 | return ch.member.id == interaction.member.id && sessionIDs.includes(ch.customId); 63 | } 64 | 65 | const os_response = await interaction.channel.awaitMessageComponent({ 66 | filter: filter, 67 | max: 1, 68 | componentType: ComponentType.StringSelect, 69 | time: 60000 70 | }).catch(err => { return; }); 71 | if (os_response == undefined) return interaction.editReply(error_embed("You did not select an update name within 1 minute so the command was cancelled.")); 72 | 73 | const selected_os = os_response.values[0]; 74 | 75 | const role_components = []; 76 | const role_list = interaction.member.guild.roles.cache.filter(ch => ch.name != '@everyone'); 77 | 78 | role_list.forEach(role => { role_components.push({ "label": `@${role.name}`, "value": role.id }); }); 79 | 80 | if (role_components.length < 1) return interaction.editReply(error_embed(`Seems like your server does not have any roles or I do not have the necessary permissions to get your server's role list.`)); 81 | 82 | const role_multiple_components = []; 83 | 84 | while (role_components.length) { 85 | var sessionID = uniqid(); sessionIDs.push(sessionID); 86 | role_multiple_components.push(new ActionRowBuilder().addComponents(new StringSelectMenuBuilder().setCustomId(sessionID).setPlaceholder('No role selected').addOptions(role_components.splice(0, 20)))); 87 | } 88 | 89 | var selected_role = undefined; 90 | 91 | if (interaction.options.getString('option').includes("add")) { 92 | await interaction.editReply({ embeds: [roles_part_2(os_updates[selected_os].slice(0, -1))], components: role_multiple_components }); 93 | 94 | const role_response = await interaction.channel.awaitMessageComponent({ 95 | filter: filter, 96 | max: 1, 97 | componentType: ComponentType.StringSelect, 98 | time: 60000 99 | }).catch(err => { return; }); 100 | if (role_response == undefined) return interaction.editReply(error_embed("You did not select a role within 1 minute so the command was cancelled.")); 101 | 102 | selected_role = interaction.member.guild.roles.cache.get(role_response.values[0]); 103 | 104 | let roles_database = await database.doc(interaction.member.guild.id).get(); 105 | let roles_data = roles_database.data(); 106 | 107 | (roles_data == undefined) ? await database.doc(interaction.member.guild.id).set({ 108 | [`${selected_os}`]: `${selected_role.id}` 109 | }) : await database.doc(interaction.member.guild.id).update({ 110 | [`${selected_os}`]: `${selected_role.id}` 111 | }); 112 | 113 | } else { 114 | let doc = await database.doc(interaction.member.guild.id).get(); 115 | let data = doc.data(); 116 | let role_id = data[selected_os]; 117 | 118 | selected_role = interaction.member.guild.roles.cache.get(role_id); 119 | 120 | await database.doc(interaction.member.guild.id).update({ 121 | [`${selected_os}`]: firebase.firestore.FieldValue.delete() 122 | }); 123 | } 124 | 125 | return interaction.editReply(roles_overall(`<@&${(selected_role) ? selected_role.id : "0100"}>`, os_updates[selected_os].slice(0, -1), interaction.options.getString('option').includes("add"))); 126 | } 127 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Created by Minh on May 19th, 2021 2 | 3 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; 4 | 5 | require('dotenv').config() 6 | 7 | global.BETA_RELEASE = process.env.NODE_ENV != "production"; 8 | global.UPDATE_MODE = false; 9 | global.SAVE_MODE = false; 10 | global.CPU_USAGE = process.cpuUsage(); 11 | 12 | const { Client, Collection, GatewayIntentBits, Partials, ActivityType } = require('discord.js'); 13 | const fs = require("fs"); 14 | const firebase = require("firebase-admin"); 15 | const { REST } = require('@discordjs/rest'); 16 | const { Routes } = require('discord-api-types/v10'); 17 | 18 | var firebase_token = (BETA_RELEASE) ? process.env.firebase_beta : process.env.firebase; 19 | var bot_token = (BETA_RELEASE) ? process.env.bot_beta_token : process.env.bot_token; 20 | 21 | firebase.initializeApp({ 22 | credential: firebase.credential.cert(JSON.parse(firebase_token)) 23 | }); 24 | 25 | require("./core/updates.js")(); 26 | require("./core/embed.js")(); 27 | 28 | global.bot = new Client({ 29 | intents: [ 30 | GatewayIntentBits.Guilds, 31 | GatewayIntentBits.GuildMessages, 32 | GatewayIntentBits.DirectMessages, 33 | GatewayIntentBits.GuildMessageReactions 34 | ], 35 | partials: [Partials.Channel] 36 | }); 37 | global.bot.login(bot_token); 38 | 39 | // ============= DISCORD BOT ============ 40 | 41 | global.bot.on("ready", async () => { 42 | if (global.bot.user.id == process.env.beta_id) console.log("[RUNNING BETA BOT INSTANCE]"); 43 | console.log(`Logged in as ${global.bot.user.tag}!`); 44 | console.log(`Currently in ${global.bot.guilds.cache.size} servers!`); 45 | console.log('Bot has started!'); 46 | setInterval(() => { 47 | if (global.BOT_STATUS == "Working") { 48 | global.bot.user.setActivity(`for updates`, { type: ActivityType.Watching }); 49 | } else { 50 | global.bot.user.setActivity(`/help | ${global.bot.guilds.cache.size}`, { type: ActivityType.Watching }); 51 | } 52 | }, 5000); 53 | }); 54 | 55 | global.bot.commands = new Collection(); 56 | global.bot.cooldowns = new Collection(); 57 | 58 | const commands = fs.readdirSync('./cmds'); 59 | const command_collection = []; 60 | 61 | for (const category of commands) { 62 | const cmd_files = fs.readdirSync(`./cmds/${category}`).filter(file => file.endsWith('.js')); 63 | for (const file of cmd_files) { 64 | const command = require(`./cmds/${category}/${file}`); 65 | global.bot.commands.set(command.name, command); 66 | if (category == "owner") continue; 67 | command_collection.push(command.data.toJSON()); 68 | } 69 | } 70 | 71 | const rest = new REST({ version: '10' }).setToken(bot_token); 72 | (async () => { 73 | try { 74 | if (global.BETA_RELEASE) await rest.put(Routes.applicationGuildCommands(process.env.beta_id, process.env.server_id), { body: command_collection }); 75 | else await rest.put(Routes.applicationCommands(process.env.client_id), { body: command_collection }); 76 | } catch (error) { 77 | console.error(error); 78 | } 79 | })(); 80 | 81 | global.bot.on('interactionCreate', async interaction => { 82 | if (!interaction.isCommand()) return; 83 | if (!interaction.guildId) return interaction.reply(error_alert(`I am unable to run this command in a DM.`)); 84 | 85 | // Get command 86 | const cmd = global.bot.commands.get(interaction.commandName); 87 | if (!cmd) return; 88 | 89 | await interaction.deferReply({ ephemeral: (cmd.ephemeral != undefined) ? cmd.ephemeral : true }); 90 | 91 | // Command cooldowns 92 | if (interaction.member.id != process.env.owner_id) { 93 | const { cooldowns } = global.bot; 94 | if (!cooldowns.has(cmd.name)) cooldowns.set(cmd.name, new Collection()); 95 | const now = Date.now(), timestamps = cooldowns.get(cmd.name), amount = (cmd.cooldown || 4) * 1000; 96 | if (timestamps.has(interaction.member.id)) { 97 | const exp_time = timestamps.get(interaction.member.id) + amount; 98 | if (now < exp_time) { 99 | const remaining = (exp_time - now) / 1000; 100 | return interaction.editReply(error_alert(`I need to rest a little bit! Please wait **${remaining.toFixed(0)} more seconds** to use \`${cmd.name}\`!`)); 101 | } 102 | } 103 | timestamps.set(interaction.member.id, now); 104 | setTimeout(() => timestamps.delete(interaction.member.id), amount); 105 | } 106 | 107 | // Execute command 108 | try { 109 | await cmd.execute(interaction); 110 | } catch (e) { 111 | console.error(e); 112 | await interaction.editReply(error_alert(`An unknown error occured while running \`${cmd.name}\`.`, e)); 113 | } 114 | }); 115 | 116 | global.bot.on("messageCreate", async message => { 117 | if (message.author.bot) return; 118 | if (message.mentions.everyone) return; 119 | if (message.channel.isDMBased()) return; 120 | 121 | // Bot prefix 122 | const prefixes = [`<@${global.bot.user.id}>`, `<@!${global.bot.user.id}>`]; 123 | if (!message.mentions.has(global.bot.user)) return; 124 | if (!prefixes.includes(message.content.split(" ")[0])) return; 125 | 126 | // Run bot commands with @mention - Owner only 127 | let isBotOwner = message.author.id == process.env.owner_id; 128 | if (!isBotOwner) return; 129 | 130 | // Get commands 131 | const args = message.content.substring(message.content.indexOf(" ") + 1).trim().split(/ +/g); 132 | const command = args.shift().toLowerCase(); 133 | const cmd = global.bot.commands.get(command); 134 | if (!cmd) return; 135 | 136 | // For use with eval, echo, bash, etc 137 | if (!["bash", "echo", "eval", "killall"].includes(cmd.name)) return; 138 | 139 | // Run commands 140 | try { 141 | cmd.execute(message, args); 142 | } catch (error) { 143 | console.error(error); 144 | message.reply(error_alert(error)); 145 | } 146 | }); 147 | 148 | // ============= UPDATES FETCH ============= 149 | 150 | fetch_gdmf(true, true, true, true, true, true); 151 | fetch_xml(); 152 | 153 | setInterval(() => fetch_gdmf(true, true, true, true, true, true), 60000); 154 | setInterval(() => fetch_xml(), 180000); 155 | -------------------------------------------------------------------------------- /core/apple/manager.js: -------------------------------------------------------------------------------- 1 | // Monitor the updates check process. 2 | 3 | const firebase = require("firebase-admin"); 4 | 5 | require('../embed.js')(); 6 | require('../error.js')(); 7 | require('../misc.js')(); 8 | require('./doc.js')(); 9 | require('./gdmf.js')(); 10 | require('./xml.js')(); 11 | require('./info.js')(); 12 | 13 | let db = firebase.firestore(); 14 | 15 | module.exports = function () { 16 | this.fetch_macos_updates = async function (assetaud, build, hwm, prodtype, prodversion, dname, beta) { 17 | let mac_update = await gdmf_macos(assetaud, build, hwm, prodtype, prodversion, beta); 18 | 19 | if (!mac_update || mac_update.length == 0) return send_error(`No asset available.`, "manager.js", `fetch_macos_ota`, `update not available for ${assetaud}.`); 20 | 21 | for (let item in mac_update) { 22 | const macos_database = db.collection("macos").doc(dname); 23 | const macos_data = await macos_database.get(); 24 | const macos_build_array = macos_data.data(); 25 | 26 | var updates = []; 27 | for (let build in macos_build_array) updates.push(build); 28 | 29 | const version = mac_update[item]['mac_version']; 30 | const size = mac_update[item]['mac_size']; 31 | const build = mac_update[item]['mac_build']; 32 | const updateid = mac_update[item]['mac_updateid']; 33 | const changelog = mac_update[item]['mac_changelog']; 34 | const postdate = mac_update[item]['mac_postdate']; 35 | const raw_response = mac_update[item]['mac_raw_response']; 36 | 37 | if (global.SAVE_MODE) { 38 | console.log("[BUILD_DATABASE] - UPLOADING UPDATE OF MACOS"); 39 | console.log(`[BUILD_DATABASE] MACOS ${version} (${build})`); 40 | await save_update("macos", version, size, build, updateid, changelog, postdate, raw_response, beta); 41 | continue; 42 | } 43 | 44 | if (updates.includes(build)) return; 45 | 46 | (beta) ? send_macos_beta(version, build, size, formatUpdatesName(updateid, version, "macOS")) : send_macos_public(version, build, size, changelog); 47 | 48 | db.collection("macos").doc(dname).update({ 49 | [`${build}`]: `${build}` 50 | }).catch(err => { 51 | send_error(err, "manager.js", `fetch_macos_ota`, `adding new build number to the database`); 52 | }); 53 | } 54 | }; 55 | 56 | this.fetch_other_updates = async function (assetaud, os_build, hwm, prodtype, prodversion, cname, dname, beta) { 57 | let os_update = await gdmf_other(assetaud, os_build, hwm, prodtype, prodversion, cname, dname, beta); 58 | 59 | if (!os_update) return send_error(`No asset (${cname} - ${dname}) available.`, "manager.js", `fetch_other_updates`, `update not available for ${assetaud}.`); 60 | 61 | const os_database = db.collection(cname.toLowerCase()).doc(dname); 62 | const os_data = await os_database.get(); 63 | const os_build_array = os_data.data(); 64 | 65 | var updates = []; 66 | for (let build in os_build_array) updates.push(build); 67 | 68 | const version = os_update['os_version']; 69 | const size = os_update['os_size']; 70 | const build = os_update['os_build']; 71 | const updateid = os_update['os_updateid']; 72 | const changelog = os_update['os_changelog']; 73 | const postdate = os_update['os_postdate']; 74 | const raw_response = os_update['os_raw_response']; 75 | 76 | if (global.SAVE_MODE) { 77 | console.log("[BUILD_DATABASE] - UPLOADING UPDATE OF " + cname.toUpperCase()); 78 | console.log(`[BUILD_DATABASE] ${cname.toUpperCase()} ${version} (${build})`); 79 | return await save_update(cname, version, size, build, updateid, changelog, postdate, raw_response, beta); 80 | } 81 | 82 | if (updates.includes(build)) return; 83 | 84 | (beta) ? send_other_beta_updates(cname, version, build, size, formatUpdatesName(updateid, version, cname)) : send_other_updates(cname, version, build, size, changelog); 85 | 86 | db.collection(cname.toLowerCase()).doc(dname).update({ 87 | [`${build}`]: `${build}` 88 | }).catch(err => { 89 | send_error(err, "manager.js", `fetch_other_updates - ${cname} ${dname}`, `adding new build number to the database`); 90 | }); 91 | }; 92 | 93 | this.fetch_macos_pkg = async function (url, beta, dname) { 94 | let xml_update = await fetch_macos_xml(url, dname); 95 | 96 | if (!xml_update || xml_update.length == 0) return send_error(`No asset (macos - ${dname}) available.`, "manager.js", `fetch_macos_pkg`, `update not available for ${url}.`); 97 | 98 | const os_database = db.collection("macos").doc(dname); 99 | const os_data = await os_database.get(); 100 | const os_build_array = os_data.data(); 101 | 102 | var updates = []; 103 | for (let build in os_build_array) updates.push(build); 104 | 105 | for (let update in xml_update) { 106 | let pkgurl = xml_update[update]['xml_pkg']; 107 | let version = xml_update[update]['xml_version']; 108 | let build = xml_update[update]['xml_build']; 109 | let size = xml_update[update]['xml_size']; 110 | let postdate = xml_update[update]['xml_postdate']; 111 | 112 | if (global.SAVE_MODE) { 113 | console.log("[BUILD_DATABASE] - UPLOADING PACKAGE OF MACOS"); 114 | console.log(`[BUILD_DATABASE] MACOS ${version} (${build})`); 115 | await save_package("macos", build, version, size, pkgurl, postdate, beta); 116 | continue; 117 | } 118 | 119 | if (updates.includes(build)) continue; 120 | 121 | (beta) ? send_macos_pkg_beta(pkgurl, version, build, size) : send_macos_pkg_public(pkgurl, version, build, size); 122 | 123 | db.collection("macos").doc(dname).update({ 124 | [`${build}`]: `${build}` 125 | }).catch(err => { 126 | send_error(err, "manager.js", `fetch_macos_pkg - ${dname}`, `adding new build number to the database`); 127 | }); 128 | 129 | } 130 | } 131 | 132 | this.get_pkg_assets = async function (url, dname) { 133 | let xml_update = await fetch_macos_xml(url, dname); 134 | return xml_update; 135 | }; 136 | } -------------------------------------------------------------------------------- /core/embed.js: -------------------------------------------------------------------------------- 1 | // Create update embeds 2 | 3 | const { EmbedBuilder, ButtonBuilder, ActionRowBuilder, ButtonStyle } = require('discord.js'); 4 | const brightColor = require('randomcolor'); 5 | 6 | let multi_icons = ['ios', 'ipados', 'watchos', 'macos', 'tvos']; 7 | 8 | require('./misc.js')(); 9 | require('./send.js')(); 10 | 11 | function isBeta(build) { 12 | if (build.length > 6 && build.toUpperCase() != build) return true; // May break in the future 13 | return false; 14 | } 15 | 16 | module.exports = function () { 17 | // Send macOS public InstallAssistant.pkg link 18 | this.send_macos_pkg_public = function (pkgurl, version, build, size) { 19 | const embed = new EmbedBuilder() 20 | .setTitle(`New macOS Full Installer Package!`) 21 | .addFields( 22 | { name: `Version`, value: `${version}`, inline: true }, 23 | { name: `Build`, value: build, inline: true }, 24 | { name: `Size`, value: formatBytes(size), inline: true } 25 | ) 26 | .setDescription(`**Installer Package**: [InstallAssistant.pkg](${pkgurl})`) 27 | .setThumbnail(getThumbnail("pkg")) 28 | .setColor(brightColor()) 29 | .setTimestamp(); 30 | send_to_servers('pkg', embed, `${version} (${build})`); 31 | }; 32 | 33 | // Send macOS beta InstallAssistant.pkg link 34 | this.send_macos_pkg_beta = function (pkgurl, version, build, size) { 35 | const embed = new EmbedBuilder() 36 | .setTitle(`New macOS Full Installer Package!`) 37 | .addFields( 38 | { name: `Version`, value: `${version} ${isBeta(build) ? "(Beta)" : "(RC)"}`, inline: true }, 39 | { name: `Build`, value: build, inline: true }, 40 | { name: `Size`, value: formatBytes(size), inline: true } 41 | ) 42 | .setDescription(`**Installer Package**: [InstallAssistant.pkg](${pkgurl})`) 43 | .setThumbnail(getThumbnail("pkg")) 44 | .setColor(brightColor()) 45 | .setTimestamp(); 46 | send_to_servers('pkg', embed, `${version} Beta (${build})`); 47 | }; 48 | 49 | // Send new macOS beta releases 50 | this.send_macos_beta = function (version, build, size, updateid, changelog) { 51 | const embed = new EmbedBuilder() 52 | .setTitle(`New macOS Beta Release!`) 53 | .addFields( 54 | { name: `Version`, value: `${version} (${updateid})`, inline: true }, 55 | { name: `Build`, value: build, inline: true }, 56 | { name: `Size`, value: formatBytes(size), inline: true } 57 | ) 58 | .setThumbnail(getThumbnail("macOS" + version.split('.')[0])) 59 | .setColor(brightColor()) 60 | .setTimestamp(); 61 | send_to_servers('macos', embed, `${version} (${updateid} - Build ${build})`); 62 | }; 63 | 64 | // Send new macOS public releases 65 | this.send_macos_public = function (version, build, size, changelog) { 66 | const embed = new EmbedBuilder() 67 | .setTitle(`New macOS Public Release!`) 68 | .addFields( 69 | { name: `Version`, value: `${version}`, inline: true }, 70 | { name: `Build`, value: build, inline: true }, 71 | { name: `Size`, value: formatBytes(size), inline: true } 72 | ) 73 | .setThumbnail(getThumbnail("macOS" + version.split('.')[0])) 74 | .setDescription(changelog) 75 | .setColor(brightColor()) 76 | .setTimestamp(); 77 | send_to_servers('macos', embed, `${version} (${build})`); 78 | }; 79 | 80 | // Send other OS updates 81 | this.send_other_updates = function (os, version, build, size, changelog) { 82 | const thumb = (multi_icons.includes(os.toLowerCase())) ? getThumbnail(os + version.split('.')[0]) : getThumbnail(os); 83 | const embed = new EmbedBuilder() 84 | .setTitle(`New ${os} Public Release!`) 85 | .addFields( 86 | { name: `Version`, value: `${version}`, inline: true }, 87 | { name: `Build`, value: build, inline: true }, 88 | { name: `Size`, value: formatBytes(size), inline: true } 89 | ) 90 | .setThumbnail(thumb) 91 | .setDescription(changelog) 92 | .setColor(brightColor()) 93 | .setTimestamp(); 94 | send_to_servers(os, embed, `${version} (${build})`); 95 | }; 96 | 97 | // Send other OS beta updates 98 | this.send_other_beta_updates = function (os, version, build, size, updateid) { 99 | const thumb = (multi_icons.includes(os.toLowerCase())) ? getThumbnail(os + version.split('.')[0]) : getThumbnail(os); 100 | const embed = new EmbedBuilder() 101 | .setTitle(`New ${os} Beta Release!`) 102 | .addFields( 103 | { name: `Version`, value: `${version} (${updateid})`, inline: true }, 104 | { name: `Build`, value: build, inline: true }, 105 | { name: `Size`, value: formatBytes(size), inline: true } 106 | ) 107 | .setThumbnail(thumb) 108 | .setColor(brightColor()) 109 | .setTimestamp(); 110 | send_to_servers(os, embed, `${version} (${updateid} - Build ${build})`); 111 | }; 112 | 113 | // Send announcements 114 | this.send_announcements = function (title, message) { 115 | const embed = new EmbedBuilder() 116 | .setTitle(title) 117 | .setColor(brightColor()) 118 | .setDescription(message) 119 | .setThumbnail(global.bot.user.displayAvatarURL()) 120 | .setTimestamp(); 121 | send_to_servers("bot", embed); 122 | }; 123 | 124 | this.error_alert = function (message, report) { 125 | const embeds = []; 126 | const button = new ActionRowBuilder().addComponents( 127 | new ButtonBuilder() 128 | .setURL("https://discord.gg/ktHmcbpMNU") 129 | .setLabel('Join support server to ask for help') 130 | .setStyle(ButtonStyle.Link)); 131 | 132 | const alert = new EmbedBuilder().setDescription("<:apple_x:869128016494751755> " + message).setColor("#FF0000"); 133 | embeds.push(alert); 134 | 135 | if (report != undefined) { 136 | const error_report = new EmbedBuilder() 137 | .setDescription(`Please report this incident in the support server:\n\`\`\`${report}\`\`\``) 138 | .setColor("#FF0000"); 139 | embeds.push(error_report); 140 | } 141 | 142 | return { embeds: embeds, components: [button] }; 143 | } 144 | } -------------------------------------------------------------------------------- /cmds/apple/update_info.js: -------------------------------------------------------------------------------- 1 | // Gets info for an update 2 | 3 | const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); 4 | const firebase = require("firebase-admin"); 5 | const checker = require('url-status-code'); 6 | const uniqid = require('uniqid'); 7 | const wait = require('util').promisify(setTimeout); 8 | const { SlashCommandBuilder } = require('@discordjs/builders'); 9 | 10 | let db = firebase.firestore(); 11 | 12 | const database = db.collection('other').doc('information'); 13 | 14 | require('../../core/misc.js')(); 15 | require('../../core/embed.js')(); 16 | 17 | let multi_icons = ['ios', 'ipados', 'watchos', 'macos', 'tvos']; 18 | 19 | function isBeta(build, remote) { 20 | let check = build.length > 6 && build.toUpperCase() != build; 21 | if (remote != undefined && remote == false) return false; 22 | if (remote != undefined && remote == true && !check) return true; 23 | return check; 24 | } 25 | 26 | function timeToEpoch(time) { 27 | return Math.floor(new Date(time).getTime() / 1000); 28 | } 29 | 30 | async function get_info(cname, data, index) { 31 | var info = {}; 32 | 33 | let beta = isBeta(data[index]["build"], data[index]["beta"]); 34 | 35 | let title = `${cname.toLowerCase().replace('os', 'OS').replace('ipad', 'iPad')} ${data[index]["version"]} ${(beta) ? "Beta" : ""}`; 36 | let update_id = (data[index]["updateid"]) ? formatUpdatesName(data[index]["updateid"], data[index]["version"], cname) : "Beta"; 37 | let version = `${data[index]["version"]} ${(beta) ? update_id : ""}`; 38 | let build = data[index]["build"]; 39 | let size = (data[index]["size"]) ? formatBytes(data[index]["size"]) : "N/A"; 40 | let changelog = (data[index]["changelog"]) ? data[index]["changelog"] : "Release note is not available."; 41 | let postdate = ((typeof data[index]["postdate"]) == "string") ? timeToEpoch(data[index]['postdate']) : timeToEpoch(data[index]['postdate'].toDate()); 42 | let thumbnail = (multi_icons.includes(cname.toLowerCase())) ? getThumbnail(cname.toLowerCase() + version.split(".")[0]) : getThumbnail(cname.toLowerCase()); 43 | 44 | var package = undefined; 45 | 46 | if (data[index]["package"]) { 47 | let code = await checker(data[index]["package"]); 48 | let status = (code == "404") ? "Expired" : formatBytes(data[index]["packagesize"]); 49 | package = `[InstallAssistant.pkg](${data[index]["package"]}) (${status})` 50 | } 51 | 52 | info.beta = beta; 53 | info.title = title; 54 | info.version = version; 55 | info.build = build; 56 | info.size = size; 57 | info.changelog = changelog; 58 | info.postdate = postdate; 59 | info.thumbnail = thumbnail; 60 | info.package = package; 61 | 62 | return info; 63 | } 64 | 65 | async function display(cname, query, index, interaction) { 66 | let data = query; 67 | let info = await get_info(cname, data, index); 68 | 69 | const embed = new EmbedBuilder() 70 | .setTitle(info.title) 71 | .addFields( 72 | { name: "Version", value: info.version, inline: true }, 73 | { name: "Build", value: info.build, inline: true }, 74 | { name: "Size", value: info.size, inline: true }, 75 | { name: "Release Date", value: ``, inline: true } 76 | ) 77 | .setDescription(info.changelog) 78 | .setThumbnail(info.thumbnail) 79 | .setColor(randomColor()) 80 | .setFooter({ text: interaction.user.username, iconURL: interaction.user.displayAvatarURL() }); 81 | 82 | if (info.package) embed.addFields({ name: "Package", value: info.package, inline: true }); 83 | 84 | return embed; 85 | } 86 | 87 | async function create_buttons(cname, data, index, ids) { 88 | let next_id = ids[0]; 89 | let prev_id = ids[1]; 90 | let cancel_id = ids[2]; 91 | 92 | if (data.length == 1) { 93 | const row = new ActionRowBuilder().addComponents( 94 | new ButtonBuilder() 95 | .setCustomId(cancel_id) 96 | .setLabel('Done') 97 | .setStyle(ButtonStyle.Success), 98 | ); 99 | 100 | return row; 101 | } else if (index == 0) { 102 | let info_next = await get_info(cname, data, index + 1); 103 | 104 | const row = new ActionRowBuilder().addComponents( 105 | new ButtonBuilder() 106 | .setCustomId(cancel_id) 107 | .setLabel('Done') 108 | .setStyle(ButtonStyle.Success), 109 | new ButtonBuilder() 110 | .setCustomId(next_id) 111 | .setLabel(`${info_next.version}`) 112 | .setStyle(ButtonStyle.Primary), 113 | ); 114 | 115 | return row; 116 | 117 | } else if (index == data.length - 1) { 118 | let info_prev = await get_info(cname, data, index - 1); 119 | 120 | const row = new ActionRowBuilder().addComponents( 121 | new ButtonBuilder() 122 | .setCustomId(prev_id) 123 | .setLabel(info_prev.version) 124 | .setStyle(ButtonStyle.Primary), 125 | new ButtonBuilder() 126 | .setCustomId(cancel_id) 127 | .setLabel('Done') 128 | .setStyle(ButtonStyle.Success), 129 | ); 130 | 131 | return row; 132 | } else { 133 | let info_next = await get_info(cname, data, index + 1); 134 | let info_prev = await get_info(cname, data, index - 1); 135 | 136 | const row = new ActionRowBuilder().addComponents( 137 | new ButtonBuilder() 138 | .setCustomId(prev_id) 139 | .setLabel(info_prev.version) 140 | .setStyle(ButtonStyle.Primary), 141 | new ButtonBuilder() 142 | .setCustomId(cancel_id) 143 | .setLabel('Done') 144 | .setStyle(ButtonStyle.Success), 145 | new ButtonBuilder() 146 | .setCustomId(next_id) 147 | .setLabel(info_next.version) 148 | .setStyle(ButtonStyle.Primary), 149 | ); 150 | 151 | return row; 152 | } 153 | } 154 | 155 | module.exports = { 156 | name: 'update_information', 157 | command: 'update_information', 158 | category: 'Apple', 159 | description: 'Gets information about an update.', 160 | ephemeral: false, 161 | usage: '`/update_information `', 162 | data: new SlashCommandBuilder().setName("update_information").setDescription("Gets information about an update.") 163 | .addStringOption(option => option.setName('os').setDescription('Specify the operating system, e.g. iOS').setRequired(true) 164 | .addChoices( 165 | { name: 'iOS', value: 'ios' }, 166 | { name: 'iPadOS', value: 'ipados' }, 167 | { name: 'watchOS', value: 'watchos' }, 168 | { name: 'tvOS', value: 'tvos' }, 169 | { name: 'macOS', value: 'macos' }, 170 | { name: 'audioOS', value: 'audioos' })) 171 | .addStringOption(option => option.setName('version').setDescription("Specify the version, e.g. 14.8.1").setRequired(true)), 172 | 173 | async execute(interaction) { 174 | const os_name = interaction.options.getString('os'); 175 | const search_query = interaction.options.getString('version'); 176 | 177 | try { 178 | let version_query_public = await database.collection(os_name.toLowerCase() + "_public").where('version', '==', search_query).get(); 179 | let version_query_beta = await database.collection(os_name.toLowerCase() + "_beta").where('version', '==', search_query).get(); 180 | 181 | if (version_query_public.empty && version_query_beta.empty) return interaction.editReply(error_alert('No results found.')); 182 | 183 | const query_data = []; 184 | if (!version_query_public.empty) version_query_public.forEach(doc => { query_data.push(doc.data()) }); 185 | if (!version_query_beta.empty) version_query_beta.forEach(doc => { query_data.push(doc.data()) }); 186 | 187 | query_data.sort(function(x, y) { 188 | return (isBeta(x["build"], x["beta"]) - isBeta(y["build"]), y["beta"]); 189 | }); 190 | 191 | const next_id = uniqid('next-'); 192 | const prev_id = uniqid('prev-'); 193 | const cancel_id = uniqid('cancel-'); 194 | const ids = [next_id, prev_id, cancel_id]; 195 | 196 | const filter = ch => { 197 | ch.deferUpdate(); 198 | return ch.member.id == interaction.member.id && ids.includes(ch.customId); 199 | } 200 | 201 | var index = 0, embed = undefined, row = undefined; 202 | 203 | embed = await display(os_name, query_data, index, interaction); 204 | row = await create_buttons(os_name, query_data, index, ids) 205 | 206 | await interaction.editReply({ embeds:[embed], components: [row] }); 207 | 208 | const collector = interaction.channel.createMessageComponentCollector({ filter, time: 180000 }); 209 | 210 | collector.on('collect', async action => { 211 | if (action.customId == next_id) index++; 212 | if (action.customId == prev_id) index--; 213 | if (action.customId == cancel_id) return collector.stop(); 214 | 215 | embed = await display(os_name, query_data, index, interaction); 216 | row = await create_buttons(os_name, query_data, index, ids) 217 | 218 | await interaction.editReply({ embeds: [embed], components: [] }); 219 | await wait(1000); 220 | await interaction.editReply({ embeds: [embed], components: [await create_buttons(os_name, query_data, index, ids)] }); 221 | }); 222 | 223 | collector.on('end', async action => { 224 | await interaction.editReply({ embeds: [embed], components: [] }); 225 | }); 226 | } catch(error) { 227 | return await interaction.editReply(error_alert('Ugh, an unknown error occurred.', error)); 228 | } 229 | }, 230 | }; 231 | -------------------------------------------------------------------------------- /cmds/setup/helper/updates.js: -------------------------------------------------------------------------------- 1 | const { ActionRowBuilder, StringSelectMenuBuilder, EmbedBuilder, ChannelType, ButtonBuilder, ButtonStyle, ComponentType } = require('discord.js'); 2 | const firebase = require("firebase-admin"); 3 | const uniqid = require('uniqid'); 4 | 5 | const db = firebase.firestore(); 6 | 7 | require("./misc/embeds.js")(); 8 | 9 | const ios_database = db.collection('discord').doc('ios'); 10 | const ipados_database = db.collection('discord').doc('ipados'); 11 | const watchos_database = db.collection('discord').doc('watchos'); 12 | const tvos_database = db.collection('discord').doc('tvos'); 13 | const audioos_database = db.collection('discord').doc('audioos'); 14 | const macos_database = db.collection('discord').doc('macos'); 15 | const pkg_database = db.collection('discord').doc('pkg'); 16 | const bot_database = db.collection('discord').doc('bot'); 17 | 18 | async function get_existing_setup(guildID) { 19 | const configuration = []; 20 | const os = [ 21 | { name: "ios", full: "iOS Updates" }, { name: "ipados", full: "iPadOS Updates" }, 22 | { name: "watchos", full: "watchOS Updates" }, { name: "tvos", full: "tvOS Updates" }, 23 | { name: "audioos", full: "audioOS Updates" }, { name: "macos", full: "macOS Updates" }, 24 | { name: "pkg", full: "macOS InstallAssistant.pkg Links"}, { name: "bot", full: "Bot Updates"} 25 | ]; 26 | 27 | for (let i in os) { 28 | const set = await db.collection('discord').doc(os[i].name).get(); 29 | const dataset = set.data(); 30 | if (dataset[guildID] != undefined) configuration.push(os[i].full); 31 | } 32 | 33 | return configuration; 34 | } 35 | 36 | module.exports = function () { 37 | this.setup_updates = async function (interaction) { 38 | const sessionIDs = []; 39 | 40 | const channels_list = interaction.member.guild.channels.cache.filter(ch => ch.type === ChannelType.GuildText || ch.type === ChannelType.GuildNews); 41 | const channel_components = []; 42 | 43 | const existing_setup = await get_existing_setup(interaction.member.guild.id); 44 | 45 | channels_list.forEach(channel => { 46 | if (global.bot.channels.cache.get(channel.parentId) != undefined) { 47 | channel_components.push({ "label": `#${channel.name}`, "value": channel.id, "description": global.bot.channels.cache.get(channel.parentId).name.toLowerCase() }); 48 | } else { 49 | channel_components.push({ "label": `#${channel.name}`, "value": channel.id }); 50 | } 51 | }); 52 | 53 | const channel_multiple_components = []; 54 | 55 | while (channel_components.length) { 56 | let sessionID = uniqid(); sessionIDs.push(sessionID); 57 | channel_multiple_components.push(new ActionRowBuilder().addComponents(new StringSelectMenuBuilder().setCustomId(sessionID).setPlaceholder('No channel selected').addOptions(channel_components.splice(0, 20)))); 58 | } 59 | 60 | await interaction.editReply({ embeds: [updates_part_1(existing_setup)], components: channel_multiple_components }); 61 | 62 | const channel_filter = ch => { 63 | ch.deferUpdate(); 64 | return ch.member.id == interaction.member.id && sessionIDs.includes(ch.customId); 65 | } 66 | 67 | const response = await interaction.channel.awaitMessageComponent({ 68 | filter: channel_filter, 69 | max: 1, 70 | componentType: ComponentType.StringSelect, 71 | time: 60000 72 | }).catch(err => { return; }); 73 | if (response == undefined) return interaction.editReply(error_embed("You did not select a channel within 1 minute so the command was cancelled.")); 74 | 75 | const selected_channel = global.bot.channels.cache.get(response.values[0]); 76 | 77 | const warning_embed = new EmbedBuilder().setDescription("**PLEASE WAIT FOR THE REACTIONS TO FULLY-LOAD BEFORE REACTING TO THE MESSAGE OR YOUR OPTIONS WON'T BE RECORDED.**").setColor("#f07800"); 78 | 79 | const msg = await interaction.editReply({ embeds: [updates_part_2(selected_channel, "Your selected options will appear here"), warning_embed], components: [] }); 80 | 81 | await msg.react("852824816092315678"); 82 | await msg.react("852824860089516033"); 83 | await msg.react("852824628921499688"); 84 | await msg.react("852824690166333440"); 85 | await msg.react("852826560725778442"); 86 | await msg.react("852826607286878228"); 87 | await msg.react("852824497202659348"); 88 | await msg.react("852825269705113610"); 89 | 90 | await interaction.editReply({ embeds: [updates_part_2(selected_channel, "Your selected options will appear here")], components: [] }); 91 | 92 | const filter = (reaction, user) => { 93 | return user.id == interaction.member.id; 94 | } 95 | 96 | const collector = msg.createReactionCollector({ filter, time: 60000, dispose: true }); 97 | 98 | let options = []; 99 | 100 | collector.on("collect", (reaction, user) => { 101 | switch (reaction.emoji.id) { 102 | case "852824816092315678": 103 | options.push("iOS Updates") 104 | break; 105 | case "852824860089516033": 106 | options.push("iPadOS Updates") 107 | break; 108 | case "852824628921499688": 109 | options.push("watchOS Updates") 110 | break; 111 | case "852824690166333440": 112 | options.push("audioOS Updates") 113 | break; 114 | case "852826560725778442": 115 | options.push("tvOS Updates") 116 | break; 117 | case "852826607286878228": 118 | options.push("macOS Updates") 119 | break; 120 | case "852824497202659348": 121 | options.push("macOS InstallAssistant.pkg Links") 122 | break; 123 | case "852825269705113610": 124 | options.push("Bot Updates") 125 | break; 126 | default: 127 | break; 128 | } 129 | 130 | interaction.editReply({ embeds: [updates_part_2(selected_channel, options.join(", "))] }); 131 | }); 132 | 133 | collector.on("remove", (reaction, user) => { 134 | switch (reaction.emoji.id) { 135 | case "852824816092315678": 136 | if (options.includes("iOS Updates")) options.splice(options.indexOf("iOS Updates"), 1); 137 | break; 138 | case "852824860089516033": 139 | if (options.includes("iPadOS Updates")) options.splice(options.indexOf("iPadOS Updates"), 1); 140 | break; 141 | case "852824628921499688": 142 | if (options.includes("watchOS Updates")) options.splice(options.indexOf("watchOS Updates"), 1); 143 | break; 144 | case "852824690166333440": 145 | if (options.includes("audioOS Updates")) options.splice(options.indexOf("audioOS Updates"), 1); 146 | break; 147 | case "852826560725778442": 148 | if (options.includes("tvOS Updates")) options.splice(options.indexOf("tvOS Updates"), 1); 149 | break; 150 | case "852826607286878228": 151 | if (options.includes("macOS Updates")) options.splice(options.indexOf("macOS Updates"), 1); 152 | break; 153 | case "852824497202659348": 154 | if (options.includes("macOS InstallAssistant.pkg Links")) options.splice(options.indexOf("macOS InstallAssistant.pkg Links"), 1); 155 | break; 156 | case "852825269705113610": 157 | if (options.includes("Bot Updates")) options.splice(options.indexOf("Bot Updates"), 1); 158 | break; 159 | default: 160 | break; 161 | } 162 | 163 | if (options.length > 0) interaction.editReply({ embeds: [updates_part_2(selected_channel, options.join(", "))] }); 164 | else interaction.editReply({ embeds: [updates_part_2(selected_channel, "Your selected options will appear here")] }); 165 | }); 166 | 167 | collector.on('end', async collected => { 168 | await msg.reactions.removeAll().catch(); 169 | if (options.length < 1) return interaction.editReply(error_embed("You did not react to the message within 1 minute so the command was cancelled.")); 170 | 171 | if (options.includes("Bot Updates")) await bot_database.update({ [`${selected_channel.guild.id}`]: `${selected_channel.id}` }); 172 | else await bot_database.update({ [`${selected_channel.guild.id}`]: firebase.firestore.FieldValue.delete() }); 173 | 174 | if (options.includes("iOS Updates")) await ios_database.update({ [`${selected_channel.guild.id}`]: `${selected_channel.id}` }); 175 | else await ios_database.update({ [`${selected_channel.guild.id}`]: firebase.firestore.FieldValue.delete() }); 176 | 177 | if (options.includes("iPadOS Updates")) await ipados_database.update({ [`${selected_channel.guild.id}`]: `${selected_channel.id}` }); 178 | else await ipados_database.update({ [`${selected_channel.guild.id}`]: firebase.firestore.FieldValue.delete() }); 179 | 180 | if (options.includes("watchOS Updates")) await watchos_database.update({ [`${selected_channel.guild.id}`]: `${selected_channel.id}` }); 181 | else await watchos_database.update({ [`${selected_channel.guild.id}`]: firebase.firestore.FieldValue.delete() }); 182 | 183 | if (options.includes("audioOS Updates")) await audioos_database.update({ [`${selected_channel.guild.id}`]: `${selected_channel.id}` }); 184 | else await audioos_database.update({ [`${selected_channel.guild.id}`]: firebase.firestore.FieldValue.delete() }); 185 | 186 | if (options.includes("macOS Updates")) await macos_database.update({ [`${selected_channel.guild.id}`]: `${selected_channel.id}` }); 187 | else await macos_database.update({ [`${selected_channel.guild.id}`]: firebase.firestore.FieldValue.delete() }); 188 | 189 | if (options.includes("tvOS Updates")) await tvos_database.update({ [`${selected_channel.guild.id}`]: `${selected_channel.id}` }); 190 | else await tvos_database.update({ [`${selected_channel.guild.id}`]: firebase.firestore.FieldValue.delete() }); 191 | 192 | if (options.includes("macOS InstallAssistant.pkg Links")) await pkg_database.update({ [`${selected_channel.guild.id}`]: `${selected_channel.id}` }); 193 | else await pkg_database.update({ [`${selected_channel.guild.id}`]: firebase.firestore.FieldValue.delete() }); 194 | 195 | const button = new ActionRowBuilder().addComponents( 196 | new ButtonBuilder() 197 | .setURL("https://discord.gg/ktHmcbpMNU") 198 | .setLabel('Join support server') 199 | .setStyle(ButtonStyle.Link)); 200 | const tip_embed = new EmbedBuilder().setDescription(`**Helpful Tip: If you want to be pinged when a new update is available, you can set up a Notification Role.**\n- To set up a notification role, use \`/setup role add\`\n- To remove a notification role, use \`/setup role remove\`\n- To list your server's configured notification roles, use \`/setup role list\``).setColor("#f07800"); 201 | return interaction.editReply({ embeds: [updates_overall(`<#${selected_channel.id}>`, options.join(", ")), tip_embed], components: [button] }); 202 | }); 203 | } 204 | } -------------------------------------------------------------------------------- /core/send.js: -------------------------------------------------------------------------------- 1 | // Send update embeds to servers & ping notification roles 2 | 3 | const firebase = require("firebase-admin"); 4 | 5 | require('./error.js')(); 6 | 7 | let db = firebase.firestore(); 8 | 9 | const ios_database = db.collection('discord').doc('ios'); 10 | const ipados_database = db.collection('discord').doc('ipados'); 11 | const watchos_database = db.collection('discord').doc('watchos'); 12 | const tvos_database = db.collection('discord').doc('tvos'); 13 | const audioos_database = db.collection('discord').doc('audioos'); 14 | const macos_database = db.collection('discord').doc('macos'); 15 | const pkg_database = db.collection('discord').doc('pkg'); 16 | const bot_database = db.collection('discord').doc('bot'); 17 | const role_database = db.collection('discord').doc('roles').collection('servers'); 18 | 19 | module.exports = function () { 20 | this.send_to_servers = async function (os, embed, version) { 21 | if (global.UPDATE_MODE) return; 22 | switch (os.toLowerCase()) { 23 | case "tvos": 24 | const tvos = await tvos_database.get(); 25 | const tvos_guilds = tvos.data(); 26 | for (let channel in tvos.data()) { 27 | let chn = global.bot.channels.cache.get(tvos_guilds[channel]); 28 | if (chn != undefined) { 29 | let role = await role_database.doc(channel).get(); 30 | let role_data = role.data(); 31 | let server = global.bot.guilds.cache.get(channel); 32 | 33 | chn.send({ embeds: [embed.setAuthor({ name: server.name, iconURL: server.iconURL() })] }).catch(function (error) { 34 | send_error(error, "send.js", `send_tvos`, `send tvos update to channels`); 35 | }); 36 | 37 | if (role_data != undefined && server != undefined && role_data['tvos'] != undefined) if (server.roles.cache.get(role_data['tvos']) != undefined) chn.send(`<@&${role_data['tvos']}> **tvOS ${version}** has been released!`).catch(function (error) { 38 | send_error(error, "send.js", `send_tvos`, `send tvos roles to channels`); 39 | }); 40 | } 41 | } 42 | break; 43 | case "audioos": 44 | const audioos = await audioos_database.get(); 45 | const audioos_guilds = audioos.data(); 46 | for (let channel in audioos.data()) { 47 | let chn = global.bot.channels.cache.get(audioos_guilds[channel]); 48 | if (chn != undefined) { 49 | let role = await role_database.doc(channel).get(); 50 | let role_data = role.data(); 51 | let server = global.bot.guilds.cache.get(channel); 52 | 53 | chn.send({ embeds: [embed.setAuthor({ name: server.name, iconURL: server.iconURL() })] }).catch(function (error) { 54 | send_error(error, "send.js", `send_audioos`, `send audioos update to channels`); 55 | }); 56 | 57 | if (role_data != undefined && server != undefined && role_data['audioos'] != undefined) if (server.roles.cache.get(role_data['audioos']) != undefined) chn.send(`<@&${role_data['audioos']}> **audioOS ${version}** has been released!`).catch(function (error) { 58 | send_error(error, "send.js", `send_audioos`, `send audioos roles to channels`); 59 | }); 60 | } 61 | } 62 | break; 63 | case "macos": 64 | const macos = await macos_database.get(); 65 | const macos_guilds = macos.data(); 66 | for (let channel in macos.data()) { 67 | let chn = global.bot.channels.cache.get(macos_guilds[channel]); 68 | if (chn != undefined) { 69 | let role = await role_database.doc(channel).get(); 70 | let role_data = role.data(); 71 | let server = global.bot.guilds.cache.get(channel); 72 | 73 | chn.send({ embeds: [embed.setAuthor({ name: server.name, iconURL: server.iconURL() })] }).catch(function (error) { 74 | send_error(error, "send.js", `send_macos`, `send macos update to channels`); 75 | }); 76 | 77 | if (role_data != undefined && server != undefined && role_data['macos'] != undefined) if (server.roles.cache.get(role_data['macos']) != undefined) chn.send(`<@&${role_data['macos']}> **macOS ${version}** has been released!`).catch(function (error) { 78 | send_error(error, "send.js", `send_macos`, `send macos roles to channels`); 79 | }); 80 | } 81 | } 82 | break; 83 | case "ios": 84 | const ios = await ios_database.get(); 85 | const ios_guilds = ios.data(); 86 | for (let channel in ios.data()) { 87 | let chn = global.bot.channels.cache.get(ios_guilds[channel]); 88 | if (chn != undefined) { 89 | let role = await role_database.doc(channel).get(); 90 | let role_data = role.data(); 91 | let server = global.bot.guilds.cache.get(channel); 92 | 93 | chn.send({ embeds: [embed.setAuthor({ name: server.name, iconURL: server.iconURL() })] }).catch(function (error) { 94 | send_error(error, "send.js", `send_ios`, `send ios update to channels`); 95 | }); 96 | 97 | if (role_data != undefined && server != undefined && role_data['ios'] != undefined) if (server.roles.cache.get(role_data['ios']) != undefined) chn.send(`<@&${role_data['ios']}> **iOS ${version}** has been released!`).catch(function (error) { 98 | send_error(error, "send.js", `send_ios`, `send ios roles to channels`); 99 | }); 100 | } 101 | } 102 | break; 103 | case "ipados": 104 | const ipados = await ipados_database.get(); 105 | const ipados_guilds = ipados.data(); 106 | for (let channel in ipados.data()) { 107 | let chn = global.bot.channels.cache.get(ipados_guilds[channel]); 108 | if (chn != undefined) { 109 | let role = await role_database.doc(channel).get(); 110 | let role_data = role.data(); 111 | let server = global.bot.guilds.cache.get(channel); 112 | 113 | chn.send({ embeds: [embed.setAuthor({ name: server.name, iconURL: server.iconURL() })] }).catch(function (error) { 114 | send_error(error, "send.js", `send_ipados`, `send ipados update to channels`); 115 | }); 116 | 117 | if (role_data != undefined && server != undefined && role_data['ipados'] != undefined) if (server.roles.cache.get(role_data['ipados']) != undefined) chn.send(`<@&${role_data['ipados']}> **iPadOS ${version}** has been released!`).catch(function (error) { 118 | send_error(error, "send.js", `send_ipados`, `send ipados roles to channels`); 119 | }); 120 | } 121 | } 122 | break; 123 | case "watchos": 124 | const watchos = await watchos_database.get(); 125 | const watchos_guilds = watchos.data(); 126 | for (let channel in watchos.data()) { 127 | let chn = global.bot.channels.cache.get(watchos_guilds[channel]); 128 | if (chn != undefined) { 129 | let role = await role_database.doc(channel).get(); 130 | let role_data = role.data(); 131 | let server = global.bot.guilds.cache.get(channel); 132 | 133 | chn.send({ embeds: [embed.setAuthor({ name: server.name, iconURL: server.iconURL() })] }).catch(function (error) { 134 | send_error(error, "send.js", `send_watchos`, `send watchos update to channels`); 135 | }); 136 | 137 | if (role_data != undefined && server != undefined && role_data['watchos'] != undefined) if (server.roles.cache.get(role_data['watchos']) != undefined) chn.send(`<@&${role_data['watchos']}> **watchOS ${version}** has been released!`).catch(function (error) { 138 | send_error(error, "send.js", `send_watchos`, `send watchos roles to channels`); 139 | }); 140 | } 141 | } 142 | break; 143 | case "pkg": 144 | const pkg = await pkg_database.get(); 145 | const pkg_guilds = pkg.data(); 146 | for (let channel in pkg.data()) { 147 | let chn = global.bot.channels.cache.get(pkg_guilds[channel]); 148 | if (chn != undefined) { 149 | let role = await role_database.doc(channel).get(); 150 | let role_data = role.data(); 151 | let server = global.bot.guilds.cache.get(channel); 152 | 153 | chn.send({ embeds: [embed.setAuthor({ name: server.name, iconURL: server.iconURL() })] }).catch(function (error) { 154 | send_error(error, "send.js", `send_pkg`, `send pkg link to channels`); 155 | }); 156 | 157 | if (role_data != undefined && server != undefined && role_data['pkg'] != undefined) if (server.roles.cache.get(role_data['pkg']) != undefined) chn.send(`<@&${role_data['pkg']}> **macOS ${version}** Full Installer Package is available!`).catch(function (error) { 158 | send_error(error, "send.js", `send_pkg`, `send pkg roles to channels`); 159 | }); 160 | } 161 | } 162 | break; 163 | case "bot": 164 | const bot = await bot_database.get(); 165 | const bot_guilds = bot.data(); 166 | for (let channel in bot.data()) { 167 | let chn = global.bot.channels.cache.get(bot_guilds[channel]); 168 | if (chn != undefined) { 169 | let role = await role_database.doc(channel).get(); 170 | let role_data = role.data(); 171 | let server = global.bot.guilds.cache.get(channel); 172 | 173 | chn.send({ embeds: [embed.setAuthor({ name: server.name, iconURL: server.iconURL() })] }).catch(function (error) { 174 | send_error(error, "send.js", `send_bot`, `send bot update to channels`); 175 | }); 176 | 177 | if (role_data != undefined && server != undefined && role_data['bot'] != undefined) if (server.roles.cache.get(role_data['bot']) != undefined) chn.send(`<@&${role_data['bot']}> New announcements!`).catch(function (error) { 178 | send_error(error, "send.js", `send_bot`, `send bot roles to channels`); 179 | }); 180 | } 181 | } 182 | break; 183 | default: 184 | return; 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------