├── .auth ├── .gitignore ├── conf.json ├── events.md ├── example-alert.png ├── package-lock.json ├── package.json ├── readme.md └── trellobot.js /.auth: -------------------------------------------------------------------------------- 1 | { 2 | "instructions": "Replace the following three values as appropriate, then rename this file to '.auth' (without '-template' at the end).", 3 | "discordToken": "discord token goes here", 4 | "trelloKey": "trello public key goes here", 5 | "trelloToken": "trello app token goes here" 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .auth 2 | .latestActivityID 3 | node_modules/ 4 | .vscode/ 5 | old-confs/ 6 | conf.json -------------------------------------------------------------------------------- /conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "boardIDs": [ 3 | "tNbPCydx" 4 | ], 5 | "serverID": "138520312697454592", 6 | "channelID": "453042376898904080", 7 | "pollInterval": 10000, 8 | "contentString": "", 9 | "enabledEvents": [ 10 | "cardCreated" 11 | ], 12 | "userIDs": { 13 | "theangush": "138520076427984896" 14 | }, 15 | "prefix": "." 16 | } 17 | -------------------------------------------------------------------------------- /events.md: -------------------------------------------------------------------------------- 1 | # Event Types 2 | 3 | ## Supported Events: 4 | 5 | Put any of these in the `enabledEvents` array of your `conf.json` file to utilize the event whitelist. They should all be self-explanatory. 6 | 7 | * `cardCreated` 8 | * `cardDescriptionChanged` 9 | * `cardDueDateChanged` 10 | * `cardPositionChanged` 11 | * `cardListChanged` 12 | * `cardNameChanged` 13 | * `cardUnarchived` 14 | * `cardArchived` 15 | * `cardDeleted` 16 | * `commentEdited` 17 | * `commentAdded` 18 | * `memberAddedToCard` 19 | * `memberAddedToCardBySelf` 20 | * `memberRemovedFromCard` 21 | * `memberRemovedFromCardBySelf` 22 | * `listCreated` 23 | * `listNameChanged` 24 | * `listPositionChanged` 25 | * `listUnarchived` 26 | * `listArchived` 27 | * `attachmentAddedToCard` 28 | * `attachmentRemovedFromCard` 29 | * `checklistAddedToCard` 30 | * `checklistRemovedFromCard` 31 | * `checklistItemMarkedComplete` 32 | * `checklistItemMarkedIncomplete` 33 | 34 | ## Unsupported Events: 35 | 36 | 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. 37 | 38 | * `addAdminToBoard` 39 | * `addAdminToOrganization` 40 | * `addBoardsPinnedToMember` 41 | * `addLabelToCard` 42 | * `addMemberToBoard` 43 | * `addMemberToOrganization` 44 | * `addToOrganizationBoard` 45 | * `convertToCardFromCheckItem` 46 | * `copyBoard` 47 | * `copyCard` 48 | * `copyChecklist` 49 | * `copyCommentCard` 50 | * `createBoard` 51 | * `createBoardInvitation` 52 | * `createBoardPreference` 53 | * `createChecklist` 54 | * `createLabel` 55 | * `createOrganization` 56 | * `createOrganizationInvitation` 57 | * `deleteBoardInvitation` 58 | * `deleteCheckItem` 59 | * `deleteLabel` 60 | * `deleteOrganizationInvitation` 61 | * `disablePlugin` 62 | * `disablePowerUp` 63 | * `emailCard` 64 | * `enablePlugin` 65 | * `enablePowerUp` 66 | * `makeAdminOfBoard` 67 | * `makeAdminOfOrganization` 68 | * `makeNormalMemberOfBoard` 69 | * `makeNormalMemberOfOrganization` 70 | * `makeObserverOfBoard` 71 | * `memberJoinedTrello` 72 | * `moveCardFromBoard` 73 | * `moveCardToBoard` 74 | * `moveListFromBoard` 75 | * `moveListToBoard` 76 | * `removeAdminFromBoard` 77 | * `removeAdminFromOrganization` 78 | * `removeBoardsPinnedFromMember` 79 | * `removeFromOrganizationBoard` 80 | * `removeLabelFromCard` 81 | * `removeMemberFromBoard` 82 | * `removeMemberFromOrganization` 83 | * `unconfirmedBoardInvitation` 84 | * `unconfirmedOrganizationInvitation` 85 | * `updateBoard` 86 | * `updateCheckItem` 87 | * `updateChecklist` 88 | * `updateLabel` 89 | * `updateMember` 90 | * `updateOrganization` 91 | * `voteOnCard` -------------------------------------------------------------------------------- /example-alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Angush/trellobot/11e0eaed5a8982fafba3d76d40522e70af4d8a9f/example-alert.png -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trellobot", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "dependencies": { 6 | "async-limiter": { 7 | "version": "1.0.0", 8 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", 9 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" 10 | }, 11 | "coffee-script": { 12 | "version": "1.3.2", 13 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.2.tgz", 14 | "integrity": "sha1-Lg0rgjQiB3sPXLDKXJuSTUytB1g=" 15 | }, 16 | "discord.js": { 17 | "version": "11.3.2", 18 | "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-11.3.2.tgz", 19 | "integrity": "sha512-Abw9CTMX3Jb47IeRffqx2VNSnXl/OsTdQzhvbw/JnqCyqc2imAocc7pX2HoRmgKd8CgSqsjBFBneusz/E16e6A==" 20 | }, 21 | "extend": { 22 | "version": "1.3.0", 23 | "resolved": "https://registry.npmjs.org/extend/-/extend-1.3.0.tgz", 24 | "integrity": "sha1-0VFvsP9WJNLr+RI+odrFoZlABPg=" 25 | }, 26 | "long": { 27 | "version": "4.0.0", 28 | "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", 29 | "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" 30 | }, 31 | "node-trello": { 32 | "version": "0.1.5", 33 | "resolved": "https://registry.npmjs.org/node-trello/-/node-trello-0.1.5.tgz", 34 | "integrity": "sha1-rGWDVYn7iXLTS6nJXbKLcnoYNpQ=" 35 | }, 36 | "oauth": { 37 | "version": "0.9.7", 38 | "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.7.tgz", 39 | "integrity": "sha1-wlVNA2jJZuswUL7JZYRiVXetHs0=" 40 | }, 41 | "prism-media": { 42 | "version": "0.0.2", 43 | "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-0.0.2.tgz", 44 | "integrity": "sha512-L6yc8P5NVG35ivzvfI7bcTYzqFV+K8gTfX9YaJbmIFfMXTs71RMnAupvTQPTCteGsiOy9QcNLkQyWjAafY/hCQ==" 45 | }, 46 | "request": { 47 | "version": "2.12.0", 48 | "resolved": "https://registry.npmjs.org/request/-/request-2.12.0.tgz", 49 | "integrity": "sha1-EfRvILPQ9ISMY4OZHIB5CvFsjkg=", 50 | "dependencies": { 51 | "form-data": { 52 | "version": "0.0.3", 53 | "bundled": true, 54 | "dependencies": { 55 | "async": { 56 | "version": "0.1.9", 57 | "bundled": true 58 | }, 59 | "combined-stream": { 60 | "version": "0.0.3", 61 | "bundled": true, 62 | "dependencies": { 63 | "delayed-stream": { 64 | "version": "0.0.5", 65 | "bundled": true 66 | } 67 | } 68 | } 69 | } 70 | }, 71 | "mime": { 72 | "version": "1.2.7", 73 | "bundled": true 74 | } 75 | } 76 | }, 77 | "safe-buffer": { 78 | "version": "5.1.2", 79 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 80 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 81 | }, 82 | "snekfetch": { 83 | "version": "3.6.4", 84 | "resolved": "https://registry.npmjs.org/snekfetch/-/snekfetch-3.6.4.tgz", 85 | "integrity": "sha512-NjxjITIj04Ffqid5lqr7XdgwM7X61c/Dns073Ly170bPQHLm6jkmelye/eglS++1nfTWktpP6Y2bFXjdPlQqdw==" 86 | }, 87 | "trello-events": { 88 | "version": "0.1.6", 89 | "resolved": "https://registry.npmjs.org/trello-events/-/trello-events-0.1.6.tgz", 90 | "integrity": "sha1-hDQckGPU4SDHq0uwbXkT1/vGl9o=" 91 | }, 92 | "tweetnacl": { 93 | "version": "1.0.0", 94 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.0.tgz", 95 | "integrity": "sha1-cT2LgY2kIGh0C/aDhtBHnmb8ins=" 96 | }, 97 | "ws": { 98 | "version": "4.1.0", 99 | "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", 100 | "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==" 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trellobot", 3 | "version": "1.0.0", 4 | "description": "A Discord bot for logging Trello events.", 5 | "main": "trellobot.js", 6 | "scripts": { 7 | "start": "node trellobot.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Angush/trellobot.git" 12 | }, 13 | "author": "Angush", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/Angush/trellobot/issues" 17 | }, 18 | "homepage": "https://github.com/Angush/trellobot#readme", 19 | "dependencies": { 20 | "discord.js": "^11.3.2", 21 | "trello-events": "^0.1.6" 22 | }, 23 | "nodemonConfig": { 24 | "ignore": ["*.md", ".latestActivityID"] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Trellobot 2 | A simple Discord bot to log and report events from your Trello boards in your Discord server. 3 | 4 | ![Image example of Trellobot alert](https://raw.githubusercontent.com/Angush/trellobot/master/example-alert.png "Image example of Trellobot alert") 5 | 6 | ## Setup 7 | 1. Clone repository. 8 | 2. Run `npm install`. 9 | 3. Configure `conf.json` file as desired ([see below](#confjson)). 10 | 4. Generate tokens and set up `.auth` file ([see below](#auth)). 11 | 5. All done. Run Trellobot with `node trellobot.js`. 12 | 13 | ## conf.json 14 | There are several important values in here which inform Trellobot's operation. Here's what they're all for, and how to set them. 15 | 16 | *(optional properties marked with * asterisks)* 17 | 18 | Property | Explanation 19 | ---------------- | ----------- 20 | `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`). 21 | `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. 22 | `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. 23 | `pollInterval` | An integer determining how often (in milliseconds) Trellobot polls your boards for activity. 24 | `prefix`* | A string determining the prefix for Trellobot commands in Discord. Currently unused. Defaults to `.` (period). 25 | `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. 26 | `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). 27 | `userIDs`* | An object mapping Discord IDs to Trello usernames, like so: `userIDs: {"TrelloUser": "1395184357104955", ...}`, so Trellobot can pull relevant user data from Discord. 28 | `realNames`* | A boolean (defaulting to true) that determines whether Trellobot uses the full names or usernames from Trello (eg. `John Smith` vs `jsmiff2`) 29 | 30 | You can refer to the `conf.json` included in the repository for an example. 31 | 32 | ## .auth 33 | 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: 34 | 35 | Property | How to get the value 36 | -------------- | ---------------------- 37 | `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. 38 | `trelloKey` | Visit [this page](https://trello.com/1/appKey/generate) to generate your public Trello API key. 39 | `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. 40 | 41 | That's all for now. 42 | 43 | *i know the name is lame* -------------------------------------------------------------------------------- /trellobot.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js') 2 | const bot = new Discord.Client() 3 | const fs = require('fs') 4 | const auth = JSON.parse(fs.readFileSync('.auth')) 5 | const conf = JSON.parse(fs.readFileSync('conf.json')) 6 | let latestActivityID = fs.existsSync('.latestActivityID') ? fs.readFileSync('.latestActivityID') : 0 7 | 8 | const Trello = require('trello-events') 9 | const events = new Trello({ 10 | pollFrequency: conf.pollInterval, // milliseconds 11 | minId: latestActivityID, // auto-created and auto-updated 12 | start: false, 13 | trello: { 14 | boards: conf.boardIDs, // array of Trello board IDs 15 | key: auth.trelloKey, // your public Trello API key 16 | token: auth.trelloToken // your private Trello token for Trellobot 17 | } 18 | }) 19 | 20 | 21 | 22 | /* 23 | ** ===================================== 24 | ** Discord event handlers and functions. 25 | ** ===================================== 26 | */ 27 | 28 | bot.login(auth.discordToken) 29 | bot.on('ready', () => { 30 | let guild = bot.guilds.get(conf.serverID) 31 | let channel = bot.channels.get(conf.channelID) 32 | if (!guild) { 33 | 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`) 34 | process.exit() 35 | } else if (!channel) { 36 | 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.`) 37 | process.exit() 38 | } else if (!conf.boardIDs || conf.boardIDs.length < 1) { 39 | 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.`) 40 | } 41 | conf.guild = guild 42 | conf.channel = channel 43 | /* 44 | ** Make contentString a map of event names to their paired strings 45 | ** like this: {"createCard": "someone created a card", ...}, so you 46 | ** can, for example, ping specific roles for specific events. 47 | ** 48 | ** Also add a new conf section for pairing lists within a board to 49 | ** contentStrings? That way you can ping one role for new Moderation 50 | ** cards, and another role for new Event cards, for example. 51 | */ 52 | if (!conf.contentString) conf.contentString = "" 53 | if (!conf.enabledEvents) conf.enabledEvents = [] 54 | if (!conf.userIDs) conf.userIDs = {} 55 | if (!conf.realNames) conf.realNames = true 56 | // set default prefix is none provided in conf 57 | if (!conf.prefix) { 58 | conf.prefix = "." 59 | fs.writeFileSync('conf.json', JSON.stringify(conf, null, 4), (err, data) => console.log(`Updated conf file with default prefix ('.')`)) 60 | } 61 | // logInitializationData() 62 | console.log(`== Bot logged in as @${bot.user.tag}. Ready for action! ==`) 63 | events.start() 64 | }) 65 | 66 | bot.on('message', (msg) => { 67 | if (msg.channel.type !== "text") return 68 | if (msg.content.startsWith(`${conf.prefix}ping`)) { 69 | let now = Date.now() 70 | msg.channel.send(`Ping!`).then(m => { 71 | m.edit(`Pong! (took ${Date.now() - now}ms)`) 72 | }) 73 | } 74 | }) 75 | 76 | 77 | 78 | /* 79 | ** ==================================== 80 | ** Trello event handlers and functions. 81 | ** ==================================== 82 | */ 83 | 84 | // Fired when a card is created 85 | events.on('createCard', (event, board) => { 86 | if (!eventEnabled(`cardCreated`)) return 87 | let embed = getEmbedBase(event) 88 | .setTitle(`New card created under __${event.data.list.name}__!`) 89 | .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})**`) 90 | send(addDiscordUserData(embed, event.memberCreator)) 91 | }) 92 | 93 | // Fired when a card is updated (description, due date, position, associated list, name, and archive status) 94 | events.on('updateCard', (event, board) => { 95 | let embed = getEmbedBase(event) 96 | if (event.data.old.hasOwnProperty("desc")) { 97 | if (!eventEnabled(`cardDescriptionChanged`)) return 98 | embed 99 | .setTitle(`Card description changed!`) 100 | .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})**`) 101 | .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]*`) 102 | .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]*`) 103 | send(addDiscordUserData(embed, event.memberCreator)) 104 | } else if (event.data.old.hasOwnProperty("due")) { 105 | if (!eventEnabled(`cardDueDateChanged`)) return 106 | embed 107 | .setTitle(`Card due date changed!`) 108 | .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})**`) 109 | send(addDiscordUserData(embed, event.memberCreator)) 110 | } else if (event.data.old.hasOwnProperty("pos")) { 111 | if (!eventEnabled(`cardPositionChanged`)) return 112 | embed 113 | .setTitle(`Card position changed!`) 114 | .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})**`) 115 | send(addDiscordUserData(embed, event.memberCreator)) 116 | } else if (event.data.old.hasOwnProperty("idList")) { 117 | if (!eventEnabled(`cardListChanged`)) return 118 | embed 119 | .setTitle(`Card list changed!`) 120 | .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})**`) 121 | send(addDiscordUserData(embed, event.memberCreator)) 122 | } else if (event.data.old.hasOwnProperty("name")) { 123 | if (!eventEnabled(`cardNameChanged`)) return 124 | embed 125 | .setTitle(`Card name changed!`) 126 | .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})**`) 127 | .addField(`New Name`, event.data.card.name) 128 | .addField(`Old Name`, event.data.old.name) 129 | send(addDiscordUserData(embed, event.memberCreator)) 130 | } else if (event.data.old.hasOwnProperty("closed")) { 131 | if (event.data.old.closed) { 132 | if (!eventEnabled(`cardUnarchived`)) return 133 | embed 134 | .setTitle(`Card unarchived!`) 135 | .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})**`) 136 | send(addDiscordUserData(embed, event.memberCreator)) 137 | } else { 138 | if (!eventEnabled(`cardArchived`)) return 139 | embed 140 | .setTitle(`Card archived!`) 141 | .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})**`) 142 | send(addDiscordUserData(embed, event.memberCreator)) 143 | } 144 | } 145 | }) 146 | 147 | // Fired when a card is deleted 148 | events.on('deleteCard', (event, board) => { 149 | if (!eventEnabled(`cardDeleted`)) return 150 | let embed = getEmbedBase(event) 151 | .setTitle(`Card deleted!`) 152 | .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})**`) 153 | send(addDiscordUserData(embed, event.memberCreator)) 154 | }) 155 | 156 | // Fired when a comment is posted, or edited 157 | events.on('commentCard', (event, board) => { 158 | let embed = getEmbedBase(event) 159 | if (event.data.hasOwnProperty("textData")) { 160 | if (!eventEnabled(`commentEdited`)) return 161 | embed 162 | .setTitle(`Comment edited on card!`) 163 | .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})**`) 164 | .addField(`Comment Text`, event.data.text.length > 1024 ? `${event.data.text.trim().slice(0, 1020)}...` : event.data.text) 165 | .setTimestamp(event.data.dateLastEdited) 166 | send(addDiscordUserData(embed, event.memberCreator)) 167 | } else { 168 | if (!eventEnabled(`commentAdded`)) return 169 | embed 170 | .setTitle(`Comment added to card!`) 171 | .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})**`) 172 | .addField(`Comment Text`, event.data.text.length > 1024 ? `${event.data.text.trim().slice(0, 1020)}...` : event.data.text) 173 | send(addDiscordUserData(embed, event.memberCreator)) 174 | } 175 | }) 176 | 177 | // Fired when a member is added to a card 178 | events.on('addMemberToCard', (event, board) => { 179 | let embed = getEmbedBase(event) 180 | .setTitle(`Member added to card!`) 181 | .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})**`) 182 | let editedEmbed = addDiscordUserData(embed, event.member) 183 | 184 | if (event.member.id === event.memberCreator.id) { 185 | if (!eventEnabled(`memberAddedToCardBySelf`)) return 186 | editedEmbed.setDescription(editedEmbed.description + ` added themselves to card.`) 187 | send(editedEmbed) 188 | } else { 189 | if (!eventEnabled(`memberAddedToCard`)) return 190 | editedEmbed.setDescription(editedEmbed.description + ` added to card by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) 191 | send(addDiscordUserData(editedEmbed, event.memberCreator)) 192 | } 193 | }) 194 | 195 | // Fired when a member is removed from a card 196 | events.on('removeMemberFromCard', (event, board) => { 197 | let embed = getEmbedBase(event) 198 | .setTitle(`Member removed from card!`) 199 | .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})**`) 200 | let editedEmbed = addDiscordUserData(embed, event.member) 201 | 202 | if (event.member.id === event.memberCreator.id) { 203 | if (!eventEnabled(`memberRemovedFromCardBySelf`)) return 204 | editedEmbed.setDescription(editedEmbed.description + ` removed themselves from card.`) 205 | send(editedEmbed) 206 | } else { 207 | if (!eventEnabled(`memberRemovedFromCard`)) return 208 | editedEmbed.setDescription(editedEmbed.description + ` removed from card by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) 209 | send(addDiscordUserData(editedEmbed, event.memberCreator)) 210 | } 211 | }) 212 | 213 | // Fired when a list is created 214 | events.on('createList', (event, board) => { 215 | if (!eventEnabled(`listCreated`)) return 216 | let embed = getEmbedBase(event) 217 | .setTitle(`New list created!`) 218 | .setDescription(`**EVENT:** List __${event.data.list.name}__ created by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) 219 | send(addDiscordUserData(embed, event.memberCreator)) 220 | }) 221 | 222 | // Fired when a list is renamed, moved, archived, or unarchived 223 | events.on('updateList', (event, board) => { 224 | let embed = getEmbedBase(event) 225 | if (event.data.old.hasOwnProperty("name")) { 226 | if (!eventEnabled(`listNameChanged`)) return 227 | embed 228 | .setTitle(`List name changed!`) 229 | .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})**`) 230 | send(addDiscordUserData(embed, event.memberCreator)) 231 | } else if (event.data.old.hasOwnProperty("pos")) { 232 | if (!eventEnabled(`listPositionChanged`)) return 233 | embed 234 | .setTitle(`List position changed!`) 235 | .setDescription(`**EVENT:** List __${event.data.list.name}__ position changed by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) 236 | send(addDiscordUserData(embed, event.memberCreator)) 237 | } else if (event.data.old.hasOwnProperty("closed")) { 238 | if (event.data.old.closed) { 239 | if (!eventEnabled(`listUnarchived`)) return 240 | embed 241 | .setTitle(`List unarchived!`) 242 | .setDescription(`**EVENT:** List __${event.data.list.name}__ unarchived by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) 243 | send(addDiscordUserData(embed, event.memberCreator)) 244 | } else { 245 | if (!eventEnabled(`listArchived`)) return 246 | embed 247 | .setTitle(`List archived!`) 248 | .setDescription(`**EVENT:** List __${event.data.list.name}__ archived by **[${conf.realNames ? event.memberCreator.fullName : event.memberCreator.username}](https://trello.com/${event.memberCreator.username})**`) 249 | send(addDiscordUserData(embed, event.memberCreator)) 250 | } 251 | } 252 | }) 253 | 254 | // Fired when an attachment is added to a card 255 | events.on('addAttachmentToCard', (event, board) => { 256 | if (!eventEnabled(`attachmentAddedToCard`)) return 257 | let embed = getEmbedBase(event) 258 | .setTitle(`Attachment added to card!`) 259 | .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})**`) 260 | send(addDiscordUserData(embed, event.memberCreator)) 261 | }) 262 | 263 | // Fired when an attachment is removed from a card 264 | events.on('deleteAttachmentFromCard', (event, board) => { 265 | if (!eventEnabled(`attachmentRemovedFromCard`)) return 266 | let embed = getEmbedBase(event) 267 | .setTitle(`Attachment removed from card!`) 268 | .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})**`) 269 | send(addDiscordUserData(embed, event.memberCreator)) 270 | }) 271 | 272 | // Fired when a checklist is added to a card (same thing as created) 273 | events.on('addChecklistToCard', (event, board) => { 274 | if (!eventEnabled(`checklistAddedToCard`)) return 275 | let embed = getEmbedBase(event) 276 | .setTitle(`Checklist added to card!`) 277 | .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})**`) 278 | send(addDiscordUserData(embed, event.memberCreator)) 279 | }) 280 | 281 | // Fired when a checklist is removed from a card (same thing as deleted) 282 | events.on('removeChecklistFromCard', (event, board) => { 283 | if (!eventEnabled(`checklistRemovedFromCard`)) return 284 | let embed = getEmbedBase(event) 285 | .setTitle(`Checklist removed from card!`) 286 | .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})**`) 287 | send(addDiscordUserData(embed, event.memberCreator)) 288 | }) 289 | 290 | // Fired when a checklist item's completion status is toggled 291 | events.on('updateCheckItemStateOnCard', (event, board) => { 292 | if (event.data.checkItem.state === "complete") { 293 | if (!eventEnabled(`checklistItemMarkedComplete`)) return 294 | let embed = getEmbedBase(event) 295 | .setTitle(`Checklist item marked complete!`) 296 | .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})**`) 297 | .addField(`Checklist Item Name`, event.data.checkItem.name.length > 1024 ? `${event.data.checkItem.name.trim().slice(0, 1020)}...` : event.data.checkItem.name) 298 | send(addDiscordUserData(embed, event.memberCreator)) 299 | } else if (event.data.checkItem.state === "incomplete") { 300 | if (!eventEnabled(`checklistItemMarkedIncomplete`)) return 301 | let embed = getEmbedBase(event) 302 | .setTitle(`Checklist item marked incomplete!`) 303 | .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})**`) 304 | .addField(`Checklist Item Name`, event.data.checkItem.name.length > 1024 ? `${event.data.checkItem.name.trim().slice(0, 1020)}...` : event.data.checkItem.name) 305 | send(addDiscordUserData(embed, event.memberCreator)) 306 | } 307 | }) 308 | 309 | 310 | 311 | /* 312 | ** ======================= 313 | ** Miscellaneous functions 314 | ** ======================= 315 | */ 316 | events.on('maxId', (id) => { 317 | if (latestActivityID == id) return 318 | latestActivityID = id 319 | fs.writeFileSync('.latestActivityID', id) 320 | }) 321 | 322 | const send = (embed, content = ``) => conf.channel.send(`${content} ${conf.contentString}`, {embed:embed}).catch(err => console.error(err)) 323 | 324 | const eventEnabled = (type) => conf.enabledEvents.length > 0 ? conf.enabledEvents.includes(type) : true 325 | 326 | const logEventFire = (event) => console.log(`${new Date(event.date).toUTCString()} - ${event.type} fired`) 327 | 328 | const getEmbedBase = (event) => new Discord.RichEmbed() 329 | .setFooter(`${conf.guild.members.get(bot.user.id).displayName} • ${event.data.board.name} [${event.data.board.shortLink}]`, bot.user.displayAvatarURL) 330 | .setTimestamp(event.hasOwnProperty(`date`) ? event.date : Date.now()) 331 | .setColor("#127ABD") 332 | 333 | // Converts Trello @username mentions in titles to Discord mentions, finds channel and role mentions, and mirros Discord user mentions outside the embed 334 | const convertMentions = (embed, event) => { 335 | 336 | } 337 | 338 | // adds thumbanil and appends user mention to the end of the description, if possible 339 | const addDiscordUserData = (embed, member) => { 340 | if (conf.userIDs[member.username]) { 341 | let discordUser = conf.guild.members.get(conf.userIDs[member.username]) 342 | if (discordUser) embed 343 | .setThumbnail(discordUser.user.displayAvatarURL) 344 | .setDescription(`${embed.description} / ${discordUser.toString()}`) 345 | } 346 | return embed 347 | } 348 | 349 | // logs initialization data (stuff loaded from conf.json) - mostly for debugging purposes 350 | const logInitializationData = () => console.log(`== INITIALIZING WITH: 351 | latestActivityID - ${latestActivityID} 352 | boardIDs --------- ${conf.boardIDs.length + " [" + conf.boardIDs.join(", ") + "]"} 353 | serverID --------- ${conf.serverID} (${conf.guild.name}) 354 | channelID -------- ${conf.channelID} (#${conf.channel.name}) 355 | pollInterval ----- ${conf.pollInterval} ms (${conf.pollInterval / 1000} seconds) 356 | prefix ----------- "${conf.prefix}"${conf.prefix === "." ? " (default)" : ""} 357 | contentString ---- ${conf.contentString !== "" ? "\"" + conf.contentString + "\"" : "none"} 358 | enabledEvents ---- ${conf.enabledEvents.length > 0 ? conf.enabledEvents.length + " [" + conf.enabledEvents.join(", ") + "]" : "all"} 359 | userIDs ---------- ${Object.getOwnPropertyNames(conf.userIDs).length}`) 360 | --------------------------------------------------------------------------------