commit 4e51dafdc035e160267f860aba41a7eabadcefe2 Author: Simon Date: Wed Apr 29 15:54:12 2020 +0200 Initial commit 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 0000000..de32fff Binary files /dev/null and b/example-alert.png differ 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}`)