├── .dockerignore ├── .gitignore ├── package.json ├── src ├── cloud-functions.js ├── web-server.js ├── gateway.js └── commands │ ├── rcon.js │ ├── gcloud.js │ └── servers.js ├── Dockerfile ├── LICENSE └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.env 2 | key.json 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord-bot", 3 | "version": "0.0.1", 4 | "description": "A Discord bot to provision Google Compute Engine VM Instances", 5 | "main": "src/gateway.js", 6 | "author": "Jason D'Amour", 7 | "license": "MIT", 8 | "private": true, 9 | "dependencies": { 10 | "@google-cloud/compute": "^2.4.1", 11 | "cat-loggr": "^1.1.0", 12 | "discord.js": "^12.5.1", 13 | "dotenv": "^8.2.0", 14 | "express": "^4.17.1", 15 | "rcon-ts": "^1.2.3", 16 | "slash-create": "^1.2.0", 17 | "string-table": "^0.1.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/cloud-functions.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | const { SlashCreator, GCFServer } = require('slash-create'); 3 | const path = require('path'); 4 | 5 | 6 | const DISCORD_APPLICATION_ID = process.env.DISCORD_APPLICATION_ID; 7 | const DISCORD_PUBLIC_KEY = process.env.DISCORD_PUBLIC_KEY; 8 | const DISCORD_TOKEN = process.env.DISCORD_TOKEN; 9 | 10 | const creator = new SlashCreator({ 11 | applicationID: DISCORD_APPLICATION_ID, 12 | publicKey: DISCORD_PUBLIC_KEY, 13 | token: DISCORD_TOKEN, 14 | }); 15 | 16 | creator 17 | .withServer(new GCFServer(module.exports)) 18 | .registerCommandsIn(path.join(__dirname, 'commands')) 19 | .syncCommands(); 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13 2 | 3 | # install gcloud 4 | RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" \ 5 | | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list \ 6 | && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg \ 7 | | apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - \ 8 | && apt-get update -y && apt-get install google-cloud-sdk -y 9 | 10 | # Create app directory 11 | WORKDIR /usr/src/app 12 | 13 | # Install app dependencies 14 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 15 | # where available (npm@5+) 16 | COPY package*.json ./ 17 | 18 | RUN npm install 19 | # If you are building your code for production 20 | # RUN npm ci --only=production 21 | 22 | # Bundle app source 23 | COPY src/ ./ 24 | 25 | CMD [ "node", "web-server.js" ] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kashiful Haque 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/web-server.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | const { SlashCreator, ExpressServer } = require('slash-create'); 3 | const path = require('path'); 4 | const execSync = require('child_process').execSync; 5 | 6 | const DISCORD_APPLICATION_ID = process.env.DISCORD_APPLICATION_ID; 7 | const DISCORD_PUBLIC_KEY = process.env.DISCORD_PUBLIC_KEY; 8 | const DISCORD_TOKEN = process.env.DISCORD_TOKEN; 9 | const SERVER_HOST = process.env.SERVER_HOST || "0.0.0.0" 10 | const PORT = process.env.PORT || 8080; 11 | const ENDPOINT_PATH = process.env.ENDPOINT_PATH || ""; 12 | const DEFAULT_COMPUTE_ZONE = process.env.DEFAULT_COMPUTE_ZONE || 'us-west2-a'; 13 | 14 | const creator = new SlashCreator({ 15 | applicationID: DISCORD_APPLICATION_ID, 16 | publicKey: DISCORD_PUBLIC_KEY, 17 | token: DISCORD_TOKEN, 18 | serverHost: SERVER_HOST, 19 | serverPort: PORT, 20 | endpointPath: ENDPOINT_PATH 21 | }); 22 | 23 | console.log("Starting server on " + SERVER_HOST + ":" + PORT + ENDPOINT_PATH); 24 | execSync("gcloud config set compute/zone " + DEFAULT_COMPUTE_ZONE); 25 | 26 | creator 27 | .registerCommandsIn(path.join(__dirname, 'commands')) 28 | .syncCommands() 29 | .withServer(new ExpressServer()) 30 | .startServer(); 31 | 32 | console.log("server started!"); 33 | -------------------------------------------------------------------------------- /src/gateway.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | require('dotenv').config({ path: path.resolve(__dirname, './.env') }) 3 | 4 | // Slach Create and related libs 5 | const { SlashCreator, GatewayServer } = require('slash-create'); 6 | const CatLoggr = require('cat-loggr'); 7 | const logger = new CatLoggr().setLevel(process.env.COMMANDS_DEBUG === 'true' ? 'debug' : 'info'); 8 | 9 | // System Exec lib 10 | const execSync = require('child_process').execSync; 11 | 12 | // Discord lib 13 | const Discord = require('discord.js'); 14 | const client = new Discord.Client(); 15 | 16 | // Constants 17 | const DISCORD_APPLICATION_ID = process.env.DISCORD_APPLICATION_ID; 18 | const DISCORD_PUBLIC_KEY = process.env.DISCORD_PUBLIC_KEY; 19 | const DISCORD_TOKEN = process.env.DISCORD_TOKEN; 20 | 21 | const creator = new SlashCreator({ 22 | applicationID: DISCORD_APPLICATION_ID, 23 | publicKey: DISCORD_PUBLIC_KEY, 24 | token: DISCORD_TOKEN, 25 | }); 26 | 27 | creator.on('debug', (message) => logger.log(message)); 28 | creator.on('warn', (message) => logger.warn(message)); 29 | creator.on('error', (error) => logger.error(error)); 30 | creator.on('synced', () => logger.info('Commands synced!')); 31 | creator.on('commandRun', (command, _, ctx) => 32 | logger.info(`${ctx.member.user.username}#${ctx.member.user.discriminator} (${ctx.member.id}) ran command ${command.commandName}`)); 33 | creator.on('commandRegister', (command) => 34 | logger.info(`Registered command ${command.commandName}`)); 35 | creator.on('commandError', (command, error) => logger.error(`Command ${command.commandName}:`, error)); 36 | 37 | console.log("Starting GatewayServer"); 38 | 39 | creator 40 | .withServer(new GatewayServer((handler) => client.ws.on('INTERACTION_CREATE', handler))) 41 | .registerCommandsIn(path.join(__dirname, 'commands')) 42 | .syncCommands(); 43 | 44 | client.login(DISCORD_TOKEN); 45 | -------------------------------------------------------------------------------- /src/commands/rcon.js: -------------------------------------------------------------------------------- 1 | const { SlashCommand, CommandOptionType } = require('slash-create'); 2 | const Rcon = require('rcon-ts').Rcon; 3 | const Discord = require('discord.js'); 4 | 5 | const MANAGER_ROLE_ID = process.env.MANAGER_ROLE_ID || '789078608236511252'; 6 | 7 | module.exports = class RconCommand extends SlashCommand { 8 | constructor(creator) { 9 | super(creator, { 10 | name: 'rcon', 11 | description: 'Interact with RCON game managment servers', 12 | guildID: process.env.DISCORD_GUILD_ID, 13 | deleteCommands: true, 14 | options: [ 15 | { 16 | name: 'run', 17 | description: 'Run raw RCON commands', 18 | type: CommandOptionType.SUB_COMMAND, 19 | options: [ 20 | { 21 | name: 'host', 22 | description: 'Host IP', 23 | type: CommandOptionType.STRING, 24 | required: true 25 | }, 26 | { 27 | name: 'port', 28 | description: 'Host IP (default 25575)', 29 | type: CommandOptionType.INTEGER, 30 | required: true 31 | }, 32 | { 33 | name: 'password', 34 | description: 'RCON server password', 35 | type: CommandOptionType.STRING, 36 | required: true 37 | }, 38 | { 39 | name: 'command', 40 | description: 'RCON command to execute', 41 | type: CommandOptionType.STRING, 42 | required: true 43 | } 44 | ] 45 | } 46 | ] 47 | }); 48 | } 49 | 50 | async run(ctx) { 51 | var subcommand = ctx.subcommands[0]; 52 | 53 | if (subcommand == "run" && ctx.member.roles.includes(MANAGER_ROLE_ID)) { 54 | await ctx.acknowledge(); 55 | 56 | const HOSTNAME = ctx.options[subcommand].host; 57 | const HOSTPORT = ctx.options[subcommand].port || 25575; 58 | const PASSWORD = ctx.options[subcommand].password; 59 | const COMMAND = ctx.options[subcommand].command || "/help"; 60 | 61 | const rcon = new Rcon({ 62 | host: HOSTNAME, 63 | password: PASSWORD, 64 | port: HOSTPORT, 65 | timeout: 5000 66 | }); 67 | 68 | rcon 69 | .session(c => c.send(COMMAND)) 70 | .then( 71 | (response) => { ctx.send({ embeds: [ generateEmbedMessage(ctx.member.displayName, HOSTNAME + ":" + HOSTPORT + " " + COMMAND, response, "success") ] }) }, 72 | (response) => { ctx.send({ embeds: [ generateEmbedMessage(ctx.member.displayName, HOSTNAME + ":" + HOSTPORT + " " + COMMAND, response, "fail") ] }) } 73 | ); 74 | } 75 | } 76 | } 77 | 78 | function generateEmbedMessage(member, input, response, status) { 79 | console.log(input); 80 | console.log(response); 81 | const embed = new Discord.MessageEmbed() 82 | .setColor((status == "success")? '#00b400' : '#b40000') 83 | .setTitle("```" + input + "```") 84 | .setDescription("```" + response + "```") 85 | .setFooter('Run by ' + member); 86 | return embed; 87 | } 88 | -------------------------------------------------------------------------------- /src/commands/gcloud.js: -------------------------------------------------------------------------------- 1 | const { SlashCommand, CommandOptionType } = require('slash-create'); 2 | const execSync = require('child_process').execSync; 3 | 4 | const MANAGER_ROLE_ID = process.env.MANAGER_ROLE_ID || '789078608236511252'; 5 | 6 | module.exports = class GcloudCommand extends SlashCommand { 7 | constructor(creator) { 8 | super(creator, { 9 | name: 'gcloud', 10 | description: 'Interact with GCP Compute Engine Instances', 11 | guildID: process.env.DISCORD_GUILD_ID, 12 | deleteCommands: true, 13 | options: [ 14 | { 15 | name: 'list', 16 | description: 'List Compute Instances and statuses', 17 | type: CommandOptionType.SUB_COMMAND 18 | }, 19 | { 20 | name: 'start', 21 | description: 'Start Compute Instances', 22 | type: CommandOptionType.SUB_COMMAND, 23 | options: [ 24 | { 25 | name: 'name', 26 | description: 'Instance name', 27 | type: CommandOptionType.STRING, 28 | required: true 29 | }, 30 | { 31 | name: 'zone', 32 | description: 'Instance zone (default us-west2-a)', 33 | type: CommandOptionType.STRING, 34 | required: false 35 | } 36 | ] 37 | }, 38 | { 39 | name: 'stop', 40 | description: 'Stop Compute Instances', 41 | type: CommandOptionType.SUB_COMMAND, 42 | options: [ 43 | { 44 | name: 'name', 45 | description: 'Instance name', 46 | type: CommandOptionType.STRING, 47 | required: true 48 | }, 49 | { 50 | name: 'zone', 51 | description: 'Instance zone (default us-west2-a)', 52 | type: CommandOptionType.STRING, 53 | required: false 54 | } 55 | ] 56 | } 57 | ] 58 | }); 59 | } 60 | 61 | async run(ctx) { 62 | if (ctx.member.roles.includes(MANAGER_ROLE_ID)) { 63 | var subcommand = ctx.subcommands[0]; 64 | await ctx.acknowledge(); 65 | 66 | if (subcommand == "list") { 67 | var gcloudCommand = "gcloud compute instances list"; 68 | console.log(gcloudCommand); 69 | var response = execSync(gcloudCommand).toString(); 70 | console.log(response); 71 | ctx.send( "```" + response + "```"); 72 | } 73 | 74 | else if (subcommand == "start") { 75 | var gcloudCommand = "gcloud compute instances start"; 76 | for (var option in ctx.options[subcommand]) { 77 | if (option == "name") gcloudCommand += " " + ctx.options[subcommand][option]; 78 | else gcloudCommand += " --" + option + "=\"" + ctx.options[subcommand][option] + "\""; 79 | } 80 | console.log(gcloudCommand); 81 | var response = execSync(gcloudCommand).toString(); 82 | console.log(response); 83 | ctx.send( "```" + response + "```"); 84 | } 85 | 86 | else if (subcommand == "stop") { 87 | var gcloudCommand = "gcloud compute instances stop"; 88 | for (var option in ctx.options[subcommand]) { 89 | if (option == "name") gcloudCommand += " " + ctx.options[subcommand][option]; 90 | else gcloudCommand += " --" + option + "=\"" + ctx.options[subcommand][option] + "\""; 91 | } 92 | console.log(gcloudCommand); 93 | var response = execSync(gcloudCommand).toString(); 94 | console.log(response); 95 | ctx.send( "```" + response + "```"); 96 | } 97 | } 98 | else { 99 | ctx.send("Insufficient Permissions"); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/commands/servers.js: -------------------------------------------------------------------------------- 1 | const { SlashCommand, CommandOptionType } = require('slash-create'); 2 | const stringTable = require('string-table'); 3 | const Compute = require('@google-cloud/compute'); 4 | const compute = new Compute(); 5 | 6 | const USER_ROLE_ID = process.env.USER_ROLE_ID || '789184783942549545'; 7 | const MANAGER_ROLE_ID = process.env.MANAGER_ROLE_ID || '789078608236511252'; 8 | const DEFAULT_COMPUTE_ZONE = process.env.DEFAULT_COMPUTE_ZONE || 'us-west2-a'; 9 | 10 | const PRESENT_VERBS = { 11 | start: "Starting", 12 | stop: "Stopping" 13 | } 14 | const PAST_VERBS = { 15 | start: "Started", 16 | stop: "Stopped" 17 | } 18 | 19 | module.exports = class ServersCommand extends SlashCommand { 20 | constructor(creator) { 21 | super(creator, { 22 | name: 'servers', 23 | description: 'Interact with GCP Compute Engine Instances', 24 | guildID: process.env.DISCORD_GUILD_ID, 25 | deleteCommands: true, 26 | options: [ 27 | { 28 | name: 'list', 29 | description: 'List Compute Instances and statuses', 30 | type: CommandOptionType.SUB_COMMAND 31 | }, 32 | { 33 | name: 'start', 34 | description: 'Start Compute Instances', 35 | type: CommandOptionType.SUB_COMMAND, 36 | options: [ 37 | { 38 | name: 'name', 39 | description: 'Instance name', 40 | type: CommandOptionType.STRING, 41 | required: true 42 | }, 43 | { 44 | name: 'zone', 45 | description: 'Instance zone (default ' + DEFAULT_COMPUTE_ZONE + ')', 46 | type: CommandOptionType.STRING, 47 | required: false 48 | } 49 | ] 50 | }, 51 | { 52 | name: 'stop', 53 | description: 'Stop Compute Instances', 54 | type: CommandOptionType.SUB_COMMAND, 55 | options: [ 56 | { 57 | name: 'name', 58 | description: 'Instance name', 59 | type: CommandOptionType.STRING, 60 | required: true 61 | }, 62 | { 63 | name: 'zone', 64 | description: 'Instance zone (default us-west2-a)', 65 | type: CommandOptionType.STRING, 66 | required: false 67 | } 68 | ] 69 | } 70 | ] 71 | }); 72 | } 73 | 74 | async run(ctx) { 75 | var subcommand = ctx.subcommands[0]; 76 | 77 | if (subcommand == "list" && (ctx.member.roles.includes(USER_ROLE_ID) || ctx.member.roles.includes(MANAGER_ROLE_ID))) { 78 | await ctx.acknowledge(true); 79 | var table = []; 80 | compute.getVMs((err, vms) => { 81 | if (err) { 82 | ctx.send( "```" + err.errors[0].message + " (" + err.code + ")```"); 83 | } 84 | else { 85 | for (var vm of vms) { 86 | var extIp = ( vm.metadata.networkInterfaces[0].accessConfigs == undefined )? "N/A" : vm.metadata.networkInterfaces[0].accessConfigs[0].natIP; 87 | var row = { 88 | "name": vm.name, 89 | "zone": vm.zone.name, 90 | // "Internal IP": vm.metadata.networkInterfaces[0].networkIP, 91 | "external IP": extIp, 92 | "status": vm.metadata.status 93 | } 94 | console.log(row); 95 | table.push(row); 96 | } 97 | } 98 | var response = stringTable.create(table, { capitalizeHeaders: true, outerBorder: ' '}); 99 | console.log(response); 100 | ctx.send("```" + response + "```"); 101 | }); 102 | } 103 | 104 | else if ((subcommand == "start" || subcommand == "stop") && (ctx.member.roles.includes(USER_ROLE_ID) || ctx.member.roles.includes(MANAGER_ROLE_ID))) { 105 | await ctx.acknowledge(true); 106 | const zoneName = (ctx.options[subcommand].zone == undefined)? DEFAULT_COMPUTE_ZONE : ctx.options[subcommand].zone; 107 | const zone = compute.zone(zoneName); 108 | const vm = zone.vm(ctx.options[subcommand].name); 109 | 110 | await vm [subcommand] ( (err, operation) => { 111 | if (err) { 112 | console.log(err) 113 | ctx.send( "```" + err.errors[0].message + " (" + err.code + ")```"); 114 | } 115 | else { 116 | var message = PRESENT_VERBS[subcommand] + " " + vm.name + "..."; 117 | console.log(message); 118 | ctx.send("```" + message + "```"); 119 | operation.on('complete', (metadata) => { 120 | var message = PAST_VERBS[subcommand] + " " + vm.name + "!"; 121 | console.log(message); 122 | ctx.send("```" + message + "```"); 123 | }); 124 | 125 | operation.on('error', (err) => { 126 | console.log(err); 127 | ctx.send( "```" + err.errors[0].message + " (" + err.code + ")```"); 128 | }); 129 | } 130 | }); 131 | } 132 | else { 133 | ctx.send("Unrecognized Command or Insufficient Permissions"); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord Bot for GCP Compute Engine 2 | 3 | This is the source code for a discord "bot" to list, start, and stop GCP compute instances. This is super useful if someone in the discord knows how to configure VMs for game servers, and other discord users just need to start and stop the instances. I'm going to refer to the code in this repo as the "Application" for the rest of the readme (cuz its technically not a bot in the recommended setup). 4 | 5 | ## Use case 6 | 7 | 1. Set up a Compute Instance in GCP to host a game server, configured so that the process starts when the Compute Instance starts 8 | 2. Set up this Application in GCP, either in Compute Engine, Cloud Run, or Cloud Functions (Functions is recommended, as its cheapest) 9 | 3. Users in the Discord server can now turn on and off the custom game servers! Want to play a bit of Minecraft without messaging that annoying nerdy kid to turn on the server? No Problem! 10 | 11 | ## Commands 12 | The commands shipped with this repo are fairly simple. Typing `/servers` into a Discord server with the commands will bring up the autocomplete menu, with more details about each command. 13 | 14 | ``` 15 | basic commands to check compute instances: 16 | /servers list 17 | /servers start (name) [zone] 18 | /servers stop (name) [zone] 19 | 20 | more advanced commands to interact with running servers 21 | /rcon (host) (port) (password) (command) 22 | ``` 23 | 24 | # Deploying the Bot 25 | 26 | ## General 27 | The bot can be deployed several different ways, and supports old and new styles of authentication. For most situations I strongly recommend deploying on **Google Cloud Functions** with **Application Commands** auth scope, for cheapest and most secure. More on those two in the next sections. 28 | 29 | 30 | If you need raw access to the `gcloud` CLI tool, you can deploy this Application to Google Cloud Run, a platform which runs docker containers serverlessly for you. It has some major drawbacks: 31 | - your command cannot take longer than 3 seconds to run. Discord requires an initial response within 3 seconds, and Cloud Run cannot run async tasks to respond after the inital ack. 32 | - Since Cloud Run scales down to 0, your first command may have a "cold start" delay as it scales up. This would pretty much throw away your first command each hour. 33 | 34 | If you need raw access to the `gcloud` CLI **AND** an always-listening bot, you can host this Application on a typical Compute Instance. This is the common practice to hosting Discord bots, and many other guides go more in depth on how to do this. The only major drawback is price. The following is the [cost to run 1,000 commands per month on each platform](https://cloud.google.com/products/calculator/#id=c83d9bf1-d49d-4a54-b275-3edda13a1ef3): 35 | 36 | | Name | Value | 37 | |-------------------------|------------------------------------------| 38 | | Google Cloud Functions | Free! (up to ~100,000 commands) | 39 | | Google Cloud Run | Free! (up to ~2 million commands) | 40 | | Google Compute Engine | ~$8-$12 Flat per month | 41 | 42 | This Application is built on Discords new [Slash Commands](https://discord.com/developers/docs/interactions/slash-commands) platform (big thanks to [Snazzah](https://github.com/Snazzah/slash-create)). While a typical bot user would need to be invited to a server to scan *all* messages for relevant commands, this Application only gets notified by Discord if someone uses the specific command. This is far more secure. So when setting up Auth for the Discord App, I recommend to use the [Application Commands](https://discord.com/developers/docs/interactions/slash-commands#authorizing-your-application) auth scope. 43 | 44 | ## Prerequisites 45 | 1. [Have a Google Cloud Platform account and Project](https://cloud.google.com/) 46 | 2. [Create an Application in Discord Dev Portal](https://discord.com/developers/applications). 47 | 3. Invite the Application to your Discord Server. 48 | - `CLIENT_ID` comes from your Application page in the Discord Developer Portal 49 | - If your application only uses slash commands (you have not modified this Application), then `SCOPE` is `applications.commands`. 50 | - If you're modifying this Application and need a typical bot with presence, then `SCOPE` is `applications.commands%20bot`. Replace the values, then paste the following in your browser: 51 | ``` 52 | https://discord.com/oauth2/authorize?client_id=[CLIENT_ID]&scope=[SCOPE] 53 | ``` 54 | 55 | ## Google Cloud Functions 56 | 1. Go to the [Google Cloud Functions console](https://console.cloud.google.com/functions). Make sure the API is enabled, then click the `Create Function` button. 57 | 2. Fill out the first page 58 | 59 | | Field | Value | 60 | |-----------------------------|---------------------------------------------------------------------------------------------------------------| 61 | | Name | I chose `discord-app`, but go nuts. | 62 | | Region | Choose the [region](https://cloud.google.com/about/locations) closest to your users | 63 | | Trigger | Choose **HTTP**, and **Allow unauthenticated invocations**. Take note of the Trigger URL. | 64 | | Advanced: Memory Allocation | 128MiB | 65 | | Advanced: Timeout | 60 | 66 | | Advanced: Service Account | App Engine Default service account | 67 | | Environment Variables | [*see below*](#environment-variables) | 68 | 69 | 3. Press `Next`. On the `Source` page, set Function entry point to `interactions` (see https://github.com/jasondamour/discord-gcloud-commands/issues/1). You'll also need to copy the source code files. I recommend just using the file editor. Delete all the default files, and create the following, copying the contents. 70 | - package.json (edit the `main` property to be `cloud-functions.js`) 71 | - cloud-functions.js 72 | - commands/servers.js 73 | 4. Press Deploy! Wait for the function to go green (hopefully :) ) and then copy the Trigger URL. At this point, if you go to your discord server and type a `/`, you should see the autocomplete prompt for your commands appear. If you did not pass the ID of the Guild (Discord Server), then there is a 1 hour delay in updating commands. 74 | 5. Go back to the Discord developer portal and paste the Trigger URL in the `INTERACTIONS ENDPOINT URL` field. If the Function started correctly, then discord will accept the URL when you save. 75 | 6. Test your commands! 76 | 77 | ## Google Cloud Run 78 | 1. Ensure you have enabled [Google Container Registry](https://console.cloud.google.com/gcr) for your project, and you have [gcloud installed](https://cloud.google.com/sdk/docs/install). Make sure you have [Docker installed](https://docs.docker.com/get-docker/) and its started. 79 | 2. Login to your GCP project by running `gcloud auth login`. 80 | 3. Authenticate Docker to the registry, by running `gcloud auth configure-docker` 81 | 4. Build and Publish the Docker image that will be used. From the root of this repository, run `docker build . -t gcr.io/[GCP_PROJECT_ID]/discord-bot && docker push gcr.io/[GCP_PROJECT_ID]/discord-bot` 82 | 5. Go to [Google Cloud Run console](https://console.cloud.google.com/run) and click `Create Service` 83 | 6. Fill out the first page 84 | 85 | | Field | Value | 86 | |-----------------------------|--------------------------------------------------------------------------------------| 87 | | Deployment platform | Cloud Run (fully managed) | 88 | | Region | Choose the [region](https://cloud.google.com/about/locations) closest to your users | 89 | | Name | I chose `discord-app`, but go nuts. | 90 | | Deploy from an existing image | gcr.io/[GCP_PROJECT_ID]/discord-bot | 91 | | Trigger | Choose **HTTP**, **Allow all traffic**, and **Allow unauthenticated invocations**. | 92 | 93 | 7. Press Deploy! Wait for the service to go green (hopefully :) ) and then copy the Trigger URL. At this point, if you go to your discord server and type a `/`, you should see the autocomplete prompt for your commands appear. If you did not pass the ID of the Guild (Discord Server), then there is a 1 hour delay in updating commands. 94 | 95 | 8. Go back to the Discord developer portal and paste the Trigger URL in the `INTERACTIONS ENDPOINT URL` field. If the Function started correctly, then discord will accept the URL when you save. 96 | 97 | ## Google Compute Engine 98 | If you're deploying to Compute Engine, I'm going to assume you are more familiar with Discord Bots and the technologies in general, so this tutorial is far more high level. If you are not familiar, please find one of the hundreds of other "Discord Bot" tutorials out there. 99 | 100 | 1. Create a Compute Instances. Machine Type `e2-micro` was large enough for me, but it all depends on your bot's workload. 101 | 2. SSH to the Compute Instance and clone this repo. Install Node 8 or later (tested with 12). 102 | 3. Set the [Environment Variables](#environment-variables) either directly from the shell using `export`, or a `.env` file. 103 | 4. If your Application is a Bot, then you probably want to run using the Discord.js gateway. It can be started using `node src/gateway.js` and you're done. 104 | 5. If your application only uses Interactions (slash commands), then you can run an Express.js Server and receive webhooks. Run `node src/web-server.js`, and configure a route from the public internet to the instances. Easiest way is to just configure a public IP for the instance. Paste the IP into the Discord dev portal. 105 | 106 | ## Environment Variables 107 | This Application requires the following variables at runtime: 108 | 109 | | Name | Value | 110 | |-----------------------------|-----------------------------------------------------------------------------------------------------------------| 111 | | DISCORD_APPLICATION_ID | The Application ID from the Discord developer portal (AKA Client ID) | 112 | | DISCORD_PUBLIC_KEY | The Public Key from the Discord developer portal | 113 | | DISCORD_TOKEN | The Client Secret from the Discord developer portal | 114 | | DISCORD_GUILD_ID | [optional, but strongly recommended] The ID of the discord server to register commands. 1 hour delay if not set | 115 | | GCLOUD_PROJECT | The GCP Project ID | 116 | | DEFAULT_COMPUTE_ZONE | [optional] The default zone to use for Compute Engine | 117 | | USER_ROLE_ID | The Role ID of low-permission users who can list, start, and stop instances | 118 | | MANAGER_ROLE_ID | The Role ID of high-permission users who can directly run RCON commands | 119 | 120 | > If you face any problems, feel free to create an Issue from the Issues tab. I will try to respond as early as possible. 121 | --------------------------------------------------------------------------------