Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@ A Discord bot made for ACM Cyber & Psi Beta Rho. :)
0. Install Node and make sure corepack is enabled (`corepack enable`).
1. Download and copy `.env.example` as `.env`
2. Run `pnpm install` to install dependencies
3. Either ask me for your own discord bot user OR Create your own discord bot application: https://discordjs.guide/preparations/setting-up-a-bot-application.html
4. [Invite your discord bot to our shared testing discord server](https://discordjs.guide/preparations/adding-your-bot-to-servers.html#creating-and-using-your-invite-link). If you need admin, let me (Alec) know.
3. Either ask me (Alec or Andrew) for your own discord bot user OR Create your own discord bot application: https://discordjs.guide/preparations/setting-up-a-bot-application.html
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably avoid hard-coded names here - maybe like "the infrastructure lead?"

4. Invite your discord bot to a testing discord server.
5. Add the token from step 3 into the `.env` in the proper location. Make sure there are no extra spaces between the text and the equals sign!
6. Replace DISCORD_CLIENT_ID with the OAuth Client id of your Discord application.
7. Before making new changes, do `git checkout -b BRANCHNAME` where `BRANCHNAME` is a name for whatever feature you are working on.

## Google Service Account credentials
- As this bot uses a feature of GSuite (Google Drive), running this bot will require obtaining some credentials. You do NOT need a project with billing enabled to do this.
1. Follow the instructions here (you can ignore the optional steps): [https://cloud.google.com/iam/docs/service-accounts-create](https://cloud.google.com/iam/docs/service-accounts-create)
2. Once in the service account, go to keys --> add key --> create new key --> JSON, download and save this file as `credentials.json`
3. Enable the API: Go to Google Workspace --> APIs, and enable the `Google Drive` API.
4. To run this bot, you must also designate a Google Drive folder for the bot to upload photos to. This can be done by creating a Drive folder, sharing it with the email of the service account, and replacing the "[FOLDER ID]" in uploadphotos.js with the id of that folder (the section of the url after "folders/").

## Running
0. Run `pnpm start` to run the bot
- Do NOT run multiple copies of a bot under a single bot token, otherwise weird issues may occur!
Expand Down
57 changes: 57 additions & 0 deletions commands/uploadphotos.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { google } from "googleapis"
import { authorize } from "../utilities/google-auth.js"
import fs from "fs"
import { ContextMenuCommandBuilder, ApplicationCommandType } from "discord.js"
import axios from "axios"
import path from "path"

export const data = new ContextMenuCommandBuilder().setName("upload photos").setType(ApplicationCommandType.Message)

export async function execute(interaction) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a bit dangerous to allow anyone to run - I would recommend adding a check to only allow those with Administrator permission (basically the officers) as otherwise anyone could cause the bot to download arbitrary files into our disk and then into drive.

if (!interaction.isMessageContextMenuCommand()) return
const message = await interaction.channel.messages.fetch(interaction.targetId)
await interaction.deferReply() // Discord requires an acknowledgement within 3 seconds. this allows the response to be deferred, with an "<application> is thinking..." response in the meantime

let auth = await authorize()
const drive = google.drive({ version: "v3", auth })
const folderId = "[FOLDER ID]" // replace this with the id of the desired folder (the part of the url after "folders/")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we should not hardcode config values - use an environment variable instead, which can be set in the .env file

const downloadDir = path.join(import.meta.dirname, "..", "downloads")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we don't want to save the files, we should probably save the files in a temporary directory, which can be created using tmpdir() + mkdtemp()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, you can look at streaming the data directly, which is probably the more ideal solution though it'll be a bit harder to get working - create a file read stream pointing to the remote attachment download url and stream it to Google Drive. Something like https://stackoverflow.com/questions/65976322/how-to-create-a-readable-stream-from-a-remote-url-in-nodejs would probably work - you might have to play around a bit.

let count = 0

for (const [key, value] of message["attachments"]) {
if (value["contentType"].substring(0, 5) !== "image") continue
/* credit to https://github.com/ZacTimTam/Upload-To-GDrive-Discord-Bot/tree/main for the WriteStream sections */
try {
const filePath = path.join(downloadDir, value["name"])
const response = await axios.get(value["url"], { responseType: "stream" })
const writer = fs.createWriteStream(filePath)
response.data.pipe(writer)
await new Promise((resolve, reject) => {
writer.on("finish", resolve)
writer.on("error", reject)
})

const res = await drive.files.create({
requestBody: {
name: value["title"],
mimeType: value["contentType"],
parents: [folderId],
},
media: {
mimeType: value["contentType"],
body: fs.createReadStream(filePath),
},
})
// console.log(res.data);
await fs.promises.unlink(filePath)
count++
} catch (error) {
console.error(`Failed to process file ${value["name"]}:`, error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an error occurs, we could be left with the file in our file system. I would recommend adding the fs.promises.unlink call inside of a finally clause.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance we could send the list of files that failed to upload to the user?

}
}
if (count === 1) {
interaction.editReply(`1 photo uploaded!`)
} else {
interaction.editReply(`${count} photos uploaded!`)
}
}
Empty file added downloads/.gitkeep
Empty file.
6 changes: 5 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ console.log(`Registered ${registerResponse.length} commands!`)

client.on(Events.InteractionCreate, async interaction => {
let command
if (interaction.isChatInputCommand()) {
if (
interaction.isChatInputCommand() ||
interaction.isMessageContextMenuCommand() ||
interaction.isUserContextMenuCommand()
) {
command = interaction.client.commands.get(interaction.commandName)
}

Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
"fmt": "pnpm run fix"
},
"dependencies": {
"axios": "^1.8.4",
"discord.js": "^14.16.3",
"dotenv": "^16.4.5"
"dotenv": "^16.4.5",
"google-auth-library": "^9.15.1",
"googleapis": "^148.0.0",
"path": "^0.12.7"
},
"devDependencies": {
"prettier": "^3.3.3"
Expand Down
15 changes: 15 additions & 0 deletions utilities/google-auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { JWT } from "google-auth-library"
import credentials_jwt from "../credentials.json" with { type: "json" };

const SCOPES = [
"https://www.googleapis.com/auth/drive"
];

export async function authorize() {
return new JWT({
email: credentials_jwt.client_email,
key: credentials_jwt.private_key,
scopes: SCOPES,
});
}