From 4e51dafdc035e160267f860aba41a7eabadcefe2 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 29 Apr 2020 15:54:12 +0200 Subject: [PATCH] Initial commit --- .auth.example | 6 + .drone.yml | 27 ++++ .gitignore | 6 + Dockerfile | 9 ++ conf.json.example | 16 ++ docker-compose.yml | 9 ++ events.md | 91 ++++++++++++ example-alert.png | Bin 0 -> 34171 bytes package-lock.json | 136 +++++++++++++++++ package.json | 29 ++++ readme.md | 43 ++++++ trellobot.js | 359 +++++++++++++++++++++++++++++++++++++++++++++ 12 files changed, 731 insertions(+) create mode 100644 .auth.example create mode 100644 .drone.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 conf.json.example create mode 100644 docker-compose.yml create mode 100644 events.md create mode 100644 example-alert.png create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 readme.md create mode 100644 trellobot.js diff --git a/.auth.example b/.auth.example new file mode 100644 index 0000000..d9c7e42 --- /dev/null +++ b/.auth.example @@ -0,0 +1,6 @@ +{ + "instructions": "Replace the following three values as appropriate, then rename this file to '.auth' (without '-template' at the end).", + "discordToken": "discord token goes here", + "trelloKey": "trello public key goes here", + "trelloToken": "trello app token goes here" +} \ No newline at end of file diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..4bd8f94 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,27 @@ +kind: pipeline +name: default + +steps: +- name: install + image: node:14.0 + commands: + - npm install + +- name: deploy + image: plugins/docker + settings: + registry: registry.cliffbreak.de + repo: registry.cliffbreak.de/trellobot + username: + from_secret: docker_username + password: + from_secret: docker_password + when: + event: + - push + - tag + - deployment + +trigger: + branch: + - master \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c137151 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.auth +.latestActivityID +node_modules/ +.vscode/ +old-confs/ +conf.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..72f49bb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM node:14.0 +ENV NODE_ENV dev +WORKDIR /usr/src/app +COPY package*.json ./ +COPY .auth.example ./.auth +COPY conf.json.example ./conf.json +RUN npm install --production --silent +COPY . . +CMD npm start \ No newline at end of file diff --git a/conf.json.example b/conf.json.example new file mode 100644 index 0000000..13e2666 --- /dev/null +++ b/conf.json.example @@ -0,0 +1,16 @@ +{ + "boardIDs": [ + "tNbPCydx" + ], + "serverID": "138520312697454592", + "channelID": "453042376898904080", + "pollInterval": 10000, + "contentString": "", + "enabledEvents": [ + "cardCreated" + ], + "userIDs": { + "theangush": "138520076427984896" + }, + "prefix": "." +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2b7f69b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +version: '2.1' + +services: + trellobot: + container_name: trellobot + restart: always + image: registry.cliffbreak.de/trellobot + environment: + NODE_ENV: production \ No newline at end of file diff --git a/events.md b/events.md new file mode 100644 index 0000000..24e879c --- /dev/null +++ b/events.md @@ -0,0 +1,91 @@ +# Event Types + +## Supported Events: + +Put any of these in the `enabledEvents` array of your `conf.json` file to utilize the event whitelist. They should all be self-explanatory. + +* `cardCreated` +* `cardDescriptionChanged` +* `cardDueDateChanged` +* `cardPositionChanged` +* `cardListChanged` +* `cardNameChanged` +* `cardUnarchived` +* `cardArchived` +* `cardDeleted` +* `commentEdited` +* `commentAdded` +* `memberAddedToCard` +* `memberAddedToCardBySelf` +* `memberRemovedFromCard` +* `memberRemovedFromCardBySelf` +* `listCreated` +* `listNameChanged` +* `listPositionChanged` +* `listUnarchived` +* `listArchived` +* `attachmentAddedToCard` +* `attachmentRemovedFromCard` +* `checklistAddedToCard` +* `checklistRemovedFromCard` +* `checklistItemMarkedComplete` +* `checklistItemMarkedIncomplete` + +## Unsupported Events: + +These are other events that *ostensibly exist*, but have not yet been implemented in Trellobot, or aren't available from the Trello API, so you can't get alerts for them. + +* `addAdminToBoard` +* `addAdminToOrganization` +* `addBoardsPinnedToMember` +* `addLabelToCard` +* `addMemberToBoard` +* `addMemberToOrganization` +* `addToOrganizationBoard` +* `convertToCardFromCheckItem` +* `copyBoard` +* `copyCard` +* `copyChecklist` +* `copyCommentCard` +* `createBoard` +* `createBoardInvitation` +* `createBoardPreference` +* `createChecklist` +* `createLabel` +* `createOrganization` +* `createOrganizationInvitation` +* `deleteBoardInvitation` +* `deleteCheckItem` +* `deleteLabel` +* `deleteOrganizationInvitation` +* `disablePlugin` +* `disablePowerUp` +* `emailCard` +* `enablePlugin` +* `enablePowerUp` +* `makeAdminOfBoard` +* `makeAdminOfOrganization` +* `makeNormalMemberOfBoard` +* `makeNormalMemberOfOrganization` +* `makeObserverOfBoard` +* `memberJoinedTrello` +* `moveCardFromBoard` +* `moveCardToBoard` +* `moveListFromBoard` +* `moveListToBoard` +* `removeAdminFromBoard` +* `removeAdminFromOrganization` +* `removeBoardsPinnedFromMember` +* `removeFromOrganizationBoard` +* `removeLabelFromCard` +* `removeMemberFromBoard` +* `removeMemberFromOrganization` +* `unconfirmedBoardInvitation` +* `unconfirmedOrganizationInvitation` +* `updateBoard` +* `updateCheckItem` +* `updateChecklist` +* `updateLabel` +* `updateMember` +* `updateOrganization` +* `voteOnCard` \ No newline at end of file diff --git a/example-alert.png b/example-alert.png new file mode 100644 index 0000000000000000000000000000000000000000..de32fff8b9aae37b9de4f1e986947a68b5e67543 GIT binary patch literal 34171 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%0D*Oe zZ#QR$VqRf#RYpY03`ImuDfW(HV`X-Ge0fdi`UwVLBh(LZPh-Q3?5)o~Wnws~1IzfV z+V5toYq**CR)j}<^<$tIOq>~$!P;k>k*VX}KLz`m_8perH0@FjqA&qX4&O9=WHvC5 zW*X5+dB?~!?k?7JA5X1JS^jlNM2kDQ+D`a* z?)t6LPvNbfr^@e+@s-t}Frs3*N{|Oz@`t!ii2wKF+uvvcM0FUqn=7?3h=p>n6x&x= z0SzIivYD2Q%0r*!zf=VtD(ONwvcH2^o0uhK34uN7v%5>{HO^e1sH8DM=t;l(sK$}+ zl|5MN4YhS*?w6WOKt*nRX4;Gxg{^9Z(fB2Sn)0>yabr&n_rQegr*ii_7G;gz+R)u<_-x9s5uP+3!VSEsrjQo}leT9!8 zMH>YceqfcYhl3zjVyOV4t!F$=1teK% z(+u#EucU$7l3}CFbX84XQs9%SVoqd?jLZnjH|wX!4~uypWk|XSIu^ zkpIDFj$F zbY9uL8Z0}^5$ah1&((g+OYk*ps^@PTwp{$~_=dYoAV3^?B?qU?7=pUd)~Y&2+kxtz zZco*KrE44NO`W-V4j9K&pv1&C=kg*5hrDD0)kfE>TFne;#T;L;JQ0k4^3QHodVa*f zWzvtQ&vQs0Z0bPGtPl5nZ=dj(-9Ep(c1E+C-1k{(^2_}$-k%z82@zYtRCf~dYC4YK zd_cKV^^@wGh0=c^3#P!ghHWl?kI6;}V%j{b$K? z3`?SjidzAD?riwL#!QWA7?E&$lmaYxiQ_oXh9^A?2Se*jKLSboK~-~GPMKIgMtk&F z^x-aZr+d|=^X_M+WBVhv+kL^-cyk4$sh)5jCo-s;&5FW$ z?2XyD;gyYhPT>pHYTA3q^9&g0pDA}{+Q3&1hH(L?Iac)g=4ZyHT|tCul?6*z=6D}z z;p`9U4}eL8?Ka}ussKi5Moy=G2rYHuF0-p0@=sT+xCPDx8T6lACuJ}WxhSkfmV89> z3VHsqSshL7ao?2Doe3onX8JMFyPdgxZ5Pvc|6L3SEQ{w(yVg`_m1ZsAN6_@A$o{FS zhEsb`fJLccu4X!(vi7}DVyf#r`4H~nlN|qKG;T3|!)r(RouFOn zsvf~b>t9yC%|rMNdTIC1!{T;icS-ejOywCpRQ3>cn8Dv7GAQ30-Vo*v=Y+_!}-h4=&iA3eZD|6dHlQLP12$Glh(Ughck z*rRU%2Q|X~f17uZ<%P5)>^5?VS!YC8epc^)d>3?AHZT~Z)sAfx_HjA?`V;u3ak6hx j2cD`7^mp8hNB`Mp>mK)L`EBx_0SG)@{an^LB{Ts5dOpaH literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a474a58 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,136 @@ +{ + "name": "trellobot", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "coffee-script": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.2.tgz", + "integrity": "sha1-Lg0rgjQiB3sPXLDKXJuSTUytB1g=" + }, + "discord.js": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-11.3.2.tgz", + "integrity": "sha512-Abw9CTMX3Jb47IeRffqx2VNSnXl/OsTdQzhvbw/JnqCyqc2imAocc7pX2HoRmgKd8CgSqsjBFBneusz/E16e6A==", + "requires": { + "long": "^4.0.0", + "prism-media": "^0.0.2", + "snekfetch": "^3.6.4", + "tweetnacl": "^1.0.0", + "ws": "^4.0.0" + } + }, + "extend": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extend/-/extend-1.3.0.tgz", + "integrity": "sha1-0VFvsP9WJNLr+RI+odrFoZlABPg=" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "node-trello": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/node-trello/-/node-trello-0.1.5.tgz", + "integrity": "sha1-rGWDVYn7iXLTS6nJXbKLcnoYNpQ=", + "requires": { + "coffee-script": "1.3.2", + "oauth": "0.9.7", + "request": "2.12.0" + } + }, + "oauth": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.7.tgz", + "integrity": "sha1-wlVNA2jJZuswUL7JZYRiVXetHs0=" + }, + "prism-media": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-0.0.2.tgz", + "integrity": "sha512-L6yc8P5NVG35ivzvfI7bcTYzqFV+K8gTfX9YaJbmIFfMXTs71RMnAupvTQPTCteGsiOy9QcNLkQyWjAafY/hCQ==" + }, + "request": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.12.0.tgz", + "integrity": "sha1-EfRvILPQ9ISMY4OZHIB5CvFsjkg=", + "requires": { + "form-data": "~0.0.3", + "mime": "~1.2.7" + }, + "dependencies": { + "form-data": { + "version": "0.0.3", + "bundled": true, + "requires": { + "async": "~0.1.9", + "combined-stream": "0.0.3", + "mime": "~1.2.2" + }, + "dependencies": { + "async": { + "version": "0.1.9", + "bundled": true + }, + "combined-stream": { + "version": "0.0.3", + "bundled": true, + "requires": { + "delayed-stream": "0.0.5" + }, + "dependencies": { + "delayed-stream": { + "version": "0.0.5", + "bundled": true + } + } + } + } + }, + "mime": { + "version": "1.2.7", + "bundled": true + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "snekfetch": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/snekfetch/-/snekfetch-3.6.4.tgz", + "integrity": "sha512-NjxjITIj04Ffqid5lqr7XdgwM7X61c/Dns073Ly170bPQHLm6jkmelye/eglS++1nfTWktpP6Y2bFXjdPlQqdw==" + }, + "trello-events": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/trello-events/-/trello-events-0.1.6.tgz", + "integrity": "sha1-hDQckGPU4SDHq0uwbXkT1/vGl9o=", + "requires": { + "extend": "^1.2.1", + "node-trello": "^0.1.4" + } + }, + "tweetnacl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.0.tgz", + "integrity": "sha1-cT2LgY2kIGh0C/aDhtBHnmb8ins=" + }, + "ws": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", + "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c9bc11b --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "trellobot", + "version": "1.0.0", + "description": "A Discord bot for logging Trello events.", + "main": "trellobot.js", + "scripts": { + "start": "node trellobot.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Angush/trellobot.git" + }, + "author": "Angush", + "license": "ISC", + "bugs": { + "url": "https://github.com/Angush/trellobot/issues" + }, + "homepage": "https://github.com/Angush/trellobot#readme", + "dependencies": { + "discord.js": "^11.3.2", + "trello-events": "^0.1.6" + }, + "nodemonConfig": { + "ignore": [ + "*.md", + ".latestActivityID" + ] + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..604001d --- /dev/null +++ b/readme.md @@ -0,0 +1,43 @@ +# Trellobot +A simple Discord bot to log and report events from your Trello boards in your Discord server. + +![Image example of Trellobot alert](https://raw.githubusercontent.com/Angush/trellobot/master/example-alert.png "Image example of Trellobot alert") + +## Setup +1. Clone repository. +2. Run `npm install`. +3. Configure `conf.json` file as desired ([see below](#confjson)). +4. Generate tokens and set up `.auth` file ([see below](#auth)). +5. All done. Run Trellobot with `node trellobot.js`. + +## conf.json +There are several important values in here which inform Trellobot's operation. Here's what they're all for, and how to set them. + +*(optional properties marked with * asterisks)* + +Property | Explanation +---------------- | ----------- +`boardIDs` | An array of board IDs (strings) determining on which boards Trellobot reports. IDs can be extracted from the URLs of your Trello boards. (eg. the board ID for [https://trello.com/b/**HF8XAoZd**/welcome-board](https://trello.com/b/HF8XAoZd/welcome-board) is `HF8XAoZd`). +`serverID` | An ID string determining which Discord server Trellobot uses. Enable developer mode in Discord and right click a server icon to copy its ID. +`channelID` | An ID string determining which channel on your Discord server Trellobot uses to post reports. Enable developer mode in Discord and right click a channel to copy its ID. +`pollInterval` | An integer determining how often (in milliseconds) Trellobot polls your boards for activity. +`prefix`* | A string determining the prefix for Trellobot commands in Discord. Currently unused. Defaults to `.` (period). +`contentString`* | A string included posted alongside all embeds. If you'd like to ping a certain role every time the bot posts, for example, you would put that string here. +`enabledEvents`* | An array of event names (strings) determining whitelisted events (ie. which events will be reported; if empty, all events are enabled). Eligible event names can be found [in the `events.md` file](https://github.com/angush/trellobot/blob/master/events.md). +`userIDs`* | An object mapping Discord IDs to Trello usernames, like so: `userIDs: {"TrelloUser": "1395184357104955", ...}`, so Trellobot can pull relevant user data from Discord. +`realNames`* | A boolean (defaulting to true) that determines whether Trellobot uses the full names or usernames from Trello (eg. `John Smith` vs `jsmiff2`) + +You can refer to the `conf.json` included in the repository for an example. + +## .auth +The `.auth` file is included as a template to save you time, but you will need to create the keys and tokens yourself to run Trellobot. Here's how: + +Property | How to get the value +-------------- | ---------------------- +`discordToken` | Create an app for Trellobot to work through on [Discord's developer site](https://discordapp.com/developers/applications/me/create), then create a bot user (below app description/icon) and copy the token. +`trelloKey` | Visit [this page](https://trello.com/1/appKey/generate) to generate your public Trello API key. +`trelloToken` | Visit `https://trello.com/1/connect?name=Trellobot&response_type=token&expiration=never&key=YOURPUBLICKEY` (replacing `YOURPUBLICKEY` with the appropriate key) to generate a token that does not expire. Remove `&expiration=never` from the URL if you'd prefer a temporary token. + +That's all for now. + +*i know the name is lame* \ No newline at end of file diff --git a/trellobot.js b/trellobot.js new file mode 100644 index 0000000..b3b9fab --- /dev/null +++ b/trellobot.js @@ -0,0 +1,359 @@ +const Discord = require('discord.js') +const bot = new Discord.Client() +const fs = require('fs') +const auth = JSON.parse(fs.readFileSync('.auth')) +const conf = JSON.parse(fs.readFileSync('conf.json')) +let latestActivityID = fs.existsSync('.latestActivityID') ? fs.readFileSync('.latestActivityID') : 0 + +const Trello = require('trello-events') +const events = new Trello({ + pollFrequency: conf.pollInterval, // milliseconds + minId: latestActivityID, // auto-created and auto-updated + start: false, + trello: { + boards: conf.boardIDs, // array of Trello board IDs + key: auth.trelloKey, // your public Trello API key + token: auth.trelloToken // your private Trello token for Trellobot + } +}) + + + +/* +** ===================================== +** Discord event handlers and functions. +** ===================================== +*/ + +bot.login(auth.discordToken) +bot.on('ready', () => { + let guild = bot.guilds.get(conf.serverID) + let channel = bot.channels.get(conf.channelID) + if (!guild) { + console.log(`Server with ID "${conf.serverID}" not found! I can't function without a valid server and channel.\nPlease add the correct server ID to your conf file, or if the conf data is correct, ensure I have proper access.\nYou may need to add me to your server using this link:\n https://discordapp.com/api/oauth2/authorize?client_id=${bot.user.id}&permissions=0&scope=bot`) + process.exit() + } else if (!channel) { + console.log(`Channel with ID "${conf.channelID}" not found! I can't function without a valid channel.\nPlease add the correct channel ID to your conf file, or if the conf data is correct, ensure I have proper access.`) + process.exit() + } else if (!conf.boardIDs || conf.boardIDs.length < 1) { + console.log(`No board IDs provided! Please add at least one to your conf file. Check the readme if you need help finding a board ID.`) + } + conf.guild = guild + conf.channel = channel + /* + ** Make contentString a map of event names to their paired strings + ** like this: {"createCard": "someone created a card", ...}, so you + ** can, for example, ping specific roles for specific events. + ** + ** Also add a new conf section for pairing lists within a board to + ** contentStrings? That way you can ping one role for new Moderation + ** cards, and another role for new Event cards, for example. + */ + if (!conf.contentString) conf.contentString = "" + if (!conf.enabledEvents) conf.enabledEvents = [] + if (!conf.userIDs) conf.userIDs = {} + if (!conf.realNames) conf.realNames = true + // set default prefix is none provided in conf + if (!conf.prefix) { + conf.prefix = "." + fs.writeFileSync('conf.json', JSON.stringify(conf, null, 4), (err, data) => console.log(`Updated conf file with default prefix ('.')`)) + } + // logInitializationData() + console.log(`== Bot logged in as @${bot.user.tag}. Ready for action! ==`) + events.start() +}) + +bot.on('message', (msg) => { + if (msg.channel.type !== "text") return + if (msg.content.startsWith(`${conf.prefix}ping`)) { + let now = Date.now() + msg.channel.send(`Ping!`).then(m => { + m.edit(`Pong! (took ${Date.now() - now}ms)`) + }) + } +}) + + + +/* +** ==================================== +** Trello event handlers and functions. +** ==================================== +*/ + +// Fired when a card is created +events.on('createCard', (event, board) => { + if (!eventEnabled(`cardCreated`)) return + let embed = getEmbedBase(event) + .setTitle(`New card created under __${event.data.list.name}__!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Card created under __${event.data.list.name}__ by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) +}) + +// Fired when a card is updated (description, due date, position, associated list, name, and archive status) +events.on('updateCard', (event, board) => { + let embed = getEmbedBase(event) + if (event.data.old.hasOwnProperty("desc")) { + if (!eventEnabled(`cardDescriptionChanged`)) return + embed + .setTitle(`Card description changed!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Card description changed (see below) by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + .addField(`New Description`, typeof event.data.card.desc === "string" && event.data.card.desc.trim().length > 0 ? (event.data.card.desc.length > 1024 ? `${event.data.card.desc.trim().slice(0, 1020)}...` : event.data.card.desc) : `*[No description]*`) + .addField(`Old Description`, typeof event.data.old.desc === "string" && event.data.old.desc.trim().length > 0 ? (event.data.old.desc.length > 1024 ? `${event.data.old.desc.trim().slice(0, 1020)}...` : event.data.old.desc) : `*[No description]*`) + send(addDiscordUserData(embed, event.memberCreator)) + } else if (event.data.old.hasOwnProperty("due")) { + if (!eventEnabled(`cardDueDateChanged`)) return + embed + .setTitle(`Card due date changed!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Card due date changed to __${event.data.card.due ? new Date(event.data.card.due).toUTCString() : `[No due date]`}__ from __${event.data.old.due ? new Date(event.data.old.due).toUTCString() : `[No due date]`}__ by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) + } else if (event.data.old.hasOwnProperty("pos")) { + if (!eventEnabled(`cardPositionChanged`)) return + embed + .setTitle(`Card position changed!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Card position in list __${event.data.list.name}__ changed by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) + } else if (event.data.old.hasOwnProperty("idList")) { + if (!eventEnabled(`cardListChanged`)) return + embed + .setTitle(`Card list changed!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Card moved to list __${event.data.listAfter.name}__ from list __${event.data.listBefore.name}__ by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) + } else if (event.data.old.hasOwnProperty("name")) { + if (!eventEnabled(`cardNameChanged`)) return + embed + .setTitle(`Card name changed!`) + .setDescription(`**CARD:** *[See below for card name]* — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Card name changed (see below) by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + .addField(`New Name`, event.data.card.name) + .addField(`Old Name`, event.data.old.name) + send(addDiscordUserData(embed, event.memberCreator)) + } else if (event.data.old.hasOwnProperty("closed")) { + if (event.data.old.closed) { + if (!eventEnabled(`cardUnarchived`)) return + embed + .setTitle(`Card unarchived!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Card unarchived and returned to list __${event.data.list.name}__ by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) + } else { + if (!eventEnabled(`cardArchived`)) return + embed + .setTitle(`Card archived!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Card under list __${event.data.list.name}__ archived by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) + } + } +}) + +// Fired when a card is deleted +events.on('deleteCard', (event, board) => { + if (!eventEnabled(`cardDeleted`)) return + let embed = getEmbedBase(event) + .setTitle(`Card deleted!`) + .setDescription(`**EVENT:** Card deleted from list __${event.data.list.name}__ by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) +}) + +// Fired when a comment is posted, or edited +events.on('commentCard', (event, board) => { + let embed = getEmbedBase(event) + if (event.data.hasOwnProperty("textData")) { + if (!eventEnabled(`commentEdited`)) return + embed + .setTitle(`Comment edited on card!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Card comment edited (see below for comment text) by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + .addField(`Comment Text`, event.data.text.length > 1024 ? `${event.data.text.trim().slice(0, 1020)}...` : event.data.text) + .setTimestamp(event.data.dateLastEdited) + send(addDiscordUserData(embed, event.memberCreator)) + } else { + if (!eventEnabled(`commentAdded`)) return + embed + .setTitle(`Comment added to card!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Card comment added (see below for comment text) by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + .addField(`Comment Text`, event.data.text.length > 1024 ? `${event.data.text.trim().slice(0, 1020)}...` : event.data.text) + send(addDiscordUserData(embed, event.memberCreator)) + } +}) + +// Fired when a member is added to a card +events.on('addMemberToCard', (event, board) => { + let embed = getEmbedBase(event) + .setTitle(`Member added to card!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Member **[${conf.realNames ? event.member.fullName : event.member.username}](https://trello.com/${event.member.username})**`) + let editedEmbed = addDiscordUserData(embed, event.member) + + if (event.member.id === event.memberCreator.id) { + if (!eventEnabled(`memberAddedToCardBySelf`)) return + editedEmbed.setDescription(editedEmbed.description + ` added themselves to card.`) + send(editedEmbed) + } else { + if (!eventEnabled(`memberAddedToCard`)) return + editedEmbed.setDescription(editedEmbed.description + ` added to card by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(editedEmbed, event.memberCreator)) + } +}) + +// Fired when a member is removed from a card +events.on('removeMemberFromCard', (event, board) => { + let embed = getEmbedBase(event) + .setTitle(`Member removed from card!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Member **[${conf.realNames ? event.member.fullName : event.member.username}](https://trello.com/${event.member.username})**`) + let editedEmbed = addDiscordUserData(embed, event.member) + + if (event.member.id === event.memberCreator.id) { + if (!eventEnabled(`memberRemovedFromCardBySelf`)) return + editedEmbed.setDescription(editedEmbed.description + ` removed themselves from card.`) + send(editedEmbed) + } else { + if (!eventEnabled(`memberRemovedFromCard`)) return + editedEmbed.setDescription(editedEmbed.description + ` removed from card by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(editedEmbed, event.memberCreator)) + } +}) + +// Fired when a list is created +events.on('createList', (event, board) => { + if (!eventEnabled(`listCreated`)) return + let embed = getEmbedBase(event) + .setTitle(`New list created!`) + .setDescription(`**EVENT:** List __${event.data.list.name}__ created by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) +}) + +// Fired when a list is renamed, moved, archived, or unarchived +events.on('updateList', (event, board) => { + let embed = getEmbedBase(event) + if (event.data.old.hasOwnProperty("name")) { + if (!eventEnabled(`listNameChanged`)) return + embed + .setTitle(`List name changed!`) + .setDescription(`**EVENT:** List renamed to __${event.data.list.name}__ from __${event.data.old.name}__ by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) + } else if (event.data.old.hasOwnProperty("pos")) { + if (!eventEnabled(`listPositionChanged`)) return + embed + .setTitle(`List position changed!`) + .setDescription(`**EVENT:** List __${event.data.list.name}__ position changed by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) + } else if (event.data.old.hasOwnProperty("closed")) { + if (event.data.old.closed) { + if (!eventEnabled(`listUnarchived`)) return + embed + .setTitle(`List unarchived!`) + .setDescription(`**EVENT:** List __${event.data.list.name}__ unarchived by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) + } else { + if (!eventEnabled(`listArchived`)) return + embed + .setTitle(`List archived!`) + .setDescription(`**EVENT:** List __${event.data.list.name}__ archived by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) + } + } +}) + +// Fired when an attachment is added to a card +events.on('addAttachmentToCard', (event, board) => { + if (!eventEnabled(`attachmentAddedToCard`)) return + let embed = getEmbedBase(event) + .setTitle(`Attachment added to card!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Attachment named \`${event.data.attachment.name}\` added to card by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) +}) + +// Fired when an attachment is removed from a card +events.on('deleteAttachmentFromCard', (event, board) => { + if (!eventEnabled(`attachmentRemovedFromCard`)) return + let embed = getEmbedBase(event) + .setTitle(`Attachment removed from card!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Attachment named \`${event.data.attachment.name}\` removed from card by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) +}) + +// Fired when a checklist is added to a card (same thing as created) +events.on('addChecklistToCard', (event, board) => { + if (!eventEnabled(`checklistAddedToCard`)) return + let embed = getEmbedBase(event) + .setTitle(`Checklist added to card!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Checklist named \`${event.data.checklist.name}\` added to card by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) +}) + +// Fired when a checklist is removed from a card (same thing as deleted) +events.on('removeChecklistFromCard', (event, board) => { + if (!eventEnabled(`checklistRemovedFromCard`)) return + let embed = getEmbedBase(event) + .setTitle(`Checklist removed from card!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Checklist named \`${event.data.checklist.name}\` removed from card by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + send(addDiscordUserData(embed, event.memberCreator)) +}) + +// Fired when a checklist item's completion status is toggled +events.on('updateCheckItemStateOnCard', (event, board) => { + if (event.data.checkItem.state === "complete") { + if (!eventEnabled(`checklistItemMarkedComplete`)) return + let embed = getEmbedBase(event) + .setTitle(`Checklist item marked complete!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Checklist item under checklist \`${event.data.checklist.name}\` marked complete by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + .addField(`Checklist Item Name`, event.data.checkItem.name.length > 1024 ? `${event.data.checkItem.name.trim().slice(0, 1020)}...` : event.data.checkItem.name) + send(addDiscordUserData(embed, event.memberCreator)) + } else if (event.data.checkItem.state === "incomplete") { + if (!eventEnabled(`checklistItemMarkedIncomplete`)) return + let embed = getEmbedBase(event) + .setTitle(`Checklist item marked incomplete!`) + .setDescription(`**CARD:** ${event.data.card.name} — **[CARD LINK](https://trello.com/c/${event.data.card.shortLink})**\n\n**EVENT:** Checklist item under checklist \`${event.data.checklist.name}\` marked incomplete by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) + .addField(`Checklist Item Name`, event.data.checkItem.name.length > 1024 ? `${event.data.checkItem.name.trim().slice(0, 1020)}...` : event.data.checkItem.name) + send(addDiscordUserData(embed, event.memberCreator)) + } +}) + + + +/* +** ======================= +** Miscellaneous functions +** ======================= +*/ +events.on('maxId', (id) => { + if (latestActivityID == id) return + latestActivityID = id + fs.writeFileSync('.latestActivityID', id) +}) + +const send = (embed, content = ``) => conf.channel.send(`${content} ${conf.contentString}`, {embed:embed}).catch(err => console.error(err)) + +const eventEnabled = (type) => conf.enabledEvents.length > 0 ? conf.enabledEvents.includes(type) : true + +const logEventFire = (event) => console.log(`${new Date(event.date).toUTCString()} - ${event.type} fired`) + +const getEmbedBase = (event) => new Discord.RichEmbed() + .setFooter(`${conf.guild.members.get(bot.user.id).displayName} • ${event.data.board.name} [${event.data.board.shortLink}]`, bot.user.displayAvatarURL) + .setTimestamp(event.hasOwnProperty(`date`) ? event.date : Date.now()) + .setColor("#127ABD") + +// Converts Trello @username mentions in titles to Discord mentions, finds channel and role mentions, and mirros Discord user mentions outside the embed +const convertMentions = (embed, event) => { + +} + +// adds thumbanil and appends user mention to the end of the description, if possible +const addDiscordUserData = (embed, member) => { + if (conf.userIDs[member.username]) { + let discordUser = conf.guild.members.get(conf.userIDs[member.username]) + if (discordUser) embed + .setThumbnail(discordUser.user.displayAvatarURL) + .setDescription(`${embed.description} / ${discordUser.toString()}`) + } + return embed +} + +// logs initialization data (stuff loaded from conf.json) - mostly for debugging purposes +const logInitializationData = () => console.log(`== INITIALIZING WITH: + latestActivityID - ${latestActivityID} + boardIDs --------- ${conf.boardIDs.length + " [" + conf.boardIDs.join(", ") + "]"} + serverID --------- ${conf.serverID} (${conf.guild.name}) + channelID -------- ${conf.channelID} (#${conf.channel.name}) + pollInterval ----- ${conf.pollInterval} ms (${conf.pollInterval / 1000} seconds) + prefix ----------- "${conf.prefix}"${conf.prefix === "." ? " (default)" : ""} + contentString ---- ${conf.contentString !== "" ? "\"" + conf.contentString + "\"" : "none"} + enabledEvents ---- ${conf.enabledEvents.length > 0 ? conf.enabledEvents.length + " [" + conf.enabledEvents.join(", ") + "]" : "all"} + userIDs ---------- ${Object.getOwnPropertyNames(conf.userIDs).length}`)