!upload MM/DD/YYsends attached images to n8n and writes parsed rows.!manualupload MM/DD/YY <json>writes manualname/culvertJSON rows to sheets.!fillinemptyfills blank score cells in the sheet with0.!getuser <username>builds a progression bar chart from Google Sheet scores and returns it as an image.!compare <user1>|<user2>plots both users on one bar chart and returns a PNG.!cumulativesums each week/date column and returns a bar-chart PNG of totals.
- Go to Discord Developer Portal: https://discord.com/developers/applications
- Create an application and add a Bot.
- Under Bot settings:
- Copy the bot token.
- Enable Message Content Intent.
- Invite the bot to your server with
botscope and message permissions.
- In Google Cloud Console, create/select a project.
- Enable Google Sheets API.
- Create a Service Account.
- Create a JSON key for that account.
- Copy these fields from the JSON:
client_email->GOOGLE_SERVICE_ACCOUNT_EMAILprivate_key->GOOGLE_PRIVATE_KEY
- Share your target Google Sheet with the service account email.
- Copy
.env.exampleto.env. - Fill in values:
DISCORD_TOKENGOOGLE_SERVICE_ACCOUNT_EMAILGOOGLE_PRIVATE_KEY(keep\nescaped)GOOGLE_SHEET_ID(from sheet URL)- Optional:
GOOGLE_SHEET_NAME,BOT_PREFIX - Optional for upload command:
N8N_WEBHOOK_URL,N8N_BASIC_AUTH_USERNAME,N8N_BASIC_AUTH_PASSWORD
npm install
npm run startDev watch mode:
npm run devType-check/build:
npm run buildBuild and start:
docker compose up -d --buildView logs:
docker compose logs -f botStop:
docker compose down!chelp!upload 02/27/26with image attachment(s)!manualupload 02/27/26 {"name":"user1","culvert":"63,398"}!fillinempty!getuser user1!compare user1|user2!cumulative
- Request is sent as
multipart/form-data. - Each image is attached as file fields:
file1,file2, ... - A
metadatatext field contains JSON with:guildId,channelId,authorId,messageIddate(from the!uploadcommand)attachmentMeta(name, url, contentType, size)sentAt
- If n8n returns parsable JSON rows with
NameandCulvert, the bot upserts by name:- If name exists in column A, culvert is written in the score column for the command date.
- If name does not exist, a new row is created and then score is written.
- Score column date comes directly from the command date.
- If multiple images are attached to
!upload, the bot sends them to n8n one-by-one and waits for each response before sending the next image.