├── .env.example ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── db ├── migrate-from-mongo.js └── migrations │ ├── 2019_11_25_000000_create_tables.js │ ├── 2019_11_25_000001_create_channel_repos_table.js │ └── 2019_11_25_000002_create_channel_orgs_table.js ├── knexfile.js ├── lib ├── Discord │ ├── Client.js │ ├── Command.js │ ├── Commands │ │ ├── Announce.js │ │ ├── Clean.js │ │ ├── Conf.js │ │ ├── Data.js │ │ ├── Eval.js │ │ ├── Exec.js │ │ ├── GitlabInit.js │ │ ├── GitlabInitOrg.js │ │ ├── GitlabIssue.js │ │ ├── GitlabMergeRequest.js │ │ ├── GitlabRemove.js │ │ ├── Help.js │ │ ├── Invite.js │ │ ├── Ping.js │ │ ├── Reboot.js │ │ ├── Reload.js │ │ ├── Stats.js │ │ └── Update.js │ ├── Module.js │ ├── Modules │ │ ├── RunCommand.js │ │ └── UnhandledError.js │ └── index.js ├── Gitlab │ ├── Constants.js │ ├── EventHandler.js │ ├── EventResponse.js │ ├── Events │ │ ├── Unknown.js │ │ ├── issue-close.js │ │ ├── issue-open.js │ │ ├── issue-reopen.js │ │ ├── issue-update.js │ │ ├── issue.js │ │ ├── merge_request-approved.js │ │ ├── merge_request-close.js │ │ ├── merge_request-merge.js │ │ ├── merge_request-open.js │ │ ├── merge_request-reopen.js │ │ ├── merge_request-unapproved.js │ │ ├── merge_request-update.js │ │ ├── note-commit.js │ │ ├── note-issue.js │ │ ├── note-mergerequest.js │ │ ├── note-snippet.js │ │ ├── pipeline.js │ │ ├── push.js │ │ ├── tag_push.js │ │ ├── wiki_page-create.js │ │ ├── wiki_page-delete.js │ │ └── wiki_page-update.js │ ├── index.js │ └── parser.js ├── Models │ ├── Channel.js │ ├── ChannelOrg.js │ ├── ChannelRepo.js │ ├── Guild.js │ ├── Model.js │ ├── index.js │ └── initialization.js ├── Util │ ├── Branch.js │ ├── Log.js │ ├── MergeDefault.js │ ├── Pad.js │ ├── YappyGitLab.js │ ├── filter.js │ └── index.js ├── Web.js └── index.js ├── package-lock.json ├── package.json └── views ├── index.hbs └── layout.hbs /.env.example: -------------------------------------------------------------------------------- 1 | WEB_IP= 2 | WEB_PORT= 3 | 4 | DISCORD_TOKEN= 5 | DISCORD_CLIENT_ID= 6 | DISCORD_CLIENT_SECRET= 7 | DISCORD_CHANNEL_LOGGING= 8 | 9 | BDPW_KEY= 10 | 11 | LOG_LEVEL=info 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .env 4 | .vscode 5 | db/*.sqlite -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.9.1](https://github.com/YappyBots/YappyGitLab/compare/v1.9.0...v1.9.1) (2020-04-08) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **discord:** allow DMs ([b34d95f](https://github.com/YappyBots/YappyGitLab/commit/b34d95f32a22abf6e4260ef6ec64a8a00ddb4ac0)) 7 | * **discord: commands:** data: don't spam, send in DMs ([5de6989](https://github.com/YappyBots/YappyGitLab/commit/5de6989c47cd40b800d46c9c02728292ef9782cc)) 8 | * **web:** allow events with no branches ([da428d0](https://github.com/YappyBots/YappyGitLab/commit/da428d07fc45b4453f264c900231cbc5495f348f)) 9 | * **web:** oh i used something not supported whoops ([3c2b05f](https://github.com/YappyBots/YappyGitLab/commit/3c2b05ff264a38dd12f6ef31db2d4d9f24c5d310)) 10 | 11 | 12 | 13 | # [1.9.0](https://github.com/YappyBots/YappyGitLab/compare/v1.8.3...v1.9.0-dev) (2020-04-08) 14 | 15 | * Discord.js v12 update 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **web:** update web code for discord v12 ([d58b62b](https://github.com/YappyBots/YappyGitLab/commit/d58b62b96f9958aba96de75b9ddc96f8bb0cebff)) 21 | 22 | 23 | ### Features 24 | 25 | * **discord: commands:** add 'data' administrator commands for insight on bot data usage ([2ea2be1](https://github.com/YappyBots/YappyGitLab/commit/2ea2be1f6540d715e8b75b2fc661356d7cb29cf4)) 26 | 27 | 28 | 29 | ## [1.8.3](https://github.com/YappyBots/YappyGitLab/compare/v1.8.2...v1.8.3) (2020-03-24) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * **discord: commands:** conf: prevent adding empty name events to filter list ([1dc4515](https://github.com/YappyBots/YappyGitLab/commit/1dc451594df9903753a1f5db5a9272169720e234)) 35 | * **discord: commands:** init: fix code error if no repo provided ([ec5d48a](https://github.com/YappyBots/YappyGitLab/commit/ec5d48aab5297b2d70786d99896c2e0f84bedfb6)) 36 | * **gitlab:** fix errors thrown when checking repo existance & not including all subgroups ([d5c8547](https://github.com/YappyBots/YappyGitLab/commit/d5c8547e27bf0ba5d9e624081f4d50c7b4cc21e1)) 37 | * **gitlab:** fix only URLs working ([54020b3](https://github.com/YappyBots/YappyGitLab/commit/54020b32819256414a2ec852b9ec200b7efac151)) 38 | * **gitlab:** parser: fix using wrong capture group ([a2ee421](https://github.com/YappyBots/YappyGitLab/commit/a2ee42103dbb53b00bd9633123cb2ff0fa346e8f)) 39 | * **gitlab:** replace url parser again with custom one for gitlab groups ([1a44dfc](https://github.com/YappyBots/YappyGitLab/commit/1a44dfc8b71a4389517a29b9a9cf6f88ed0bf358)) 40 | * remove mongoose require ([818e789](https://github.com/YappyBots/YappyGitLab/commit/818e7890899af77bfb30fd27af542f5f690cae20)) 41 | 42 | 43 | 44 | ## [1.8.2](https://github.com/YappyBots/YappyGitLab/compare/v1.8.1...v1.8.2) (2019-12-04) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * **discord:** do not exit on disconnect ([53f4a47](https://github.com/YappyBots/YappyGitLab/commit/53f4a473e6e03a3f7d995a0f3b792586531cb32e)) 50 | 51 | 52 | 53 | ## [1.8.1](https://github.com/YappyBots/YappyGitLab/compare/v1.8.0...v1.8.1) (2019-12-04) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * **discord: commands:** init: show usage if repo not provided ([89df14f](https://github.com/YappyBots/YappyGitLab/commit/89df14fa512a2b169c481d3a49ea7bcd40563116)) 59 | * **discord: commands:** invite: fix error when running ([1da955f](https://github.com/YappyBots/YappyGitLab/commit/1da955f637e4d76553b02603b2bdef67d1d64e4c)) 60 | * **models:** fix errors thrown when model doesn't exist in db ([d9673d0](https://github.com/YappyBots/YappyGitLab/commit/d9673d08d713f907d46c6693bb391453fc7def46)) 61 | * **npm:** remove snyk mentions from npm scripts to fix deploy script ([01890da](https://github.com/YappyBots/YappyGitLab/commit/01890dadaa846b08ec3b6230d60178c54a4e1633)) 62 | 63 | 64 | 65 | # [1.8.0](https://github.com/YappyBots/YappyGitLab/compare/v1.7.2...v1.8.0) (2019-11-28) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * **db:** add 'job' event to automatically ignored (not styled yet) ([2dc511f](https://github.com/YappyBots/YappyGitLab/commit/2dc511f4d10077ea6ef3cf40db0eb5a8c3bf7b8b)) 71 | * **db:** fix initialization using nonexistent 'has' method ([c985e68](https://github.com/YappyBots/YappyGitLab/commit/c985e68c2454510cfc64c51af08a9a2bcc515a21)) 72 | * **db:** update yappy addons package name and passing models ([10976c4](https://github.com/YappyBots/YappyGitLab/commit/10976c47817236f01389e9b37d67000ede4eb70d)) 73 | * **dependencies:** add punycode ([20ffbe8](https://github.com/YappyBots/YappyGitLab/commit/20ffbe88c229f144af7bd43cbb176c5b43fcad6b)) 74 | * **discord:** restart on disconnect ([405172a](https://github.com/YappyBots/YappyGitLab/commit/405172ab05c34b846a9c65a61b5f8ef8b457f4a9)) 75 | * **discord: commands:** conf: don't check if event exists ([507ad6d](https://github.com/YappyBots/YappyGitLab/commit/507ad6d01fc001f197a53258a294e47fcdc5318c)) 76 | * **discord: commands:** conf: fix Action.DISABLE not working (was falsy value) ([c013c5d](https://github.com/YappyBots/YappyGitLab/commit/c013c5d7e02e6bfaac98ee5d20838280f8aac3b3)) 77 | * **discord: commands:** ping: update code for latest djs ([4534588](https://github.com/YappyBots/YappyGitLab/commit/4534588b57453cd16921eba8b5ae0f16270e978a)) 78 | * **github:** allow job events, improve url parser ([34ef9ec](https://github.com/YappyBots/YappyGitLab/commit/34ef9ec39a8fa4f9e6c5ab22df35696bc71ae1ac)), closes [#43](https://github.com/YappyBots/YappyGitLab/issues/43) [#29](https://github.com/YappyBots/YappyGitLab/issues/29) [#52](https://github.com/YappyBots/YappyGitLab/issues/52) 79 | 80 | 81 | ### Features 82 | 83 | * **discord: commands:** init: add support for IDN ([#36](https://github.com/YappyBots/YappyGitLab/issues/36)) ([7f811ed](https://github.com/YappyBots/YappyGitLab/commit/7f811ed4378b23ec38965c6910d7441d800e7849)) 84 | 85 | 86 | ### Performance Improvements 87 | 88 | * **db:** cache guild prefixes ([9d9e41d](https://github.com/YappyBots/YappyGitLab/commit/9d9e41d9393b635f0c9fc3e1d93eb856a66f4bcf)) 89 | * **db:** switch to sqlite, add whitelist/blacklist system to conf ([cffdb35](https://github.com/YappyBots/YappyGitLab/commit/cffdb3594bfc0d034ab6f0c1d5b92b70368b11dc)) 90 | 91 | 92 | 93 | 94 | ## [1.7.1](https://github.com/YappyBots/YappyGitLab/compare/v1.7.0...v1.7.1) (2018-07-18) 95 | 96 | 97 | 98 | 99 | # [1.7.0](https://github.com/YappyBots/YappyGitLab/compare/v1.6.1...v1.7.0) (2018-07-18) 100 | 101 | 102 | ### Features 103 | 104 | * **gitlab: events:** merge_request/approved and merge_request/unapproved ([#34](https://github.com/YappyBots/YappyGitLab/issues/34)) ([01bf76e](https://github.com/YappyBots/YappyGitLab/commit/01bf76e)) 105 | 106 | 107 | 108 | 109 | ## [1.6.1](https://github.com/YappyBots/YappyGitLab/compare/v1.6.0...v1.6.1) (2018-07-16) 110 | 111 | 112 | ### Bug Fixes 113 | 114 | * **models: channelconfig:** fix collection using deprecated find method ([7f29fb5](https://github.com/YappyBots/YappyGitLab/commit/7f29fb5)) 115 | * **moduls: channelconfig:** make sure a string isn't split by character onto array ([4a86d74](https://github.com/YappyBots/YappyGitLab/commit/4a86d74)) 116 | 117 | 118 | 119 | 120 | # [1.6.0](https://github.com/YappyBots/YappyGitLab/compare/v1.5.7...v1.6.0) (2018-07-09) 121 | 122 | 123 | ### Features 124 | 125 | * **discord: commands:** add `conf events [view|list|ignore|disable|enable|unignore] [event]` ([3c88665](https://github.com/YappyBots/YappyGitLab/commit/3c88665)) 126 | 127 | 128 | 129 | 130 | ## [1.5.7](https://github.com/YappyBots/YappyGitLab/compare/v1.5.6...v1.5.7) (2018-06-27) 131 | 132 | 133 | ### Bug Fixes 134 | 135 | * **gitlab: events:** fix event handler getting property from undefined ([9b9b94a](https://github.com/YappyBots/YappyGitLab/commit/9b9b94a)) 136 | 137 | 138 | 139 | 140 | ## [1.5.5](https://github.com/YappyBots/YappyGitLab/compare/v1.5.4...v1.5.5) (2018-06-25) 141 | 142 | 143 | ### Bug Fixes 144 | 145 | * **discord: commands:** conf: fix syntax error °_° ([ab16b27](https://github.com/YappyBots/YappyGitLab/commit/ab16b27)) 146 | 147 | 148 | 149 | 150 | ## [1.5.4](https://github.com/YappyBots/YappyGitLab/compare/v1.5.3...v1.5.4) (2018-06-25) 151 | 152 | 153 | ### Bug Fixes 154 | 155 | * **discord: commands:** conf: fix arg containing `-g` being ignored even if not global ([52271a9](https://github.com/YappyBots/YappyGitLab/commit/52271a9)) 156 | 157 | 158 | 159 | 160 | ## [1.5.3](https://github.com/YappyBots/YappyGitLab/compare/v1.5.2...v1.5.3) (2018-06-25) 161 | 162 | 163 | ### Bug Fixes 164 | 165 | * **discord: commands:** fix `-g` included in the argument name being interpreted as global ([0a10ef5](https://github.com/YappyBots/YappyGitLab/commit/0a10ef5)) 166 | 167 | 168 | 169 | 170 | ## [1.5.2](https://github.com/YappyBots/YappyGitLab/compare/v1.5.1...v1.5.2) (2018-06-19) 171 | 172 | 173 | ### Bug Fixes 174 | 175 | * **discord: commands:** init/remove: fix showing 'false' if embeds are enabled ([9796079](https://github.com/YappyBots/YappyGitLab/commit/9796079)) 176 | * **gitlab:** parser: fix double backslash if no subgroup ([67f7cbc](https://github.com/YappyBots/YappyGitLab/commit/67f7cbc)) 177 | 178 | 179 | 180 | 181 | ## [1.5.1](https://github.com/YappyBots/YappyGitLab/compare/v1.5.0...v1.5.1) (2018-06-17) 182 | 183 | 184 | ### Bug Fixes 185 | 186 | * **gitlab:** parser: include subgroup(s) in full repo name ([9263b1f](https://github.com/YappyBots/YappyGitLab/commit/9263b1f)) 187 | 188 | 189 | 190 | 191 | # [1.5.0](https://github.com/YappyBots/YappyGitLab/compare/v1.4.2...v1.5.0) (2018-06-17) 192 | 193 | 194 | ### Bug Fixes 195 | 196 | * **dependencies:** update discordjs/discord.js to latest master ([0198e1e](https://github.com/YappyBots/YappyGitLab/commit/0198e1e)) 197 | * **discord: commands:** fix initorg using project name instead of path ([6934709](https://github.com/YappyBots/YappyGitLab/commit/6934709)) 198 | * **discord: commands:** include private repos in initorg ([a1c2bcb](https://github.com/YappyBots/YappyGitLab/commit/a1c2bcb)) 199 | * **discord: commands:** initorg: fix `e` undefined ([568cad0](https://github.com/YappyBots/YappyGitLab/commit/568cad0)) 200 | * **discord: commands:** initorg: fix success message error ([ff03906](https://github.com/YappyBots/YappyGitLab/commit/ff03906)) 201 | * **discord: commands:** update texts with 'github' to 'gitlab' ([5053987](https://github.com/YappyBots/YappyGitLab/commit/5053987)) 202 | * **gitlab:** move embed description limit into handler parser ([e5e7155](https://github.com/YappyBots/YappyGitLab/commit/e5e7155)) 203 | * **gitlab:** rework regex parser to accept more url variations ([e85d845](https://github.com/YappyBots/YappyGitLab/commit/e85d845)) 204 | * **gitlab: events:** merge_request/merge: fix error if merge commit sha is undefined ([2ec858a](https://github.com/YappyBots/YappyGitLab/commit/2ec858a)), closes [#26](https://github.com/YappyBots/YappyGitLab/issues/26) 205 | * **gitlab: events:** wiki_page/create: fix error in text mode ([7e79df6](https://github.com/YappyBots/YappyGitLab/commit/7e79df6)) 206 | * **log:** remove binding of #message in Log ([e132410](https://github.com/YappyBots/YappyGitLab/commit/e132410)) 207 | * **models: channelconfig:** don't add channel config if it's missing a value ([d4e8522](https://github.com/YappyBots/YappyGitLab/commit/d4e8522)) 208 | * **web:** fix error if an error occurs when generating eventResponse ([8c4e633](https://github.com/YappyBots/YappyGitLab/commit/8c4e633)), closes [#27](https://github.com/YappyBots/YappyGitLab/issues/27) 209 | * **web:** fix webhooks without action not working... ? ([1246cb4](https://github.com/YappyBots/YappyGitLab/commit/1246cb4)), closes [#31](https://github.com/YappyBots/YappyGitLab/issues/31) 210 | * **web:** increase body limit to 500kb ([02fb01e](https://github.com/YappyBots/YappyGitLab/commit/02fb01e)), closes [#24](https://github.com/YappyBots/YappyGitLab/issues/24) 211 | * **web:** set body limit to 5mb ([1fff9cb](https://github.com/YappyBots/YappyGitLab/commit/1fff9cb)) 212 | 213 | 214 | ### Features 215 | 216 | * **discord:** use addons command, allows for commands page ([0e53dcd](https://github.com/YappyBots/YappyGitLab/commit/0e53dcd)) 217 | * **gitlab: events:** add issue ([#31](https://github.com/YappyBots/YappyGitLab/issues/31)) ([8bfc74f](https://github.com/YappyBots/YappyGitLab/commit/8bfc74f)) 218 | 219 | 220 | ### Performance Improvements 221 | 222 | * **models: serverconfig:** only save server config into cache for servers where messages are being ([7ca752d](https://github.com/YappyBots/YappyGitLab/commit/7ca752d)) 223 | 224 | 225 | 226 | 227 | ## [1.4.2](https://github.com/YappyBots/YappyGitLab/compare/v1.4.1...v1.4.2) (2018-02-17) 228 | 229 | 230 | ### Bug Fixes 231 | 232 | * **gitlab:** fix repo using input and not parsed output ([7797844](https://github.com/YappyBots/YappyGitLab/commit/7797844)) 233 | * **gitlab:** rework parser to support .git and multiple organization groups ([eb49c5c](https://github.com/YappyBots/YappyGitLab/commit/eb49c5c)) 234 | * **gitlab:** rework parser url to allow urls starting with git@ ([101ab7a](https://github.com/YappyBots/YappyGitLab/commit/101ab7a)) 235 | 236 | 237 | 238 | 239 | ## [1.4.1](https://github.com/YappyBots/YappyGitLab/compare/v1.4.0...v1.4.1) (2018-02-17) 240 | 241 | 242 | ### Bug Fixes 243 | 244 | * **dependencies:** update bufferutil, chalk, moment1, snekfetch, snyk, eslint, dotenv ([c274ed7](https://github.com/YappyBots/YappyGitLab/commit/c274ed7)) 245 | * **discord:** fix DM's not working ([ab54c6b](https://github.com/YappyBots/YappyGitLab/commit/ab54c6b)) 246 | * **models: serverconfig:** don't add server config if guild is unavailable ([d9a20da](https://github.com/YappyBots/YappyGitLab/commit/d9a20da)) 247 | * **models: serverconfig:** hopefully fix adding guild that is already in config ([7d87ac9](https://github.com/YappyBots/YappyGitLab/commit/7d87ac9)) 248 | * **snyk:** update policy ([b01390c](https://github.com/YappyBots/YappyGitLab/commit/b01390c)) 249 | 250 | 251 | 252 | 253 | # [1.4.0](https://github.com/YappyBots/YappyGitLab/compare/v1.3.8...v1.4.0) (2017-12-16) 254 | 255 | 256 | ### Bug Fixes 257 | 258 | * **models: channelconfig:** maybe fix e.repos not being defined, set e.repos to [] if null ([e927229](https://github.com/YappyBots/YappyGitLab/commit/e927229)) 259 | 260 | 261 | ### Features 262 | 263 | * **gitlab: events:** add pipeline ([#21](https://github.com/YappyBots/YappyGitLab/issues/21)) ([7e8a790](https://github.com/YappyBots/YappyGitLab/commit/7e8a790)) 264 | * **logging:** add discord logging with yappybots-addons ([5c944c7](https://github.com/YappyBots/YappyGitLab/commit/5c944c7)) 265 | 266 | 267 | 268 | 269 | ## [1.3.8](https://github.com/YappyBots/YappyGitLab/compare/v1.3.7...v1.3.8) (2017-10-21) 270 | 271 | 272 | ### Bug Fixes 273 | 274 | * **discord: commands:** make reboot require owner ([6bac3d5](https://github.com/YappyBots/YappyGitLab/commit/6bac3d5)) 275 | 276 | 277 | 278 | 279 | ## [1.3.7](https://github.com/YappyBots/YappyGitLab/compare/v1.3.6...v1.3.7) (2017-10-14) 280 | 281 | 282 | ### Bug Fixes 283 | 284 | * **discord: commands:** invite - set client id to user ID ([9c72d7d](https://github.com/YappyBots/YappyGitLab/commit/9c72d7d)) 285 | * **models:** fix no config found errors by creating config on the spot ([0098d23](https://github.com/YappyBots/YappyGitLab/commit/0098d23)) 286 | * **snyk:** fix new vulnerabilities ([17ebc6b](https://github.com/YappyBots/YappyGitLab/commit/17ebc6b)) 287 | 288 | 289 | 290 | 291 | ## [1.3.6](https://github.com/YappyBots/YappyGitLab/compare/v1.3.5...v1.3.6) (2017-10-12) 292 | 293 | 294 | ### Bug Fixes 295 | 296 | * **discord: commands:** init - fix allowing multiple inits of same repo ([a2a3fdf](https://github.com/YappyBots/YappyGitLab/commit/a2a3fdf)) 297 | * **discord: commands:** update - show "no output" if no NPM stdout ([4644c7e](https://github.com/YappyBots/YappyGitLab/commit/4644c7e)) 298 | * **gitlab:** fix something with parser get repo thing ([c62d258](https://github.com/YappyBots/YappyGitLab/commit/c62d258)) 299 | * **gitlab:** parser - fix wrong type in repository typedef ([169e91f](https://github.com/YappyBots/YappyGitLab/commit/169e91f)) 300 | 301 | 302 | 303 | 304 | ## [1.3.5](https://github.com/YappyBots/YappyGitLab/compare/v1.3.4...v1.3.5) (2017-10-12) 305 | 306 | 307 | ### Bug Fixes 308 | 309 | * **models:** try to keep alive db connection ([6657d0a](https://github.com/YappyBots/YappyGitLab/commit/6657d0a)) 310 | * **models: serverconfig:** fix this.delete causing error bc it's #deleteGuild ([d84e0fe](https://github.com/YappyBots/YappyGitLab/commit/d84e0fe)) 311 | 312 | 313 | 314 | 315 | ## [1.3.4](https://github.com/YappyBots/YappyGitLab/compare/v1.3.3...v1.3.4) (2017-10-11) 316 | 317 | 318 | ### Bug Fixes 319 | 320 | * **dependencies:** update body-parser, moment, winston, snekfetch, snyk, express, mongoose, jsdoc, e ([b65963d](https://github.com/YappyBots/YappyGitLab/commit/b65963d)) 321 | * **discord: commands:** init - explain how to init private repo ([c1afb94](https://github.com/YappyBots/YappyGitLab/commit/c1afb94)), closes [#13](https://github.com/YappyBots/YappyGitLab/issues/13) 322 | * **gitlab:** use regex for parser, support groups ([79f4f27](https://github.com/YappyBots/YappyGitLab/commit/79f4f27)), closes [#14](https://github.com/YappyBots/YappyGitLab/issues/14) 323 | * **gitlab: events:** push - return null if 0 commits, then gets ignored ([d92ab36](https://github.com/YappyBots/YappyGitLab/commit/d92ab36)) 324 | * **models:** fix bot not adding new channels/guilds if none in cache ([1c7fd72](https://github.com/YappyBots/YappyGitLab/commit/1c7fd72)) 325 | * **web:** fix parsing project namespace, therefore not supporting groups ([c372011](https://github.com/YappyBots/YappyGitLab/commit/c372011)) 326 | 327 | 328 | 329 | 330 | ## [1.3.3](https://github.com/YappyBots/YappyGitLab/compare/v1.3.2...v1.3.3) (2017-09-09) 331 | 332 | 333 | ### Bug Fixes 334 | 335 | * **dependencies:** update express, mongoose, jsdoc, body-parser, bugsnag, chalk, snekfetch, snyk, es ([a0b7f12](https://github.com/YappyBots/YappyGitLab/commit/a0b7f12)) 336 | * **discord: commands:** fix update - move to NPM and limit stdout to 1000 chars ([78d3df9](https://github.com/YappyBots/YappyGitLab/commit/78d3df9)) 337 | 338 | 339 | 340 | 341 | ## [1.3.2](https://github.com/YappyBots/YappyGitLab/compare/v1.3.1...v1.3.2) (2017-09-09) 342 | 343 | 344 | ### Bug Fixes 345 | 346 | * **dependencies:** update dependencies (discord.js#master) ([d86b710](https://github.com/YappyBots/YappyGitLab/commit/d86b710)) 347 | * **discord: commands:** change init to use new domain ([ab65fd3](https://github.com/YappyBots/YappyGitLab/commit/ab65fd3)) 348 | * **discord: commands:** change initorg to use new domain ([8e106a0](https://github.com/YappyBots/YappyGitLab/commit/8e106a0)) 349 | * **discord: commands:** fix clean: change to msg.channel.messages.fetch ([f5810b8](https://github.com/YappyBots/YappyGitLab/commit/f5810b8)) 350 | * **discord: modules:** change casual help to use new domain ([e593d37](https://github.com/YappyBots/YappyGitLab/commit/e593d37)) 351 | * **web:** fix login button link, removed '/' ([c3e1f1c](https://github.com/YappyBots/YappyGitLab/commit/c3e1f1c)) 352 | 353 | 354 | 355 | 356 | ## [1.3.1](https://github.com/YappyBots/YappyGitLab/compare/v1.3.0...v1.3.1) (2017-08-23) 357 | 358 | 359 | ### Bug Fixes 360 | 361 | * **web:** fix url encoded not working ([4822eb9](https://github.com/YappyBots/YappyGitLab/commit/4822eb9)) 362 | 363 | 364 | 365 | 366 | # [1.3.0](https://github.com/YappyBots/YappyGitLab/compare/v1.2.1...v1.3.0) (2017-07-21) 367 | 368 | 369 | ### Bug Fixes 370 | 371 | * **dependencies:** add yappy-bots package from github ([5abf681](https://github.com/YappyBots/YappyGitLab/commit/5abf681)) 372 | * **dependencies:** remove unused dependency from package-lock.json ([561aed0](https://github.com/YappyBots/YappyGitLab/commit/561aed0)) 373 | * **discord:** fix references to RichEmbed instead of MessageEmbed ([ec3ed4c](https://github.com/YappyBots/YappyGitLab/commit/ec3ed4c)) 374 | * **web:** allow repos in groups to work properly ([d290ada](https://github.com/YappyBots/YappyGitLab/commit/d290ada)) 375 | * **web:** fix another wrong require path ([6fe24fe](https://github.com/YappyBots/YappyGitLab/commit/6fe24fe)) 376 | * **web:** fix some wrong local dendencies paths ([94a308a](https://github.com/YappyBots/YappyGitLab/commit/94a308a)) 377 | 378 | 379 | ### Features 380 | 381 | * **web):** add basic discord oAuth ([418ebd5](https://github.com/YappyBots/YappyGitLab/commit/418ebd5)) 382 | 383 | 384 | 385 | 386 | ## [1.2.1](https://github.com/YappyBots/YappyGitLab/compare/v1.2.0...v1.2.1) (2017-07-16) 387 | 388 | 389 | ### Bug Fixes 390 | 391 | * **gitlab:** fix undef var and comma dangle ([fc0b30b](https://github.com/YappyBots/YappyGitLab/commit/fc0b30b)) 392 | 393 | 394 | 395 | 396 | # [1.2.0](https://github.com/YappyBots/YappyGitLab/compare/v1.1.2...v1.2.0) (2017-07-16) 397 | 398 | 399 | ### Bug Fixes 400 | 401 | * **discord:** fix commands not working in DMs ([8775292](https://github.com/YappyBots/YappyGitLab/commit/8775292)) 402 | * **discord:** guild-only commands error in DM and don't show in help now ([8e28ea7](https://github.com/YappyBots/YappyGitLab/commit/8e28ea7)) 403 | * **discord: commands:** fix command usage of invite ([1ea56ad](https://github.com/YappyBots/YappyGitLab/commit/1ea56ad)) 404 | 405 | 406 | ### Features 407 | 408 | * **discord: commands:** add initorg command ([8f48842](https://github.com/YappyBots/YappyGitLab/commit/8f48842)) 409 | 410 | 411 | 412 | 413 | ## [1.1.2](https://github.com/YappyBots/YappyGitLab/compare/v1.1.1...v1.1.2) (2017-07-15) 414 | 415 | 416 | ### Bug Fixes 417 | 418 | * **models: channelconfig:** fix ChannelConfig#setChannel setting map w/ undefined property ([35aebc7](https://github.com/YappyBots/YappyGitLab/commit/35aebc7)) 419 | 420 | 421 | 422 | 423 | ## [1.1.1](https://github.com/YappyBots/YappyGitLab/compare/v1.1.0...v1.1.1) (2017-07-13) 424 | 425 | 426 | ### Bug Fixes 427 | 428 | * **discord:** remove CHANNEL_CREATE from disabled events ffs ([b656754](https://github.com/YappyBots/YappyGitLab/commit/b656754)) 429 | * **gitlab: events:** fix event handler author icon_url erroring ([9c03f81](https://github.com/YappyBots/YappyGitLab/commit/9c03f81)) 430 | * **web:** fix disabledEvents looking for event name rather than shortname ([4dac4aa](https://github.com/YappyBots/YappyGitLab/commit/4dac4aa)) 431 | 432 | 433 | 434 | 435 | # [1.1.0](https://github.com/YappyBots/YappyGitLab/compare/v1.0.3...v1.1.0) (2017-05-22) 436 | 437 | 438 | ### Features 439 | 440 | * **gitlab: events:** add ability to ignore branch(es) & user(s) via conf ([6d572c1](https://github.com/YappyBots/YappyGitLab/commit/6d572c1)), closes [#11](https://github.com/YappyBots/YappyGitLab/issues/11) [#12](https://github.com/YappyBots/YappyGitLab/issues/12) 441 | 442 | 443 | 444 | 445 | ## [1.0.3](https://github.com/YappyBots/YappyGitLab/compare/v1.0.2...v1.0.3) (2017-05-17) 446 | 447 | 448 | ### Bug Fixes 449 | 450 | * **discord: commands:** conf: add disabled events config to `GL! conf` ([1254653](https://github.com/YappyBots/YappyGitLab/commit/1254653)), closes [#9](https://github.com/YappyBots/YappyGitLab/issues/9) 451 | * **discord: commands:** conf: show name of property in field view ([6441cc2](https://github.com/YappyBots/YappyGitLab/commit/6441cc2)), closes [#10](https://github.com/YappyBots/YappyGitLab/issues/10) 452 | * **discord: commands:** init: fix command to enable embed in success msg ([4f7ea84](https://github.com/YappyBots/YappyGitLab/commit/4f7ea84)), closes [#8](https://github.com/YappyBots/YappyGitLab/issues/8) 453 | 454 | 455 | 456 | 457 | ## [1.0.2](https://github.com/YappyBots/YappyGitLab/compare/v1.0.1...v1.0.2) (2017-05-15) 458 | 459 | 460 | ### Bug Fixes 461 | 462 | * **models:** hopefully fix duplicating config items on start ([e3a36b1](https://github.com/YappyBots/YappyGitLab/commit/e3a36b1)) 463 | 464 | 465 | 466 | 467 | ## [1.0.1](https://github.com/YappyBots/YappyGitLab/compare/v1.0.0...v1.0.1) (2017-05-14) 468 | 469 | 470 | ### Bug Fixes 471 | 472 | * **dependencies:** update mongoose[@4](https://github.com/4).9.9 ([610dfcb](https://github.com/YappyBots/YappyGitLab/commit/610dfcb)) 473 | * **snyk:** run `snyk wizard` and fix a few vulnerabilities ([14f7469](https://github.com/YappyBots/YappyGitLab/commit/14f7469)) 474 | * **snyk:** run `snyk wizard` once again ([e10173d](https://github.com/YappyBots/YappyGitLab/commit/e10173d)) 475 | * **web:** add error handling and log error ([b52f33d](https://github.com/YappyBots/YappyGitLab/commit/b52f33d)) 476 | * **web:** fix channels with repo handling, expected `channelId` instead of `channelID` ([5489e09](https://github.com/YappyBots/YappyGitLab/commit/5489e09)) 477 | 478 | 479 | 480 | 481 | # [1.0.0](https://github.com/YappyBots/YappyGitLab/compare/59ccd5e...v1.0.0) (2017-05-13) 482 | 483 | 484 | ### Bug Fixes 485 | 486 | * **models: channelconfig:** remove `prefix` item from .validKeys ([7870590](https://github.com/YappyBots/YappyGitLab/commit/7870590)) 487 | * package.json & .snyk to reduce vulnerabilities ([59ccd5e](https://github.com/YappyBots/YappyGitLab/commit/59ccd5e)) 488 | * **bot:** fix disconnect error log, now using event.code ([61e8e48](https://github.com/YappyBots/YappyGitLab/commit/61e8e48)) 489 | * **dependencies:** add discord.js back ([ec00054](https://github.com/YappyBots/YappyGitLab/commit/ec00054)) 490 | * **dependencies:** remove express-handlebars ([ef45337](https://github.com/YappyBots/YappyGitLab/commit/ef45337)) 491 | * **dependencies:** update dependencies ([308e38a](https://github.com/YappyBots/YappyGitLab/commit/308e38a)) 492 | * **dependencies:** update dependencies ([d79db1e](https://github.com/YappyBots/YappyGitLab/commit/d79db1e)) 493 | * **dependencies:** update dependencies ([c3ca9b9](https://github.com/YappyBots/YappyGitLab/commit/c3ca9b9)) 494 | * **dependencies:** update djs to master (v12.0.0 dev) ([65d8549](https://github.com/YappyBots/YappyGitLab/commit/65d8549)) 495 | * **dependencies:** update djs to v11.1.0 (from repo) ([35bd8e8](https://github.com/YappyBots/YappyGitLab/commit/35bd8e8)) 496 | * **discord:** st prefix to ([2638b7e](https://github.com/YappyBots/YappyGitLab/commit/2638b7e)) 497 | * **discord: commands:** actually fix stats' embed footer icon_url (was iconURL) ([6c39ac1](https://github.com/YappyBots/YappyGitLab/commit/6c39ac1)) 498 | * **discord: commands:** fix color on updating embed ([036978d](https://github.com/YappyBots/YappyGitLab/commit/036978d)) 499 | * **discord: commands:** fix conf command interpreting '-' as --global ([ed51c20](https://github.com/YappyBots/YappyGitLab/commit/ed51c20)), closes [#3](https://github.com/YappyBots/YappyGitLab/issues/3) 500 | * **discord: commands:** fix pulling update on `update` command ([034b8a5](https://github.com/YappyBots/YappyGitLab/commit/034b8a5)) 501 | * **discord: commands:** fix stats' embed iconURL, and change to use footer ([c7b6d27](https://github.com/YappyBots/YappyGitLab/commit/c7b6d27)) 502 | * **discord: modules:** fix casual help module requiring prefix + "yappy" + "gitlab", only prefix now ([a2cbc6c](https://github.com/YappyBots/YappyGitLab/commit/a2cbc6c)) 503 | * **eslint:** fix unecessary module required ([0795998](https://github.com/YappyBots/YappyGitLab/commit/0795998)) 504 | * **gitlab: events:** fix embed colors by transiforming to 0x###### ([f3c8237](https://github.com/YappyBots/YappyGitLab/commit/f3c8237)) 505 | * **help:** add quotes to usage in help for command ([dad6137](https://github.com/YappyBots/YappyGitLab/commit/dad6137)) 506 | * **models: channelconfig:** add `pipeline` to default disabledEvents in channelConfigSchema ([fc21636](https://github.com/YappyBots/YappyGitLab/commit/fc21636)) 507 | * **models: serverconfig:** fix ServerConfig#add setting Collection with .channelId prop of guild ([ac330b1](https://github.com/YappyBots/YappyGitLab/commit/ac330b1)), closes [#6](https://github.com/YappyBots/YappyGitLab/issues/6) 508 | * **mongoose:** add error handling (logging) ([0ac5f12](https://github.com/YappyBots/YappyGitLab/commit/0ac5f12)) 509 | 510 | 511 | ### Features 512 | 513 | * **commands:** add issue comamnd to search and get issue info ([82fe8ab](https://github.com/YappyBots/YappyGitLab/commit/82fe8ab)) 514 | * **conf:** add ServerConf for prefix configuration ([2d6f20e](https://github.com/YappyBots/YappyGitLab/commit/2d6f20e)) 515 | * **dependencies:** add express-handlebars & run snyk wizard ([5172c13](https://github.com/YappyBots/YappyGitLab/commit/5172c13)) 516 | * **dependencies:** add hbs ([7dd535a](https://github.com/YappyBots/YappyGitLab/commit/7dd535a)) 517 | * **discord: commands:** add `invite` command ([6307b74](https://github.com/YappyBots/YappyGitLab/commit/6307b74)) 518 | * **discord: commands:** add merge request command ([df60c39](https://github.com/YappyBots/YappyGitLab/commit/df60c39)) 519 | * **discord: commands:** add update command ([d34ab21](https://github.com/YappyBots/YappyGitLab/commit/d34ab21)), closes [#5](https://github.com/YappyBots/YappyGitLab/issues/5) 520 | * **discord: commands:** if issue description contains image, add and send with embed ([17ca650](https://github.com/YappyBots/YappyGitLab/commit/17ca650)) 521 | * **discord: commands:** if merge request description contains image, add and send with embed ([edc437e](https://github.com/YappyBots/YappyGitLab/commit/edc437e)) 522 | * **discord: commands:** send msg to channel when invite is sent to user in invite commmand ([c0466d5](https://github.com/YappyBots/YappyGitLab/commit/c0466d5)) 523 | * **gitlab: events:** add note/issue - fired when someone comments on issue ([13fcf70](https://github.com/YappyBots/YappyGitLab/commit/13fcf70)) 524 | * **gitlab: events:** add note/mergerequest - fired when someone comments on merge request ([214389e](https://github.com/YappyBots/YappyGitLab/commit/214389e)) 525 | * **gitlab: events:** add note/snippet - fired when someone comments on snippet ([692178b](https://github.com/YappyBots/YappyGitLab/commit/692178b)) 526 | * **gitlab: events:** add wiki_page/create - fired when wiki page is created ([de4dd33](https://github.com/YappyBots/YappyGitLab/commit/de4dd33)) 527 | * **gitlab: events:** add wiki_page/delete - fired when wiki page is deleted ([37908f7](https://github.com/YappyBots/YappyGitLab/commit/37908f7)) 528 | * **gitlab: events:** add wiki_page/update - fired when wiki page is updated ([34870e8](https://github.com/YappyBots/YappyGitLab/commit/34870e8)) 529 | * **middleware:** add UnhandledError middleware to handle unhandled errors ([abca414](https://github.com/YappyBots/YappyGitLab/commit/abca414)) 530 | * **modules:** add modules for message middlware ([46f2407](https://github.com/YappyBots/YappyGitLab/commit/46f2407)) 531 | * **modules:** create module CasualHelp for detecting need of help with ApiAI and responding ([5211eaa](https://github.com/YappyBots/YappyGitLab/commit/5211eaa)) 532 | * **web:** add nice-looking landing page with bot stats ([8534bab](https://github.com/YappyBots/YappyGitLab/commit/8534bab)) 533 | * **web:** add online status color circle, move screenshot img to right ([577af2c](https://github.com/YappyBots/YappyGitLab/commit/577af2c)) 534 | 535 | 536 | ### Performance Improvements 537 | 538 | * **bot:** add message sweeping & cache options to decrease mem usage ([93d1900](https://github.com/YappyBots/YappyGitLab/commit/93d1900)) 539 | * **command:** remove `async` from init command ([6dcd12c](https://github.com/YappyBots/YappyGitLab/commit/6dcd12c)) 540 | * **gitlab: emitting:** only generate event embed & text once per webhook ([4ce70dd](https://github.com/YappyBots/YappyGitLab/commit/4ce70dd)) 541 | * **gitlab: events:** update issue/* & note/commit - remove string creation on embed descriptions ([360d7b8](https://github.com/YappyBots/YappyGitLab/commit/360d7b8)) 542 | 543 | 544 | 545 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at dsevilla192@icloud.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Yappy GitLab 2 | 3 | ### Clone repo 4 | 5 | ```sh 6 | $ git clone https://github.com/YappyBots/YappyGitlab.git 7 | ``` 8 | 9 | ### Linting 10 | 11 | Please use an ESLint plugin for your editor, and use the current configuration (located in `.eslintrc`). 12 | 13 | ### GitLab Events 14 | 15 | The different [GitLab events](http://docs.gitlab.com/ce/web_hooks/web_hooks.html) each have their own name, followed by " Hook". 16 | 17 | An event may have an action. For example, the event can be an `issue` event, and the action may be `open`. 18 | The file that will be read for the styling of the event is `EVENT-ACTION.js`, everything being lowercase. 19 | If the event has a space in its actual name, like "Tag Push Hook", the corresponding file would be called `tag_push.js`, replacing the space with `_`. 20 | 21 | ### Starting the bot 22 | 23 | Yappy GitLab needs the following environment variables: 24 | 25 | The following settings are required: 26 | - `DISCORD_TOKEN` 27 | - `DB_URL` 28 | - Use `mongodb://yappy:gitlab@ds157298.mlab.com:57298/yappy_gitlab_contributors` for testing 29 | - `DISCORD_CLIENT_ID` (for the web dashboard) 30 | - `DISCORD_CLIENT_SECRET` (for the web dashboard) 31 | 32 | Yappy GitLab also needs to be run with NodeJS v8 or higher. 33 | A few examples on running the bot: 34 | 35 | ```sh 36 | $ node lib/index.js 37 | $ npm start 38 | $ nodemon # if 39 | ``` 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ####################################### 2 | # Install node dependencies 3 | ####################################### 4 | FROM node:alpine as install 5 | RUN apk update && \ 6 | apk upgrade && \ 7 | apk add --no-cache bash git openssh 8 | WORKDIR /app 9 | COPY package.json package-lock.json ./ 10 | RUN npm ci --production 11 | 12 | ####################################### 13 | # Setting up production 14 | ####################################### 15 | FROM node:alpine 16 | COPY --from=install /app /app 17 | WORKDIR /app 18 | COPY lib lib 19 | ENV PORT=8080 \ 20 | IP="0.0.0.0" \ 21 | DISABLED_COMMANDS="exec,update,reload,reboot,eval" 22 | ENV DISCORD_TOKEN \ 23 | DB_URL \ 24 | DISCORD_CLIENT_ID \ 25 | DISCORD_CLIENT_SECRET \ 26 | DISCORD_CHANNEL_LOGGING \ 27 | BDPW_KEY \ 28 | LOG_LEVEL 29 | 30 | ####################################### 31 | # Run application 32 | ####################################### 33 | USER node 34 | CMD IS_DOCKER=true node lib/ 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 David Sevilla Martin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yappy, the GitLab Monitor 2 | 3 | Monitor your GitLab repos by adding this bot to your server, set up a channel for it, and don't miss any events! 4 | 5 | [![Online Users in Yappy's Discord Server](https://discordapp.com/api/guilds/231548941492027393/embed.png)](https://discord.gg/HHqndMG) 6 | 7 | ### Info 8 | 9 | Join our Discord server at https://discord.gg/HHqndMG 10 | 11 | Invite the bot at http://bit.ly/DiscordYappyGitlab 12 | 13 | ### Commands 14 | 15 | Mention the bot to use commands. 16 | Get more updated details of these commands at https://yappy.dsev.dev/gitlab/commands. 17 | 18 | __**Util**__: 19 | - `help` - a help command... yeah :P 20 | - `invite` - how to invite the bot and set up gitlab events! 21 | - `clean` - cleans the bot's messages found in the last 100 messages 22 | - `ping` - uh... ping? pong! 23 | - `stats` - shows the stats of the bot... what else? 24 | 25 | __**GitLab**__: 26 | - `issues search [p#]` - search issues by any field in the channel repo 27 | - `issue ` - gives info about that specific issue in the channel repo 28 | - `mr list [p#]` - list merge requests by any field in the channel repo 29 | - `mr ` - gives info about that specific merge request in the channel repo 30 | 31 | 32 | __**Admin**__: 33 | - `conf [view]` - views the channel's config 34 | - `conf get ` - gets a specific config key in the channel's config 35 | - `conf set [value]` - sets the key to the value, `repo`'s value may be none to disable 36 | - `conf -g [view/set/get] [key] [value]` - view/get/set global config (using `-g`) 37 | - `init [private]` - initialize repo events on channel 38 | - `remove [repo]` - remove repo events on channel 39 | 40 | ### Developer Documentation 41 | 42 | https://yappybots.github.io/#/docs/yappygitlab/ 43 | -------------------------------------------------------------------------------- /db/migrate-from-mongo.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { default: PQueue } = require('p-queue'); 3 | const ProgressBar = require('progress'); 4 | const Guild = require('../lib/Models/Guild'); 5 | const Channel = require('../lib/Models/Channel'); 6 | 7 | require('dotenv').config(); 8 | 9 | const uniqueElementsBy = (arr, fn) => 10 | arr.reduce((acc, v) => { 11 | if (!acc.some(x => fn(v, x))) acc.push(v); 12 | return acc; 13 | }, []); 14 | const uniqueBy = prop => (a, b) => a[prop] == b[prop]; 15 | const progressBarFormat = '[:bar] :rate/s :percent :elapseds (estimated :etas)'; 16 | 17 | const channelConfig = mongoose.model('ChannelConfig', { 18 | guildName: String, 19 | guildID: String, 20 | channelName: String, 21 | channelID: String, 22 | repos: Array, 23 | repo: String, 24 | embed: Boolean, 25 | disabledEvents: { 26 | type: Array, 27 | default: ['merge_request/update', 'job'], 28 | }, 29 | ignoredUsers: Array, 30 | ignoredBranches: Array, 31 | }); 32 | 33 | const serverConfig = mongoose.model('ServerConfig', { 34 | guildName: String, 35 | guildID: String, 36 | prefix: String, 37 | }); 38 | 39 | process.on('unhandledRejection', console.error); 40 | 41 | (async () => { 42 | console.log('DB |> Connecting'); 43 | 44 | await mongoose.connect(process.env.DB_URL, { 45 | useNewUrlParser: true, 46 | }); 47 | 48 | console.log('DB |> Connected'); 49 | 50 | // === GUILDS === 51 | 52 | console.log('DB |> Guilds |> Retrieving'); 53 | 54 | const guilds = await Guild.fetchAll(); 55 | const allGuilds = await serverConfig.find({}); 56 | 57 | console.log('DB |> Guilds |> Filtering'); 58 | 59 | const guildsToMigrate = uniqueElementsBy( 60 | allGuilds.filter(g => g && g.guildID && !guilds.get(g.guildID)), 61 | uniqueBy('guildID') 62 | ); 63 | 64 | console.log(`DB |> Guilds |> Migrating (${guildsToMigrate.length})`); 65 | 66 | let progress; 67 | 68 | if (guildsToMigrate.length) { 69 | progress = new ProgressBar(`DB |> Guilds |> Migrating ${progressBarFormat}`, { total: guildsToMigrate.length, width: 20 }); 70 | } 71 | 72 | for (const guild of guildsToMigrate) { 73 | await Guild.forge({ 74 | id: guild.guildID, 75 | name: guild.guildName, 76 | prefix: guild.prefix, 77 | }).save(null, { 78 | method: 'insert', 79 | }); 80 | 81 | progress.tick(); 82 | } 83 | 84 | // === CHANNELS === 85 | 86 | console.log('DB |> Channels |> Retrieving'); 87 | 88 | const channels = await Channel.fetchAll(); 89 | const allChannels = await channelConfig.find({}); 90 | const channelsAdded = []; 91 | 92 | console.log('DB |> Channels |> Filtering'); 93 | 94 | const channelsToMigrate = allChannels.filter(ch => ch && ch.channelID && !channels.get(ch.channelID) && ch.guildID); 95 | 96 | console.log(`DB |> Channels |> Migrating (${channelsToMigrate.length})`); 97 | 98 | if (channelsToMigrate.length) { 99 | progress = new ProgressBar(`DB |> Channels |> Migrating ${progressBarFormat}`, { total: channelsToMigrate.length, width: 20 }); 100 | } 101 | 102 | for (const ch of channelsToMigrate) { 103 | if (channelsAdded.includes(ch.channelID)) continue; 104 | 105 | const channel = await Channel.forge({ 106 | id: ch.channelID, 107 | name: ch.channelName, 108 | guild_id: ch.guildID, 109 | repo: ch.repo, 110 | use_embed: !!ch.embed, 111 | events_list: JSON.stringify(ch.disabledEvents || []) || [], 112 | users_list: JSON.stringify(ch.ignoredUsers || []) || [], 113 | branches_list: JSON.stringify(ch.ignoredBranches || []) || [], 114 | }).save(null, { 115 | method: 'insert', 116 | }); 117 | 118 | if (Array.isArray(ch.repos)) 119 | await Promise.all( 120 | ch.repos.map(repo => 121 | channel.related('repos').create({ 122 | name: repo, 123 | }) 124 | ) 125 | ); 126 | 127 | channelsAdded.push(ch.channelID); 128 | progress.tick(); 129 | } 130 | 131 | console.log(); 132 | console.log(`DB |> Channels |> Migrated (${channelsAdded.length})`); 133 | 134 | if (channelsToMigrate.length) process.stdout.write('\n'); 135 | 136 | process.exit(0); 137 | })().then(() => process.exit()); 138 | -------------------------------------------------------------------------------- /db/migrations/2019_11_25_000000_create_tables.js: -------------------------------------------------------------------------------- 1 | exports.up = (knex) => { 2 | return knex.schema 3 | .createTable('guilds', (t) => { 4 | t.string('id').primary(); 5 | 6 | t.string('name').nullable(); 7 | 8 | t.string('prefix').nullable(); 9 | }) 10 | 11 | .createTable('channels', (t) => { 12 | t.string('id').primary(); 13 | 14 | t.string('name').nullable(); 15 | t.string('guild_id').nullable(); 16 | 17 | t.string('repo').nullable(); 18 | 19 | t.boolean('use_embed').defaultTo(true); 20 | 21 | t.enum('events_type', ['whitelist', 'blacklist']).defaultTo('blacklist'); 22 | t.json('events_list').defaultTo(['merge_request/update']); 23 | 24 | t.enum('users_type', ['whitelist', 'blacklist']).defaultTo('blacklist'); 25 | t.json('users_list').defaultTo([]); 26 | 27 | t.enum('branches_type', ['whitelist', 'blacklist']).defaultTo('blacklist'); 28 | t.json('branches_list').defaultTo([]); 29 | 30 | t.enum('repos_type', ['whitelist', 'blacklist']).defaultTo('blacklist'); 31 | t.json('repos_list').defaultTo([]); 32 | 33 | t.foreign('guild_id').references('guilds.id').onDelete('cascade'); 34 | }); 35 | }; 36 | 37 | exports.down = (knex) => { 38 | return knex.schema.dropTable('channels').dropTable('guilds'); 39 | }; 40 | -------------------------------------------------------------------------------- /db/migrations/2019_11_25_000001_create_channel_repos_table.js: -------------------------------------------------------------------------------- 1 | exports.up = (knex) => 2 | knex.schema.createTable('channel_repos', (t) => { 3 | t.increments('id'); 4 | 5 | t.string('channel_id'); 6 | t.string('name').index(); 7 | 8 | t.foreign('channel_id').references('channels.id').onDelete('cascade'); 9 | }); 10 | 11 | exports.down = (knex) => knex.schema.dropTable('channel_repos'); 12 | -------------------------------------------------------------------------------- /db/migrations/2019_11_25_000002_create_channel_orgs_table.js: -------------------------------------------------------------------------------- 1 | exports.up = (knex) => 2 | knex.schema.createTable('channel_orgs', (t) => { 3 | t.increments('id'); 4 | 5 | t.string('channel_id'); 6 | t.string('name').index(); 7 | 8 | t.foreign('channel_id').references('channels.id').onDelete('cascade'); 9 | }); 10 | 11 | exports.down = (knex) => knex.schema.dropTable('channel_orgs'); 12 | -------------------------------------------------------------------------------- /knexfile.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | module.exports = { 4 | client: 'sqlite3', 5 | connection: { 6 | filename: "./db/db.sqlite" 7 | }, 8 | pool: { 9 | min: 2, 10 | max: 10 11 | }, 12 | migrations: { 13 | tableName: 'migrations', 14 | directory: './db/migrations' 15 | }, 16 | useNullAsDefault: true, 17 | }; -------------------------------------------------------------------------------- /lib/Discord/Client.js: -------------------------------------------------------------------------------- 1 | const { Client: DiscordClient, Collection, PermissionsBitField, Message, ChannelType } = require('discord.js'); 2 | const fs = require('fs'); 3 | const Guild = require('../Models/Guild'); 4 | const Log = require('../Util/Log'); 5 | 6 | /** 7 | * @typedef {Discord.ClientOptions} ClientOptions 8 | * @property {String} owner discord bot's owner user id 9 | * @property {String} [name] discord bot's name 10 | */ 11 | 12 | /** 13 | * Yappy's custom Discord client 14 | * @extends {Discord.Client} 15 | */ 16 | class Client extends DiscordClient { 17 | /** 18 | * The main hub for interacting with the Discord API, and the starting point for any bot. 19 | * @param {ClientOptions} opts Custom client options 20 | */ 21 | constructor(opts = {}) { 22 | super(opts); 23 | 24 | /** 25 | * Discord bot's commands 26 | * @type {Collection} 27 | */ 28 | this.commands = new Collection(); 29 | 30 | /** 31 | * Discord bot's middleware 32 | * @type {Collection} 33 | */ 34 | this.middleware = new Collection(); 35 | 36 | /** 37 | * Discord bot's command aliases 38 | * @type {Collection} 39 | */ 40 | this.aliases = new Collection(); 41 | 42 | /** 43 | * Discord prefix is just mention. 44 | * Only for usage in commands. 45 | * @type {String} 46 | */ 47 | this.prefix = '@Yappy '; 48 | 49 | /** 50 | * Discord bot's name 51 | * @type {String} 52 | */ 53 | this.name = opts.name || 'Unknown'; 54 | 55 | this.config = { 56 | owner: opts.owner, 57 | }; 58 | } 59 | 60 | /** 61 | * Load commands from directory 62 | * @param {String} cwd path to commands directory 63 | */ 64 | loadCommands(cwd) { 65 | fs.readdir(cwd, (err, files) => { 66 | if (err) { 67 | this.emit('error', err); 68 | return this; 69 | } 70 | 71 | if (!files.length) { 72 | Log.info(`Command | No Commands Loaded.`); 73 | return this; 74 | } 75 | 76 | const disabled = (process.env.DISABLED_COMMANDS || '').split(','); 77 | 78 | files.forEach((f) => { 79 | try { 80 | let Command = require(`./Commands/${f}`); 81 | 82 | Command = new Command(this); 83 | 84 | if (disabled.includes(Command.help.name)) return Log.info(`Skipping command: ${Command.help.name}`); 85 | 86 | Log.debug(`Command | Loaded ${Command.help.name}`); 87 | Command.props.help.file = f; 88 | 89 | this.commands.set(Command.help.name, Command); 90 | 91 | Command.conf.aliases.forEach((alias) => { 92 | this.aliases.set(alias, Command.help.name); 93 | }); 94 | } catch (error) { 95 | this.emit('error', `Command | ${f}`); 96 | this.emit('error', error); 97 | } 98 | }); 99 | return this; 100 | }); 101 | } 102 | 103 | /** 104 | * Load modules from directory 105 | * @param {String} cwd path to modules directory 106 | */ 107 | loadModules(cwd) { 108 | fs.readdir(cwd, (err, files) => { 109 | if (err) { 110 | this.emit('error', err); 111 | return this; 112 | } 113 | 114 | if (!files.length) { 115 | Log.info(`Module | No Modules Loaded`); 116 | return this; 117 | } 118 | 119 | files.forEach((f) => { 120 | try { 121 | const module = new (require(`./Modules/${f}`))(this); 122 | const name = module.constructor.name.replace('Module', ''); 123 | 124 | Log.debug(`Module | Loaded ${name}`); 125 | 126 | this.middleware.set(name, module); 127 | } catch (error) { 128 | this.emit('error', `Module | ${f}`); 129 | this.emit('error', error); 130 | } 131 | }); 132 | 133 | this.middleware = this.middleware.sort((a, b) => b.priority - a.priority); 134 | 135 | return this; 136 | }); 137 | } 138 | 139 | /** 140 | * Reload command 141 | * @param {String} command command to reload 142 | * @return {Promise} 143 | */ 144 | reloadCommand(command) { 145 | return new Promise((resolve, reject) => { 146 | try { 147 | delete require.cache[require.resolve(`./Commands/${command}`)]; 148 | Log.debug(`Command | Reloading ${command}`); 149 | let cmd = require(`./Commands/${command}`); 150 | let Command = new cmd(this); 151 | Command.props.help.file = command; 152 | this.commands.set(Command.help.name, Command); 153 | Command.conf.aliases.forEach((alias) => { 154 | this.aliases.set(alias, Command.help.name); 155 | }); 156 | resolve(); 157 | } catch (e) { 158 | reject(e); 159 | } 160 | }); 161 | } 162 | 163 | /** 164 | * Reload file 165 | * @param {String} file path of file to reload 166 | * @return {Promise} 167 | */ 168 | reloadFile(file) { 169 | return new Promise((resolve, reject) => { 170 | try { 171 | delete require.cache[require.resolve(file)]; 172 | let thing = require(file); 173 | resolve(thing); 174 | } catch (e) { 175 | reject(e); 176 | } 177 | }); 178 | } 179 | 180 | run(msg) { 181 | this.execute(msg) 182 | .catch((e) => this.middleware.last().run(msg, null, null, null, e)) 183 | .catch(Log.error); 184 | } 185 | 186 | /** 187 | * Message event handling, uses modules (aka middleware) 188 | * @param {Message} msg message 189 | * @return {Client} 190 | */ 191 | async execute(msg) { 192 | if (msg.author.id === this.user.id || msg.author.bot) return; 193 | 194 | const userMention = this.user.toString(); 195 | const botMention = userMention.replace('@', '@!'); 196 | 197 | if (msg.channel.type !== ChannelType.DM && !msg.content.startsWith(userMention) && !msg.content.startsWith(botMention)) return false; 198 | 199 | const content = 200 | (msg.content.startsWith(userMention) && msg.content.replace(`${userMention} `, '')) || 201 | (msg.content.startsWith(botMention) && msg.content.replace(`${botMention} `, '')) || 202 | msg.content; 203 | const command = content.split(' ')[0].toLowerCase(); 204 | const args = content.split(' ').slice(1); 205 | 206 | const middleware = this.middleware; 207 | let i = 0; 208 | 209 | const handleErr = (err, currentMiddleware) => { 210 | // Always use the last middleware for error handling 211 | const lastMiddleware = middleware.at(middleware.size - 1); 212 | if (lastMiddleware && typeof lastMiddleware.run === 'function') { 213 | lastMiddleware.run(msg, args, null, currentMiddleware, err); 214 | } 215 | }; 216 | 217 | const next = (err) => { 218 | if (err) return handleErr(err, middleware.at(i - 1)); 219 | const nextMiddleware = middleware.at(i++); 220 | if (nextMiddleware) { 221 | try { 222 | const result = nextMiddleware.run(msg, args, next, command); 223 | if (result && typeof result.catch === 'function') { 224 | result.catch((e) => handleErr(e, nextMiddleware)); 225 | } 226 | } catch (e) { 227 | handleErr(e, nextMiddleware); 228 | } 229 | } 230 | }; 231 | 232 | next(); 233 | 234 | return this; 235 | } 236 | 237 | /** 238 | * Calculates permissions from message member 239 | * @param {Message} msg Message 240 | * @return {Number} Permission level 241 | */ 242 | permissions(msg) { 243 | /* This function should resolve to an ELEVATION level which 244 | is then sent to the command handler for verification*/ 245 | let permlvl = 0; 246 | 247 | if (msg.member && msg.member.permissions.has(PermissionsBitField.Flags.Administrator)) permlvl = 1; 248 | if (this.config.owner === msg.author.id) permlvl = 2; 249 | 250 | return permlvl; 251 | } 252 | } 253 | 254 | module.exports = Client; 255 | -------------------------------------------------------------------------------- /lib/Discord/Command.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('@YappyBots/addons').discord.structures.Command; 2 | const { EmbedBuilder } = require('discord.js'); 3 | 4 | /** 5 | * Discord bot command 6 | */ 7 | class Command extends DefaultCommand { 8 | constructor(...args) { 9 | super(...args); 10 | 11 | this._path = Log._path; 12 | this.embed = EmbedBuilder; 13 | } 14 | 15 | errorUsage(msg) { 16 | return this.commandError( 17 | msg, 18 | `Correct usage: \`@Yappy ${this.help.usage}\`\nRun \`@Yappy help ${this.help.name}\` for help and examples`, 19 | 'Incorrect Usage' 20 | ); 21 | } 22 | } 23 | 24 | module.exports = Command; 25 | -------------------------------------------------------------------------------- /lib/Discord/Commands/Announce.js: -------------------------------------------------------------------------------- 1 | const Command = require('../Command'); 2 | 3 | class AnnounceCommand extends Command { 4 | constructor(bot) { 5 | super(bot); 6 | this.props.help = { 7 | name: 'announce', 8 | description: 'announce something to all server owners', 9 | usage: 'announce ', 10 | }; 11 | this.setConf({ 12 | permLevel: 2, 13 | }); 14 | } 15 | async run(msg, args) { 16 | let announcement = args.join(' '); 17 | let owners = []; 18 | 19 | for (const guild of this.bot.guilds.cache.values()) { 20 | try { 21 | const owner = await guild.fetchOwner(); 22 | if (owner && !owners.some((o) => o.id === owner.id)) { 23 | owners.push(owner); 24 | } 25 | } catch (e) { 26 | // handle error if needed 27 | } 28 | } 29 | let messagedOwners = []; 30 | let message = await msg.channel.send({ 31 | embeds: [new this.embed().setTitle('Announce').setColor(0xfb5432).setDescription('Announcing message....').setTimestamp()], 32 | }); 33 | for (let owner of owners) { 34 | if (!owner) continue; 35 | if (messagedOwners.includes(owner.id)) continue; 36 | messagedOwners.push(owner.id); 37 | let embed = new this.embed() 38 | .setAuthor({ name: msg.author.username, iconURL: msg.author.avatarURL() }) 39 | .setColor(0xfb5432) 40 | .setTitle(`Announcement to all server owners of servers using Yappy`) 41 | .setDescription([`\u200B`, announcement, `\u200B`].join('\n')) 42 | .setTimestamp(); 43 | await owner.send({ embeds: [embed] }); 44 | } 45 | // await message.delete(); 46 | return message.edit({ 47 | embeds: [new this.embed().setTitle('Announce').setColor(0x1f9523).setDescription('Successfully announced!').setTimestamp()], 48 | }); 49 | } 50 | } 51 | 52 | module.exports = AnnounceCommand; 53 | -------------------------------------------------------------------------------- /lib/Discord/Commands/Clean.js: -------------------------------------------------------------------------------- 1 | const Command = require('../Command'); 2 | 3 | class CleanCommand extends Command { 4 | constructor(bot) { 5 | super(bot); 6 | 7 | this.props.help = { 8 | name: 'clean', 9 | description: 'clean the messages of the bot found in the number provided', 10 | usage: 'clean [number=10]', 11 | examples: ['clean', 'clean 14'], 12 | }; 13 | 14 | this.setConf({ 15 | permLevel: 1, 16 | }); 17 | } 18 | 19 | run(msg, args) { 20 | const messageCount = args[0] && !isNaN(args[0]) ? Number(args[0]) : 10; 21 | 22 | return msg.channel.messages 23 | .fetch({ 24 | limit: 50, 25 | }) 26 | .then((messages) => { 27 | let msgs = messages.filter((e) => e.author.equals(this.bot.user)); 28 | let i = 1; 29 | for (let [, message] of msgs) { 30 | if (i > messageCount) break; 31 | message.delete(); 32 | i++; 33 | } 34 | }); 35 | } 36 | } 37 | 38 | module.exports = CleanCommand; 39 | -------------------------------------------------------------------------------- /lib/Discord/Commands/Conf.js: -------------------------------------------------------------------------------- 1 | const capitalize = require('lodash/capitalize'); 2 | const Command = require('../Command'); 3 | const Channel = require('../../Models/Channel'); 4 | const Guild = require('../../Models/Guild'); 5 | const EventHandler = require('../../Gitlab/EventHandler'); 6 | 7 | const bools = ['no', 'yes']; 8 | 9 | const Actions = { 10 | INVALID: -1, 11 | DISABLE: 1, 12 | ENABLE: 2, 13 | VIEW: 3, 14 | }; 15 | 16 | class ConfCommand extends Command { 17 | constructor(bot) { 18 | super(bot); 19 | this.props.help = { 20 | name: 'conf', 21 | summary: 'configure some settings for this channel/server', 22 | description: 'Configure some settings for this channel or all current existing channels in this server', 23 | usage: 'conf [view|get|set|filter] [key] [value] ["--global"|"-g"]', 24 | examples: [ 25 | 'conf view', 26 | 'conf set repo datitisev/DiscordBot-Yappy', 27 | 'conf filter events ignore merge_request/update', 28 | 'conf filter events enable merge_request/update', 29 | 'conf filter users blacklist', 30 | 'conf filter branches allow master', 31 | ], 32 | }; 33 | this.setConf({ 34 | permLevel: 1, 35 | guildOnly: true, 36 | }); 37 | } 38 | 39 | async run(msg, args) { 40 | const action = (args.filter((e) => !e.includes('-'))[0] || 'view').toLowerCase(); 41 | 42 | const channelConf = await Channel.find(msg.channel, ['guild', 'repos']); 43 | const serverConf = channelConf.related('guild'); 44 | 45 | if (['view', 'get', 'set', 'events', 'filter'].includes(action)) { 46 | return this[`_${action}`](msg, args, channelConf, serverConf); 47 | } 48 | } 49 | 50 | _view(msg, args, channelConf, serverConf) { 51 | const { guild, channel } = msg; 52 | 53 | if (args.includes('--global') || args.includes('-g')) { 54 | let embed = new this.embed() 55 | .setColor('#FB9738') 56 | .setAuthor({ name: guild.name, iconURL: guild.iconURL() }) 57 | .setDescription(`This is your current server's configuration.`) 58 | .addFields([ 59 | { 60 | name: 'Prefix', 61 | value: serverConf.has('prefix') ? this.format(serverConf.get('prefix')) : '`GL! \u200B`', 62 | inline: true, 63 | }, 64 | ]); 65 | return msg.channel.send({ embeds: [embed] }); 66 | } 67 | 68 | let embed = new this.embed() 69 | .setColor('#FB9738') 70 | .setAuthor({ name: `${guild.name} #${channel.name}`, iconURL: guild.iconURL() }) 71 | .setDescription(`This is your current channel's configuration.`) 72 | .addFields([ 73 | { name: 'Repos', value: this.formatArrayOutput(channelConf.getRepos()) }, 74 | { name: 'Repo (repo)', value: this.format(channelConf.get('repo')), inline: true }, 75 | { name: 'Use Embed (useEmbed)', value: this.format(channelConf.get('useEmbed'), true), inline: true }, 76 | { name: `Events [${channelConf.get('eventsType')}]`, value: this.format(channelConf.get('eventsList')), inline: true }, 77 | { name: `Users [${channelConf.get('usersType')}]`, value: this.format(channelConf.get('usersList')), inline: true }, 78 | { name: `Branches [${channelConf.get('branchesType')}]`, value: this.format(channelConf.get('branchesList')), inline: true }, 79 | ]); 80 | 81 | return msg.channel.send({ embeds: [embed] }); 82 | } 83 | 84 | _get(msg, args, channelConf, serverConf) { 85 | const { guild, channel } = msg; 86 | const key = args.filter((e) => !e.includes('-'))[1]; 87 | const conf = args.includes('--global') || args.includes('-g') ? serverConf : channelConf; 88 | 89 | const embed = new this.embed() 90 | .setColor('#FB9738') 91 | .setAuthor({ name: conf === channelConf ? `${guild.name} #${channel.name}` : guild.name, iconURL: guild.iconURL() }) 92 | .setDescription(`This is your current ${conf === channelConf ? 'channel' : 'server'}'s configuration.`) 93 | .addFields([{ name: key, value: this.format(channelConf[key]) }]); 94 | 95 | return msg.channel.send({ embeds: [embed] }); 96 | } 97 | 98 | _set(msg, a, channelConf, serverConf) { 99 | const { guild, channel } = msg; 100 | const args = this.generateArgs(a); 101 | const key = args.filter((e) => !e.includes('-'))[1]; 102 | const global = (args.includes('--global') || args.includes('-g')) && args.length > 3; 103 | const conf = global ? serverConf : channelConf; 104 | const validKeys = (global ? Guild : Channel).validKeys; 105 | let value = global 106 | ? args 107 | .filter((e) => !e.includes('-g')) 108 | .slice(2) 109 | .join(' ') 110 | : args.slice(2).join(' '); 111 | if (key !== 'prefix' && value.includes(', ')) value = value.split(', '); 112 | 113 | if (key.endsWith('List') || key.endsWith('Type')) { 114 | return this.commandError('Use the `conf filter` command to modify these options'); 115 | } 116 | 117 | const embedData = { 118 | author: conf === channelConf ? `${guild.name} #${channel.name}` : guild.name, 119 | confName: conf === channelConf ? 'channel' : 'server', 120 | }; 121 | 122 | const embed = new this.embed().setAuthor({ name: embedData.author, iconURL: guild.iconURL() }); 123 | 124 | if (!validKeys.includes(key)) { 125 | return msg.channel.send({ 126 | embeds: [ 127 | embed 128 | .setColor('#CE0814') 129 | .setDescription( 130 | [ 131 | `An error occured when trying to set ${embedData.confName}'s configuration`, 132 | '', 133 | `The key \`${key}\` is invalid.`, 134 | `Valid configuration keys: \`${validKeys.join('`, `')}\``, 135 | ].join('\n') 136 | ), 137 | ], 138 | }); 139 | } 140 | 141 | return conf 142 | .set(key, value) 143 | .save() 144 | .then(() => key === 'prefix' && Guild.updateCachedPrefix(guild, value)) 145 | .then(() => 146 | msg.channel.send({ 147 | embeds: [ 148 | embed 149 | .setColor('#84F139') 150 | .setDescription(`Successfully updated ${embedData.confName}'s configuration`) 151 | .addFields([{ name: key, value: this.format(value, conf.casts && conf.casts[key] === 'boolean') }]), 152 | ], 153 | }) 154 | ) 155 | .catch((err) => 156 | msg.channel.send({ 157 | embeds: [ 158 | embed 159 | .setColor('#CE0814') 160 | .setDescription( 161 | [`An error occured when trying to update ${embedData.confName}'s configuration`, '```js', err, '```'].join('\n') 162 | ) 163 | .addFields([{ name: key, value: String(value) }]), 164 | ], 165 | }) 166 | ); 167 | } 168 | 169 | async _filter(msg, [, obj, cmd, ...args], conf) { 170 | if (obj === 'events') return this._events(...arguments); 171 | 172 | if (!['users', 'branches'].includes(obj)) { 173 | return this.commandError( 174 | msg, 175 | `Correct Usage: \`${this.bot.prefix}conf filter [users|branches|events] [whitelist|blacklist|add|remove] [item]\``, 176 | 'Incorrect usage' 177 | ); 178 | } 179 | 180 | const type = conf.get(`${obj}Type`); 181 | let list = conf.get(`${obj}List`); 182 | 183 | // set filtering to whitelist or blacklist 184 | if (/^(whitelist|blacklist)$/i.test(cmd)) { 185 | if (type === cmd) { 186 | return this.commandError(msg, `The filtering mode of ${obj} is already set to ${type}`, 'Nothing was updated'); 187 | } 188 | 189 | await conf.set(`${obj}Type`, cmd.toLowerCase()).save(); 190 | 191 | return msg.channel.send({ 192 | embeds: [ 193 | new this.embed() 194 | .setColor('#84F139') 195 | .setDescription(`Successfully updated #${msg.channel.name}'s filtering mode for ${obj} to ${cmd}`), 196 | ], 197 | }); 198 | } 199 | 200 | const action = (cmd && (/^(add|set)$/i.test(cmd) ? Actions.ENABLE : /^(remove|take)$/i.test(cmd) && Actions.DISABLE)) || Actions.INVALID; 201 | const item = args.join(' ').toLowerCase(); 202 | 203 | // if add, add to whitelist/blacklist list 204 | if (action === Actions.ENABLE && !list.includes(item)) list.push(item); 205 | else if (action === Actions.DISABLE) list = list.filter((e) => e !== item); 206 | 207 | if (!cmd || action === Actions.INVALID) { 208 | const embed = new this.embed().setTitle(`#${msg.channel.name}'s ${type}ed ${obj}`); 209 | 210 | if (!list || !list.length) embed.setDescription(`No ${type}ed ${obj} found.`); 211 | 212 | list.forEach((e) => embed.addFields([{ name: e, value: '\u200B', inline: true }])); 213 | 214 | return msg.channel.send({ embeds: [embed] }); 215 | } 216 | 217 | return conf 218 | .set(`${obj}List`, list) 219 | .save() 220 | .then(() => { 221 | const embed = new this.embed() 222 | .setColor('#84F139') 223 | .setTitle(`Successfully updated #${msg.channel.name}'s ${type}ed ${obj}`) 224 | .setDescription( 225 | [`${action === Actions.ENABLE ? 'Added' : 'Removed'} \`${item}\`.`, '', list.length ? '' : `No ${obj} are ${type}ed.`].join( 226 | '\n' 227 | ) 228 | ); 229 | 230 | list.forEach((e) => embed.addFields([{ name: e, value: '\u200B', inline: true }])); 231 | 232 | return msg.channel.send({ embeds: [embed] }); 233 | }); 234 | } 235 | 236 | _events(msg, [, , a, ...args], conf) { 237 | const action = (a && (/^(ignore|disable)$/i.test(a) ? Actions.DISABLE : /^(enable|unignore)$/i.test(a) && Actions.ENABLE)) || Actions.INVALID; 238 | const key = args.join(' ').toLowerCase(); 239 | 240 | if (action === Actions.INVALID) { 241 | if (!a) { 242 | const events = conf.get('eventsList'); 243 | const embed = new this.embed().setColor('#84F139').setTitle(`#${msg.channel.name}'s disabled events`); 244 | 245 | if (!events || !events.length) embed.setDescription('No disabled events found.'); 246 | 247 | events.forEach((e) => e && embed.addFields([{ name: e, value: '\u200B', inline: true }])); 248 | 249 | return msg.channel.send({ embeds: [embed] }); 250 | } else if (a.toLowerCase() === 'list') { 251 | const embed = new this.embed().setTitle('List of available events'); 252 | 253 | EventHandler.eventsList.forEach( 254 | (evt, name) => name !== 'Unknown' && embed.addFields([{ name: '\u200B', value: `\`${name.replace('-', '/')}\``, inline: true }]) 255 | ); 256 | 257 | return msg.channel.send({ embeds: [embed] }); 258 | } 259 | } 260 | 261 | if (action === Actions.INVALID || !key) { 262 | return this.commandError(msg, `Correct Usage: \`${this.bot.prefix}conf filter events [list|ignore|disable] [event]\``, 'Incorrect usage'); 263 | } 264 | 265 | let events = conf.get('eventsList'); 266 | 267 | if (action === Actions.DISABLE && !events.includes(key)) events.push(key); 268 | else if (action === Actions.ENABLE) events = events.filter((e) => e !== key); 269 | 270 | // remove empty events 271 | events = events.filter((e) => !!e); 272 | 273 | return conf 274 | .set('eventsList', events) 275 | .save() 276 | .then(() => { 277 | const embed = new this.embed() 278 | .setColor('#84F139') 279 | .setTitle(`Successfully updated #${msg.channel.name}'s disabled events`) 280 | .setDescription( 281 | [ 282 | `${action === Actions.ENABLE ? 'Enabled' : 'Disabled'} \`${key}\`.`, 283 | '', 284 | events.length ? 'Disabled events:\n' : 'No events are disabled.', 285 | ].join('\n') 286 | ); 287 | 288 | events.forEach((e) => embed.addFields([{ name: e, value: '\u200B', inline: true }])); 289 | 290 | return msg.channel.send({ embeds: [embed] }); 291 | }); 292 | } 293 | 294 | format(val, isBoolean) { 295 | if (isBoolean) { 296 | return capitalize(bools[val] || bools[Number(String(val) === 'true')]); 297 | } 298 | 299 | return (Array.isArray(val) && this.formatArrayOutput(val)) || (val && `\`${val}\u200B\``) || 'None'; 300 | } 301 | 302 | formatArrayOutput(arr) { 303 | return arr && arr.length ? arr.map((e) => `\`${e}\``).join(', ') : 'None'; 304 | } 305 | } 306 | 307 | module.exports = ConfCommand; 308 | -------------------------------------------------------------------------------- /lib/Discord/Commands/Data.js: -------------------------------------------------------------------------------- 1 | const Command = require('../Command'); 2 | 3 | class DataCommand extends Command { 4 | constructor(bot) { 5 | super(bot); 6 | 7 | this.props.help = { 8 | name: 'data', 9 | description: "know what data is being stored and what it's used for", 10 | usage: 'data', 11 | }; 12 | } 13 | 14 | run(msg) { 15 | const embed = new this.embed().setTitle(`${this.bot.user.username} Data`).addFields([ 16 | { 17 | name: 'Commands', 18 | value: 19 | `${this.bot.user} collects messages of valid commands that are run and unexpected errors thrown by commands, along with the guild, channel & user's name.` + 20 | '\n⮩ This data is stored in a private Discord channel for debugging & support purposes. It is not shared with outside services.', 21 | }, 22 | { 23 | name: 'Guilds & Channels', 24 | value: 25 | `When you add ${this.bot.user} to a server, it creates guild & channel configurations for them. These configurations contain the guild's or channel's ID & name.` + 26 | '\n⮩ This data is stored in a local database that no one but the owner has access to.', 27 | }, 28 | { 29 | name: 'Repositories', 30 | value: 31 | 'When you initialize a repository in a channel, the repository name and webhook URL are stored.' + 32 | '\n⮩ This data is only used for event delivery and is not shared externally.', 33 | }, 34 | ]); 35 | return msg.channel.send({ embeds: [embed] }); 36 | } 37 | } 38 | 39 | module.exports = DataCommand; 40 | -------------------------------------------------------------------------------- /lib/Discord/Commands/Eval.js: -------------------------------------------------------------------------------- 1 | const Command = require('../Command'); 2 | const util = require('util'); 3 | const path = require('path'); 4 | const Log = require('../../Util/Log'); 5 | 6 | class EvalCommand extends Command { 7 | constructor(bot) { 8 | super(bot); 9 | 10 | this.tokenRegEx = new RegExp(this.bot.token, 'g'); 11 | this.pathRegEx = new RegExp(path.resolve(__dirname, '../../../'), 'g'); 12 | 13 | this.props.help = { 14 | name: 'eval', 15 | description: 'Eval code, admin only', 16 | usage: 'eval ', 17 | }; 18 | 19 | this.setConf({ 20 | permLevel: 2, 21 | }); 22 | } 23 | 24 | run(msg, args) { 25 | let command = args.join(' '); 26 | let bot = this.bot; 27 | if (!command || command.length === 0) return; 28 | 29 | this._evalCommand(bot, msg, command, Log) 30 | .then((evaled) => { 31 | if (evaled && typeof evaled === 'string') { 32 | evaled = evaled.replace(this.tokenRegEx, '-- snip --').replace(this.pathRegEx, '.'); 33 | } 34 | 35 | let message = ['`EVAL`', '```js', evaled !== undefined ? this._clean(evaled) : 'undefined', '```'].join('\n'); 36 | 37 | return msg.channel.send(message); 38 | }) 39 | .catch((error) => { 40 | if (error.stack) error.stack = error.stack.replace(this.pathRegEx, '.'); 41 | let message = ['`EVAL`', '```js', this._clean(error) || error, '```'].join('\n'); 42 | return msg.channel.send(message); 43 | }); 44 | } 45 | _evalCommand(bot, msg, command, log) { 46 | return new Promise((resolve, reject) => { 47 | if (!log) log = Log; 48 | let code = command; 49 | try { 50 | var evaled = eval(code); 51 | if (evaled) { 52 | if (typeof evaled === 'object') { 53 | if (evaled._path) delete evaled._path; 54 | try { 55 | evaled = util.inspect(evaled, { depth: 0 }); 56 | } catch (err) { 57 | evaled = JSON.stringify(evaled, null, 2); 58 | } 59 | } 60 | } 61 | resolve(evaled); 62 | } catch (error) { 63 | reject(error); 64 | } 65 | }); 66 | } 67 | 68 | _clean(text) { 69 | if (typeof text === 'string') { 70 | return text 71 | .replace(/`/g, `\`${String.fromCharCode(8203)}`) 72 | .replace(/@/g, `@${String.fromCharCode(8203)}`) 73 | .replace('``', `\`${String.fromCharCode(8203)}\`}`); 74 | } else { 75 | return text; 76 | } 77 | } 78 | } 79 | 80 | module.exports = EvalCommand; 81 | -------------------------------------------------------------------------------- /lib/Discord/Commands/Exec.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const Command = require('../Command'); 3 | const Log = require('../../Util/Log'); 4 | 5 | class ExecCommand extends Command { 6 | constructor(bot) { 7 | super(bot); 8 | 9 | this.props.help = { 10 | name: 'exec', 11 | description: 'Execute a command in bash', 12 | usage: 'exec ', 13 | }; 14 | this.setConf({ 15 | permLevel: 2, 16 | }); 17 | } 18 | run(msg, args) { 19 | let command = args.join(' '); 20 | 21 | let runningMessage = ['`RUNNING`', '```xl', this._clean(command), '```'].join('\n'); 22 | 23 | let messageToEdit; 24 | 25 | return msg.channel 26 | .send(runningMessage) 27 | .then((message) => { 28 | messageToEdit = message; 29 | }) 30 | .then(() => this._exec(command)) 31 | .then((stdout) => { 32 | stdout = stdout.substring(0, 1500); 33 | 34 | let message = ['`STDOUT`', '```sh', this._clean(stdout) || ' ', '```'].join('\n'); 35 | 36 | messageToEdit.edit(message); 37 | }) 38 | .catch((data) => { 39 | let { stdout, stderr } = data || {}; 40 | if (stderr && stderr.stack) { 41 | Log.error(stderr); 42 | } else if (!stdout && !stderr && data) { 43 | throw data; 44 | } 45 | 46 | stderr = stderr ? stderr.substring(0, 800) : ' '; 47 | stdout = stdout ? stdout.substring(0, stderr ? stderr.length : 2046 - 40) : ' '; 48 | 49 | let message = ['`STDOUT`', '```sh', this._clean(stdout) || '', '```', '`STDERR`', '```sh', this._clean(stderr) || '', '```'] 50 | .join('\n') 51 | .substring(0, 2000); 52 | 53 | if (messageToEdit) messageToEdit.edit(message); 54 | else msg.channel.send(message); 55 | }); 56 | } 57 | _exec(cmd, opts = {}) { 58 | return new Promise((resolve, reject) => { 59 | exec(cmd, opts, (err, stdout, stderr) => { 60 | if (err) return reject({ stdout, stderr }); 61 | resolve(stdout); 62 | }); 63 | }); 64 | } 65 | _clean(text) { 66 | if (typeof text === 'string') { 67 | return text.replace('``', `\`${String.fromCharCode(8203)}\``); 68 | } else { 69 | return text; 70 | } 71 | } 72 | } 73 | 74 | module.exports = ExecCommand; 75 | -------------------------------------------------------------------------------- /lib/Discord/Commands/GitlabInit.js: -------------------------------------------------------------------------------- 1 | const Command = require('../Command'); 2 | const Channel = require('../../Models/Channel'); 3 | const Gitlab = require('../../Gitlab'); 4 | const parse = require('../../Gitlab/parser'); 5 | const punycode = require('punycode'); 6 | 7 | class GitlabInitCommand extends Command { 8 | constructor(bot) { 9 | super(bot); 10 | 11 | this.props.help = { 12 | name: 'init', 13 | summary: 'Initialize repo events on the channel.', 14 | description: 'Initialize repo events on the channel.\nInsert "private" as 2nd argument if the repo is private', 15 | usage: 'init [private]', 16 | examples: ['init gitlab-org/gitlab-ce', 'init https://gitlab.com/gitlab-org/gitlab-ce', 'init user/privaterepo private'], 17 | }; 18 | 19 | this.setConf({ 20 | permLevel: 1, 21 | aliases: ['initialize'], 22 | guildOnly: true, 23 | }); 24 | } 25 | 26 | async run(msg, args) { 27 | const repo = args[0]; 28 | const isPrivate = args[1] && args[1].toLowerCase() === 'private'; 29 | 30 | if (!repo) return this.errorUsage(msg); 31 | 32 | const repository = parse(punycode.toASCII(repo)); 33 | 34 | const repoName = repository.name; 35 | const repoUser = repository.owner; 36 | const repoFullName = repository.repo && repository.repo.toLowerCase(); 37 | if (!repository || !repoName || !repoUser) return this.errorUsage(msg); 38 | 39 | const embed = new this.embed().setTitle(`\`${repo}\`: ⚙ Working...`).setColor(0xfb9738); 40 | const workingMsg = await msg.channel.send({ embeds: [embed] }); 41 | 42 | const conf = await Channel.find(msg.channel, ['repos']); 43 | const repos = conf.getRepos(); 44 | 45 | if (!repository.isGitlab || isPrivate) { 46 | // GitlabCache.add(repository.repo);; 47 | const exists = repos.includes(repoFullName); 48 | 49 | if (exists) return this.commandError(msg, Gitlab.Constants.Errors.REPO_ALREADY_INITIALIZED(repository)); 50 | 51 | return this.addRepo(workingMsg, conf, repository.repo); 52 | } 53 | 54 | return Gitlab.getRepo(repository.repo) 55 | .then(() => { 56 | const exists = repos.includes(repoFullName); 57 | 58 | if (exists) return this.commandError(msg, Gitlab.Constants.Errors.REPO_ALREADY_INITIALIZED(repository)); 59 | 60 | return this.addRepo(workingMsg, conf, repository.repo); 61 | }) 62 | .catch((err) => { 63 | if (!err) return; 64 | 65 | const res = err.response; 66 | const body = res?.data && JSON.parse(res?.data); 67 | 68 | if (res?.statusCode == 404) 69 | return this.commandError( 70 | msg, 71 | `The repository \`${repository.repo}\` could not be found!\nIf it's private, please run \`${this.bot.prefix}init ${repository.repo} private\`.`, 72 | res.statusMessage 73 | ); 74 | else if (body) 75 | return this.commandError( 76 | msg, 77 | `Unable to get repository info for \`${repo}\`\n${(body && body.message) || ''}`, 78 | res.statusMessage 79 | ); 80 | else return this.commandError(msg, err); 81 | }); 82 | } 83 | 84 | addRepo(msg, conf, repo) { 85 | return conf.addRepo(repo.toLowerCase()).then(() => msg.edit({ embeds: [this._successMessage(repo, conf.get('useEmbed'))] })); 86 | } 87 | 88 | _successMessage(repo, hasEmbed) { 89 | const embed = new this.embed() 90 | .setColor(0x84f139) 91 | .setFooter({ text: this.bot.user.username }) 92 | .setTitle(`\`${repo}\`: Successfully initialized repository events`) 93 | .setDescription( 94 | [ 95 | 'The repository must a webhook pointing to ', 96 | '_Note that this is a \*new\* url -- make sure to update any old hooks in other repos if needed._', 97 | !hasEmbed 98 | ? 'To use embeds to have a nicer GitLab log, say `GL! conf set embed true` in this channel to enable embeds for the current channel.' 99 | : '', 100 | ].join('\n') 101 | ); 102 | return embed; 103 | } 104 | } 105 | 106 | module.exports = GitlabInitCommand; 107 | -------------------------------------------------------------------------------- /lib/Discord/Commands/GitlabInitOrg.js: -------------------------------------------------------------------------------- 1 | const Command = require('../Command'); 2 | const Channel = require('../../Models/Channel'); 3 | const Gitlab = require('../../Gitlab'); 4 | 5 | class GitlabInitOrgCommand extends Command { 6 | constructor(bot) { 7 | super(bot); 8 | 9 | this.props.help = { 10 | name: 'initorg', 11 | summary: 'Initialize all repo events from a group on the channel.', 12 | usage: 'initorg ', 13 | examples: ['initorg YappyBots', 'initorg Discord'], 14 | }; 15 | 16 | this.setConf({ 17 | permLevel: 1, 18 | aliases: ['initializeorg', 'initgroup', 'initializegroup'], 19 | guildOnly: true, 20 | }); 21 | } 22 | 23 | async run(msg, args) { 24 | const org = args[0]; 25 | const organization = /^(?:https?:\/\/)?(?:gitlab.com\/)?(\S+)$/.exec(org); 26 | 27 | if (!org || !organization || !organization[0]) return this.errorUsage(msg); 28 | 29 | const orgName = organization[0]; 30 | const workingMsg = await msg.channel.send({ 31 | embeds: [ 32 | { 33 | color: 0xfb9738, 34 | title: `Group \`${orgName}\`: ⚙ Working...`, 35 | }, 36 | ], 37 | }); 38 | 39 | const conf = await Channel.find(msg.channel); 40 | const repos = conf.getRepos(); 41 | 42 | return Gitlab.getGroupProjects(orgName) 43 | .then((res) => { 44 | const r = res.body.filter((e) => !repos.includes(e.path_with_namespace.toLowerCase())).map((e) => e.path_with_namespace); 45 | 46 | return Promise.all(r.map((repo) => conf.addRepo(repo))).then(() => r); 47 | }) 48 | .then((r) => workingMsg.edit({ embeds: [this._successMessage(orgName, r, conf.get('useEmbed'))] })) 49 | .catch((err) => { 50 | if (err.response.statusCode == 404) return this.commandError(msg, `Unable to initialize! Cannot find the \`${orgName}\` group!`); 51 | 52 | Log.error(err); 53 | 54 | return this.commandError(msg, `Unable to get group info for \`${orgName}\`\n${err.response.statusMessage}`); 55 | }); 56 | } 57 | 58 | _successMessage(org, repos, hasEmbed) { 59 | const embed = new this.embed() 60 | .setColor(0x84f139) 61 | .setFooter({ text: this.bot.user.username }) 62 | .setTitle(`\`${org}\`: Successfully initialized all public repository events`) 63 | .setDescription( 64 | [ 65 | 'The repositories must all have a webhook pointing to ', 66 | !hasEmbed ? 'To use embeds to have a nicer Gitlab log, say `G! conf set embed true` in this channel.' : '', 67 | `Initialized repos: ${repos.length ? repos.map((e) => `\`${e}\``).join(', ') : 'None'}`, 68 | ].join('\n') 69 | ); 70 | return embed; 71 | } 72 | } 73 | 74 | module.exports = GitlabInitOrgCommand; 75 | -------------------------------------------------------------------------------- /lib/Discord/Commands/GitlabIssue.js: -------------------------------------------------------------------------------- 1 | const Command = require('../Command'); 2 | const Channel = require('../../Models/Channel'); 3 | const Gitlab = require('../../Gitlab'); 4 | 5 | class GitlabIssue extends Command { 6 | constructor(bot) { 7 | super(bot); 8 | 9 | this.props.help = { 10 | name: 'issue', 11 | description: 'Search issues or get info about specific issue', 12 | usage: 'issue [query] [p(page)]', 13 | examples: ['issue 5', 'issue search error', 'issue search event p2'], 14 | }; 15 | 16 | this.setConf({ 17 | aliases: ['issues'], 18 | guildOnly: true, 19 | }); 20 | } 21 | 22 | run(msg, args) { 23 | if (!args[0]) return this.errorUsage(msg); 24 | 25 | if (args[0] === 'search' && args.length > 1) return this._search(msg, args); 26 | if (args.length === 1) return this._issue(msg, args); 27 | 28 | return this.errorUsage(msg); 29 | } 30 | 31 | async _issue(msg, args) { 32 | const issueNumber = parseInt(args[0].replace(/#/g, '')); 33 | const conf = await Channel.find(msg.channel); 34 | const repository = conf.get('repo'); 35 | 36 | if (!repository) return this.commandError(msg, Gitlab.Constants.Errors.NO_REPO_CONFIGURED(this)); 37 | if (!issueNumber) return this.errorUsage(msg); 38 | 39 | return Gitlab.getProjectIssue(repository, null, issueNumber) 40 | .then((res) => { 41 | const issue = res.body; 42 | const description = issue.description; 43 | const [, imageUrl] = /!\[(?:.*?)\]\((.*?)\)/.exec(description) || []; 44 | 45 | const embed = new this.embed() 46 | .setTitle(`Issue \`#${issue.iid}\` - ${issue.title}`) 47 | .setURL(issue.web_url) 48 | .setDescription(`${description.slice(0, 2040)}\n\u200B`) 49 | .setColor('#84F139') 50 | .addFields([ 51 | { name: 'Status', value: issue.state === 'opened' ? 'Open' : 'Closed', inline: true }, 52 | { name: 'Labels', value: issue.labels.length ? issue.labels.map((e) => `\`${e}\``).join(', ') : 'None', inline: true }, 53 | { name: 'Milestone', value: issue.milestone ? `${issue.milestone.title}` : 'None', inline: true }, 54 | { name: 'Author', value: issue.author ? `[${issue.author.name}](${issue.author.web_url})` : 'Unknown', inline: true }, 55 | { name: 'Assignee', value: issue.assignee ? `[${issue.assignee.name}](${issue.assignee.web_url})` : 'None', inline: true }, 56 | { name: 'Comments', value: String(issue.user_notes_count), inline: true }, 57 | ]) 58 | .setFooter({ text: repository, iconURL: this.bot.user.avatarURL() }); 59 | if (imageUrl) embed.setImage(imageUrl.startsWith('/') ? `https://gitlab.com/${repository}/${imageUrl}` : imageUrl); 60 | 61 | return msg.channel.send({ embeds: [embed] }); 62 | }) 63 | .catch((err) => { 64 | if (err.response?.statusCode === 404) { 65 | return this.commandError(msg, 'Issue Not Found', '404', repository); 66 | } else { 67 | return this.commandError(msg, err); 68 | } 69 | }); 70 | } 71 | 72 | async _search(msg, args) { 73 | const page = args[args.length - 1].indexOf('p') === 0 ? parseInt(args[args.length - 1].slice(1)) : 1; 74 | const query = args.slice(1).join(' ').replace(`p${page}`, ''); 75 | 76 | if (!query) return false; 77 | 78 | const conf = await Channel.find(msg.channel); 79 | const repository = conf.get('repo'); 80 | 81 | if (!repository) return this.commandError(msg, Gitlab.Constants.Errors.NO_REPO_CONFIGURED(this)); 82 | 83 | return Gitlab.getProjectIssues(repository, null, { 84 | page, 85 | per_page: 5, 86 | search: query, 87 | }).then((res) => { 88 | const totalPages = res.headers['x-total-pages']; 89 | 90 | let embed = new this.embed({ 91 | title: `Issues - query \`${query}\``, 92 | description: '\u200B', 93 | }) 94 | .setColor('#84F139') 95 | .setFooter(`${repository} ; page ${page} / ${totalPages === '0' ? 1 : totalPages}`); 96 | 97 | if (res.body && res.body.length) { 98 | res.body.forEach((issue) => { 99 | embed.description += `\n– [**\`#${issue.iid}\`**](${issue.web_url}) ${issue.title}`; 100 | }); 101 | } else { 102 | embed.setDescription('No issues found'); 103 | } 104 | 105 | msg.channel.send({ embeds: [embed] }); 106 | }); 107 | } 108 | } 109 | 110 | module.exports = GitlabIssue; 111 | -------------------------------------------------------------------------------- /lib/Discord/Commands/GitlabMergeRequest.js: -------------------------------------------------------------------------------- 1 | const Command = require('../Command'); 2 | const Channel = require('../../Models/Channel'); 3 | const Gitlab = require('../../Gitlab'); 4 | 5 | class GitlabIssue extends Command { 6 | constructor(bot) { 7 | super(bot); 8 | 9 | this.props.help = { 10 | name: 'mr', 11 | description: 'Search merge requests or get info about specific merge request', 12 | usage: 'mr [page]', 13 | examples: ['mr 5', 'mr list', 'mr list 2'], 14 | }; 15 | 16 | this.setConf({ 17 | aliases: ['mergerequest', 'merge'], 18 | guildOnly: true, 19 | }); 20 | } 21 | 22 | run(msg, args) { 23 | if (!args[0]) return this.errorUsage(msg); 24 | 25 | if (args[0] === 'list') return this._list(msg, args); 26 | if (args.length === 1) return this._mr(msg, args); 27 | 28 | return this.errorUsage(msg); 29 | } 30 | 31 | async _mr(msg, args) { 32 | const mrNumber = parseInt(args[0].replace(/!/g, '')); 33 | const conf = await Channel.find(msg.channel); 34 | const repository = conf.get('repo'); 35 | 36 | if (!repository) return this.commandError(msg, Gitlab.Constants.Errors.NO_REPO_CONFIGURED(this)); 37 | if (!mrNumber) return this.errorUsage(msg); 38 | 39 | return Gitlab.getProjectMergeRequest(repository, null, mrNumber) 40 | .then((res) => { 41 | const mr = res.body; 42 | const description = mr.description; 43 | const [, imageUrl] = /!\[(?:.*?)\]\((.*?)\)/.exec(description) || []; 44 | 45 | const embed = new this.embed() 46 | .setTitle(`Merge Request \`#${mr.iid}\` - ${mr.title}`) 47 | .setURL(mr.web_url) 48 | .setDescription(`\u200B\n${description.slice(0, 2040)}\n\u200B`) 49 | .setColor('#84F139') 50 | .addFields([ 51 | { name: 'Source', value: `${mr.author.username}:${mr.source_branch}`, inline: true }, 52 | { name: 'Target', value: `${repository.split('/')[0]}:${mr.target_branch}`, inline: true }, 53 | { name: 'State', value: mr.state === 'opened' ? 'Open' : `${mr.state[0].toUpperCase()}${mr.state.slice(1)}`, inline: true }, 54 | { name: 'Status', value: mr.work_in_progress ? 'WIP' : 'Finished', inline: true }, 55 | { name: 'Labels', value: mr.labels.length ? mr.labels.map((e) => `\`${e}\``).join(', ') : 'None', inline: true }, 56 | { name: 'Milestone', value: mr.milestone ? `${mr.milestone.title}` : 'None', inline: true }, 57 | { name: 'Comments', value: mr.user_notes_count, inline: true }, 58 | ]) 59 | .setFooter(repository, this.bot.user.avatarURL()); 60 | if (imageUrl) embed.setImage(imageUrl.startsWith('/') ? `https://gitlab.com/${repository}/${imageUrl}` : imageUrl); 61 | 62 | return msg.channel.send({ embeds: [embed] }); 63 | }) 64 | .catch((err) => { 65 | const res = err.response; 66 | 67 | if (res?.statusCode === 404) return this.commandError(msg, 'Merge Request Not Found', '404', repository); 68 | if (res?.statusCode === 403) return this.error403(msg, repository); 69 | 70 | return this.commandError(msg, err); 71 | }); 72 | } 73 | 74 | async _list(msg, args) { 75 | const page = args[1] ? parseInt(args) : 1; 76 | const conf = await Channel.find(msg.channel); 77 | const repository = conf.get('repo'); 78 | 79 | if (!repository) return this.commandError(msg, Gitlab.Constants.Errors.NO_REPO_CONFIGURED(this)); 80 | 81 | return Gitlab.getProjectMergeRequests(repository, null, { 82 | page, 83 | per_page: 5, 84 | }) 85 | .then((res) => { 86 | const totalPages = res.headers['x-total-pages']; 87 | 88 | let embed = new this.embed({ 89 | title: `Merge Requests`, 90 | description: '\u200B', 91 | }) 92 | .setColor('#84F139') 93 | .setFooter(`${repository} ; page ${page} / ${totalPages === '0' ? 1 : totalPages}`); 94 | 95 | if (res.body && res.body.length) { 96 | res.body.forEach((mr) => { 97 | embed.description += `\n– [**\`#${mr.iid}\`**](${mr.web_url}) ${mr.title}`; 98 | }); 99 | } else { 100 | embed.setDescription('No merge requests found'); 101 | } 102 | 103 | msg.channel.send({ embeds: [embed] }); 104 | }) 105 | .catch((err) => { 106 | const res = err.response; 107 | 108 | if (res.statusCode === 403) return this.error403(msg, repository); 109 | 110 | return Promise.reject(err); 111 | }); 112 | } 113 | 114 | error403(msg, r) { 115 | return this.commandError(msg, 'Merge requests are not visible to the public', '403', r); 116 | } 117 | } 118 | 119 | module.exports = GitlabIssue; 120 | -------------------------------------------------------------------------------- /lib/Discord/Commands/GitlabRemove.js: -------------------------------------------------------------------------------- 1 | const Command = require('../Command'); 2 | const Channel = require('../../Models/Channel'); 3 | const ChannelRepo = require('../../Models/ChannelRepo'); 4 | const parse = require('../../Gitlab/parser'); 5 | 6 | class GitlabRemoveCommand extends Command { 7 | constructor(bot) { 8 | super(bot); 9 | 10 | this.props.help = { 11 | name: 'remove', 12 | summary: 'Remove repo events from the channel.', 13 | usage: 'remove [repo]', 14 | examples: ['remove', 'remove private/repo'], 15 | }; 16 | 17 | this.setConf({ 18 | permLevel: 1, 19 | guildOnly: true, 20 | }); 21 | } 22 | 23 | async run(msg, args) { 24 | const conf = await Channel.find(msg.channel, ['repos']); 25 | const repos = conf && (await conf.getRepos()); 26 | const repo = args[0] ? parse(args[0].toLowerCase()) : {}; 27 | let repoFound = repo && !!repo.repo; 28 | 29 | if (!repos || !repos[0]) { 30 | return this.commandError(msg, "This channel doesn't have any GitLab events!"); 31 | } 32 | 33 | if (repos.length > 1 && repo.repo) repoFound = repos.filter((e) => e.toLowerCase() === repo.repo.toLowerCase())[0]; 34 | else if (repos.length === 1) repoFound = repos[0]; 35 | 36 | if (args[0] && !repoFound) { 37 | return this.commandError(msg, `This channel doesn't have GitLab events for **${repo.repo || args[0]}**!`); 38 | } else if (repos.length && repos.length > 1 && !repoFound) { 39 | return this.commandError(msg, `Specify what GitLab repo event to remove! Current repos: ${repos.map((e) => `**${e}**`).join(', ')}`); 40 | } 41 | 42 | const workingMsg = await msg.channel.send({ 43 | embeds: [new this.embed().setColor(0xfb9738).setTitle(`\`${repoFound}\`: ⚙ Working...`)], 44 | }); 45 | 46 | return ChannelRepo.where('channel_id', conf.id) 47 | .where('name', repoFound) 48 | .destroy() 49 | .then(() => workingMsg.edit({ embeds: [this._successMessage(repoFound)] })) 50 | .catch((err) => { 51 | Log.error(err); 52 | return this.commandError( 53 | msg, 54 | `An error occurred while trying to remove repository events for **${repoFound}** in this channel.\n\`${err}\`` 55 | ); 56 | }); 57 | } 58 | 59 | _successMessage(repo) { 60 | return new this.embed() 61 | .setColor(0x84f139) 62 | .setFooter({ text: this.bot.user.username }) 63 | .setTitle(`\`${repo}\`: Successfully removed repository events`); 64 | } 65 | } 66 | 67 | module.exports = GitlabRemoveCommand; 68 | -------------------------------------------------------------------------------- /lib/Discord/Commands/Help.js: -------------------------------------------------------------------------------- 1 | const { ChannelType } = require('discord.js'); 2 | const Command = require('../Command'); 3 | 4 | class HelpCommand extends Command { 5 | constructor(bot) { 6 | super(bot); 7 | this.props.help = { 8 | name: 'help', 9 | description: 'you all need some help', 10 | usage: 'help [command]', 11 | }; 12 | this.props.conf.aliases = ['support']; 13 | } 14 | 15 | run(msg, args) { 16 | const commandName = args[0]; 17 | 18 | if (!commandName) { 19 | const commands = this.bot.commands; 20 | let commandsForEveryone = commands.filter((e) => !e.conf.permLevel || e.conf.permLevel === 0); 21 | let commandsForAdmin = commands.filter((e) => e.conf.permLevel === 1); 22 | let commandsForOwner = commands.filter((e) => e.conf.permLevel === 2); 23 | 24 | if (msg.channel.type === ChannelType.DM) { 25 | commandsForEveryone = commandsForEveryone.filter((e) => !e.conf.guildOnly); 26 | } 27 | 28 | const embed = new this.embed() 29 | .setColor('#84F139') 30 | .setTitle(`Commands List`) 31 | .setDescription( 32 | [`Use \`${this.bot.prefix}help \` for details`, 'Or visit https://yappy.dsev.dev/gitlab/commands', '\u200B'].join('\n') 33 | ) 34 | .setFooter({ text: this.bot.user.username, iconURL: this.bot.user.avatarURL() }); 35 | 36 | embed.addFields([ 37 | { 38 | name: '__Public__', 39 | value: 40 | commandsForEveryone 41 | .map((command) => { 42 | let help = command.help; 43 | return `\`${help.name}\`: ${help.summary || help.description}`; 44 | }) 45 | .join('\n') || '\u200B', 46 | }, 47 | ]); 48 | 49 | if (msg.client.permissions(msg) > 0 && commandsForAdmin.size) { 50 | embed.addFields([ 51 | { 52 | name: '__Guild Administrator__', 53 | value: 54 | commandsForAdmin 55 | .map((command) => { 56 | let help = command.help; 57 | return `\`${help.name}\`: ${help.summary || help.description}`; 58 | }) 59 | .join('\n') || '\u200B', 60 | }, 61 | ]); 62 | } 63 | 64 | if (msg.client.permissions(msg) > 1 && commandsForOwner.size) { 65 | embed.addFields([ 66 | { 67 | name: '__Bot Owner__', 68 | value: 69 | commandsForOwner 70 | .map((command) => { 71 | let help = command.help; 72 | return `\`${help.name}\`: ${help.summary || help.description}`; 73 | }) 74 | .join('\n') || '\u200B', 75 | }, 76 | ]); 77 | } 78 | 79 | return msg.channel.send({ embeds: [embed] }); 80 | } 81 | 82 | const command = 83 | this.bot.commands.get(commandName) || 84 | (this.bot.aliases.has(commandName) ? this.bot.commands.get(this.bot.aliases.get(commandName)) : null); 85 | if (!command) return this.commandError(msg, `Command \`${commandName}\` doesn't exist`); 86 | 87 | const embed = new this.embed() 88 | .setColor('#84F139') 89 | .setTitle(`Command \`${command.help.name}\``) 90 | .setDescription(`${command.help.description || command.help.summary}\n\u200B`) 91 | .setFooter({ text: this.bot.user.username, iconURL: this.bot.user.avatarURL() }) 92 | .addFields( 93 | [ 94 | { name: 'Usage', value: `\`${this.bot.prefix}${command.help.usage}\`` }, 95 | command.conf.aliases?.length && { name: 'Aliases', value: command.conf.aliases.map((e) => `\`${e}\``).join(', ') }, 96 | command.help.examples?.length && { 97 | name: 'Examples', 98 | value: command.help.examples.map((e) => `\`${this.bot.prefix}${e}\``).join('\n'), 99 | }, 100 | { name: 'Permission', value: `${this._permLevelToWord(command.conf.permLevel)}\n\u200B`, inline: true }, 101 | { name: 'Guild Only', value: command.conf.guildOnly ? 'Yes' : 'No', inline: true }, 102 | ].filter(Boolean) 103 | ); 104 | 105 | return msg.channel.send({ embeds: [embed] }); 106 | } 107 | } 108 | 109 | module.exports = HelpCommand; 110 | -------------------------------------------------------------------------------- /lib/Discord/Commands/Invite.js: -------------------------------------------------------------------------------- 1 | const Command = require('../Command'); 2 | 3 | class InviteCommand extends Command { 4 | constructor(bot) { 5 | super(bot); 6 | 7 | this.props.help = { 8 | name: 'invite', 9 | description: 'get invite link', 10 | usage: 'invite', 11 | }; 12 | } 13 | 14 | run(msg) { 15 | const botInviteLink = `https://discordapp.com/oauth2/authorize?permissions=67193856&scope=bot&client_id=${this.bot.user.id}`; 16 | const serverInviteLink = 'http://discord.gg/HHqndMG'; 17 | 18 | const embed = new this.embed() 19 | .setTitle('Yappy, the GitLab Monitor') 20 | .setDescription(['__Invite Link__:', `**<${botInviteLink}>**`, '', '__Official Server__:', `**<${serverInviteLink}>**`].join('\n')) 21 | .setColor('#84F139') 22 | .setThumbnail(this.bot.user.avatarURL()); 23 | 24 | return msg.author.send({ embeds: [embed] }).then(() => 25 | msg.channel.send({ 26 | embeds: [new this.embed().setTitle('Yappy, the GitLab Monitor').setDescription('📬 Sent invite link!')], 27 | }) 28 | ); 29 | } 30 | } 31 | 32 | module.exports = InviteCommand; 33 | -------------------------------------------------------------------------------- /lib/Discord/Commands/Ping.js: -------------------------------------------------------------------------------- 1 | const Command = require('../Command'); 2 | 3 | class PingCommand extends Command { 4 | constructor(bot) { 5 | super(bot); 6 | this.props.help = { 7 | name: 'ping', 8 | description: 'ping, pong', 9 | usage: 'ping', 10 | }; 11 | } 12 | run(msg) { 13 | const startTime = msg.createdTimestamp; 14 | return msg.channel.send(`⏱ Pinging...`).then((message) => { 15 | const endTime = message.createdTimestamp; 16 | let difference = (endTime - startTime).toFixed(0); 17 | if (difference > 1000) difference = (difference / 1000).toFixed(0); 18 | let differenceText = endTime - startTime > 999 ? 's' : 'ms'; 19 | return message.edit( 20 | `⏱ Ping, Pong! The message round-trip took ${difference} ${differenceText}. The heartbeat ping is ${this.bot.ws.ping.toFixed(0)}ms` 21 | ); 22 | }); 23 | } 24 | } 25 | 26 | module.exports = PingCommand; 27 | -------------------------------------------------------------------------------- /lib/Discord/Commands/Reboot.js: -------------------------------------------------------------------------------- 1 | const Command = require('../Command'); 2 | 3 | class RebootCommand extends Command { 4 | constructor(bot) { 5 | super(bot); 6 | this.props.help = { 7 | name: 'reboot', 8 | description: 'reboot bot', 9 | usage: 'ping', 10 | }; 11 | this.setConf({ 12 | permLevel: 2, 13 | }); 14 | } 15 | run(msg) { 16 | return msg.channel 17 | .send({ 18 | embeds: [ 19 | { 20 | color: 0x2ecc71, 21 | title: 'Updating', 22 | description: 'Restarting...', 23 | }, 24 | ], 25 | }) 26 | .then(() => { 27 | Log.info('RESTARTING - Executed `reboot` command'); 28 | process.exit(); 29 | }); 30 | } 31 | } 32 | 33 | module.exports = RebootCommand; 34 | -------------------------------------------------------------------------------- /lib/Discord/Commands/Reload.js: -------------------------------------------------------------------------------- 1 | const Command = require('../Command'); 2 | 3 | class ReloadCommand extends Command { 4 | constructor(bot) { 5 | super(bot); 6 | this.setHelp({ 7 | name: 'reload', 8 | description: 'reloads a command, duh', 9 | usage: 'reload ', 10 | examples: ['reload stats', 'reload test'], 11 | }); 12 | this.setConf({ 13 | permLevel: 2, 14 | aliases: ['r'], 15 | }); 16 | } 17 | run(msg, args) { 18 | let argName = args[0] ? args[0].toLowerCase() : null; 19 | let bot = this.bot; 20 | let command = bot.commands.get(argName); 21 | if (argName === 'all') { 22 | return this.reloadAllCommands(msg).catch((err) => this.sendError(`all`, null, err, msg)); 23 | } else if (!command && bot.aliases.has(argName)) { 24 | command = bot.commands.get(bot.aliases.get(argName)); 25 | } else if (!argName) { 26 | return this.errorUsage(msg); 27 | } else if (!command) { 28 | return msg.channel.send(`❌ Command \`${argName}\` doesn't exist`); 29 | } 30 | let fileName = command ? command.help.file : args[0]; 31 | let cmdName = command ? command.help.name : args[0]; 32 | 33 | msg.channel.send(`⚙ Reloading Command \`${cmdName}\`...`).then((m) => { 34 | bot.reloadCommand(fileName) 35 | .then(() => { 36 | m.edit(`✅ Successfully Reloaded Command \`${cmdName}\``); 37 | }) 38 | .catch((e) => this.sendError(cmdName, m, e)); 39 | }); 40 | } 41 | sendError(t, m, e, msg) { 42 | let content = [`❌ Unable To Reload \`${t}\``, '```js', e.stack ? e.stack.replace(this._path, `.`) : e, '```']; 43 | if (m) { 44 | return m.edit(content); 45 | } else { 46 | return msg.channel.send(content); 47 | } 48 | } 49 | async reloadAllCommands(msg) { 50 | let m = await msg.channel.send(`⚙ Reloading All Commands...`); 51 | this.bot.commands.forEach((command) => { 52 | let cmdName = command.help.file || command.help.name; 53 | this.bot.reloadCommand(cmdName).catch((err) => { 54 | this.sendError(cmdName, null, err, msg); 55 | }); 56 | }); 57 | return m.edit(`✅ Successfully Reloaded All Commands`); 58 | } 59 | } 60 | 61 | module.exports = ReloadCommand; 62 | -------------------------------------------------------------------------------- /lib/Discord/Commands/Stats.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const DiscordJS = require('discord.js'); 3 | const Command = require('../Command'); 4 | const pack = require('../../../package.json'); 5 | 6 | require('moment-duration-format'); 7 | 8 | const unit = ['', 'K', 'M', 'G', 'T', 'P']; 9 | const getUptime = (bot) => moment.duration(bot.uptime).format('d[ days], h[ hours], m[ minutes, and ]s[ seconds]'); 10 | const bytesToSize = (input, precision) => { 11 | let index = Math.floor(Math.log(input) / Math.log(1024)); 12 | if (unit >= unit.length) return `${input} B`; 13 | let msg = `${(input / Math.pow(1024, index)).toFixed(precision)} ${unit[index]}B`; 14 | return msg; 15 | }; 16 | 17 | class StatsCommand extends Command { 18 | constructor(bot) { 19 | super(bot); 20 | this.props.help = { 21 | name: 'stats', 22 | description: 'Shows some stats of the bot... what else?', 23 | usage: 'stats', 24 | }; 25 | this.setConf({ 26 | aliases: ['info'], 27 | }); 28 | } 29 | run(msg) { 30 | const bot = this.bot; 31 | const MemoryUsage = bytesToSize(process.memoryUsage().heapUsed, 3); 32 | const booted = bot.booted; 33 | const channels = bot.channels?.cache; 34 | 35 | const textChannels = channels?.filter((e) => e.type !== 'voice').size ?? '??'; 36 | const voiceChannels = channels?.filter((e) => e.type === 'voice').size ?? '??'; 37 | 38 | const embed = new this.embed() 39 | .setTitle('Stats') 40 | .setDescription('Bot statistics here.') 41 | .setColor('#84F139') 42 | .addFields([ 43 | { name: '❯ Uptime', value: getUptime(bot), inline: true }, 44 | { name: '❯ Booted', value: `${booted.date} ${booted.time}`, inline: true }, 45 | { name: '❯ Memory Usage', value: MemoryUsage, inline: true }, 46 | { name: '\u200B', value: '\u200B', inline: false }, 47 | { name: '❯ Guilds', value: String(bot.guilds.cache?.size) ?? '???', inline: true }, 48 | { name: '❯ Channels', value: `${channels.size} (${textChannels} text, ${voiceChannels} voice)`, inline: true }, 49 | { name: '❯ Users', value: String(bot.users.cache?.size) ?? '???', inline: true }, 50 | { name: '\u200B', value: '\u200B', inline: false }, 51 | { name: '❯ Author', value: pack.author.replace(/<\S+[@]\S+[.]\S+>/g, ''), inline: true }, 52 | { name: '❯ Version', value: pack.version, inline: true }, 53 | { name: '❯ DiscordJS', value: `v${DiscordJS.version}`, inline: true }, 54 | ]); 55 | 56 | return msg.channel.send({ embeds: [embed] }); 57 | } 58 | } 59 | 60 | module.exports = StatsCommand; 61 | -------------------------------------------------------------------------------- /lib/Discord/Commands/Update.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const jsondiffpatch = require('jsondiffpatch'); 5 | const beforePackageJSON = require('../../../package.json'); 6 | const Command = require('../Command'); 7 | 8 | class UpdateCommand extends Command { 9 | constructor(bot) { 10 | super(bot); 11 | 12 | this.props.help = { 13 | name: 'update', 14 | description: 'update the bot', 15 | usage: 'update [commit/branch]', 16 | }; 17 | 18 | this.setConf({ 19 | permLevel: 2, 20 | }); 21 | } 22 | 23 | run(msg, args) { 24 | const commitOrBranch = args[0]; 25 | let embedData = { 26 | title: 'Updating', 27 | color: 0xfb9738, 28 | description: '\u200B', 29 | fields: [], 30 | footer: { 31 | text: this.bot.user.username, 32 | icon_url: this.bot.user.avatarURL(), 33 | }, 34 | }; 35 | let message; 36 | 37 | return msg.channel 38 | .send({ 39 | embeds: [embedData], 40 | }) 41 | .then((m) => { 42 | message = m; 43 | return this.exec('git pull'); 44 | }) 45 | .then((stdout) => { 46 | if (stdout.includes('Already up-to-date')) { 47 | return this.addFieldToEmbed(message, embedData, { 48 | name: 'Git Pull', 49 | value: 'Already up-to-date', 50 | }).then((m) => { 51 | embedData = m.embeds[0]; 52 | return Promise.reject('No update'); 53 | }); 54 | } 55 | return this.addFieldToEmbed(message, embedData, { 56 | name: 'Git Pull', 57 | value: `\`\`\`sh\n${stdout}\n\`\`\``, 58 | }); 59 | }) 60 | .then(() => { 61 | if (commitOrBranch) return this.exec(`git checkout ${commitOrBranch}`); 62 | return; 63 | }) 64 | .then(this.getDepsToInstall) 65 | .then((info) => { 66 | if (!info) return Promise.resolve(); 67 | return this.addFieldToEmbed(message, embedData, { 68 | name: 'Dependencies', 69 | value: [ 70 | ...(info.install.length ? ['**Install:**', [...info.install].map((e) => `- \`${e[0]}@${e[1]}\`\n`).join('')] : []), 71 | ...(info.update.length ? ['**Update:**', info.update.map((e) => `- \`${e[0]}@${e[1]} -> ${e[2]}\`\n`).join('')] : []), 72 | ...(info.remove.length ? ['**Remove:**', [...info.remove].map((e) => `- \`${e[0]}@${e[1]}\`\n`).join('')] : []), 73 | ].join('\n'), 74 | }).then(() => info); 75 | }) 76 | .then((info) => { 77 | if (!info) return Promise.resolve(); 78 | return this.installDeps(info).then((stdouts) => 79 | this.addFieldToEmbed(message, embedData, { 80 | name: 'NPM', 81 | value: stdouts.map((stdout) => `\`\`\`sh\n${stdout.slice(0, 1000)}\n\`\`\``).join('\n') || 'No output', 82 | }) 83 | ); 84 | }) 85 | .then(() => 86 | message.channel.send({ 87 | embeds: [ 88 | { 89 | color: 0x2ecc71, 90 | title: 'Updating', 91 | description: 'Restarting...', 92 | }, 93 | ], 94 | }) 95 | ) 96 | .then(() => { 97 | Log.info('RESTARTING - Executed `update` command'); 98 | process.exit(0); 99 | }) 100 | .catch((err) => { 101 | if (err === 'No update') return; 102 | return this.commandError(msg, err); 103 | }); 104 | } 105 | 106 | getDepsToInstall() { 107 | return new Promise((resolve, reject) => { 108 | fs.readFile(path.resolve(__dirname, '../../../package.json'), (err, content) => { 109 | if (err) return reject(err); 110 | const afterPackageJSON = JSON.parse(content); 111 | delete afterPackageJSON.dependencies.debug; 112 | const diff = jsondiffpatch.diff(beforePackageJSON.dependencies, afterPackageJSON.dependencies); 113 | if (!diff) return resolve(); 114 | let data = { 115 | install: Object.keys(diff) 116 | .filter((e) => diff[e].length === 1) 117 | .map((e) => [e, diff[e][0]]), 118 | update: Object.keys(diff) 119 | .filter((e) => diff[e].length === 2) 120 | .map((e) => [e, diff[e][0], diff[e][1]]), 121 | remove: Object.keys(diff) 122 | .filter((e) => diff[e].length === 3) 123 | .map((e) => [e, diff[e][0]]), 124 | }; 125 | resolve(data); 126 | }); 127 | }); 128 | } 129 | 130 | async installDeps(data) { 131 | let stdouts = [ 132 | data.install.length && (await this.exec(`npm i --no-progress ${data.install.map((e) => `${e[0]}@${e[1]}`).join(' ')}`)), 133 | data.update.length && (await this.exec(`npm upgrade --no-progress ${data.update.map((e) => `${e[0]}@${e[1]}`).join(' ')}`)), 134 | data.remove.length && (await this.exec(`npm rm --no-progress ${data.remove.map((e) => e[0]).join(' ')}`)), 135 | ]; 136 | return stdouts.filter((e) => !!e); 137 | } 138 | 139 | addFieldToEmbed(message, data, field) { 140 | data.fields.push(field); 141 | return message.edit({ embeds: [data] }); 142 | } 143 | 144 | exec(cmd, opts = {}) { 145 | return new Promise((resolve, reject) => { 146 | exec(cmd, opts, (err, stdout, stderr) => { 147 | if (err) return reject(stderr); 148 | resolve(stdout); 149 | }); 150 | }); 151 | } 152 | } 153 | 154 | module.exports = UpdateCommand; 155 | -------------------------------------------------------------------------------- /lib/Discord/Module.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder, Message } = require('discord.js'); 2 | 3 | /** 4 | * Discord bot middleware, or module 5 | * @property {EmbedBuilder.constructor} embed 6 | * @property {Client} bot 7 | */ 8 | class Module { 9 | /** 10 | * @param {Client} bot - discord bot 11 | */ 12 | constructor(bot) { 13 | this.bot = bot; 14 | this._path = Log._path; 15 | this.embed = EmbedBuilder; 16 | } 17 | 18 | /** 19 | * Middleware's priority 20 | * @readonly 21 | * @type {number} 22 | */ 23 | get priority() { 24 | return 0; 25 | } 26 | 27 | /** 28 | * Init module 29 | */ 30 | init() {} 31 | 32 | /** 33 | * Bot's message middleware function 34 | * @param {Message} msg - the message 35 | * @param {string[]} args - message split by spaces 36 | * @param {function} next - next middleware pls <3 37 | */ 38 | run() { 39 | throw new Error(`No middleware method was set up in module ${this.constructor.name}`); 40 | } 41 | 42 | /** 43 | * Function to shorten sending error messages 44 | * @param {Message} msg - message sent by user (for channel) 45 | * @param {string} str - error message to send user 46 | * @return {Promise} 47 | */ 48 | moduleError(msg, str) { 49 | return msg.channel.send(`❌ ${str}`); 50 | } 51 | 52 | /** 53 | * Convert normal text to an embed object 54 | * @param {string} [title = 'Auto Generated Response'] - embed title 55 | * @param {string|string[]} text - embed description, joined with newline if array 56 | * @param {color} [color = '#84F139'] - embed color 57 | * @return {MessageBuilder} 58 | */ 59 | textToEmbed(title = 'Auto Generated Response', text = null, color = '#84F139') { 60 | if (Array.isArray(text)) text = text.join('\n'); 61 | 62 | const embed = new this.embed().setColor(color).setTitle(title).setFooter({ 63 | text: this.bot.user.username, 64 | iconURL: this.bot.user.avatarURL(), 65 | }); 66 | 67 | if (text) embed.setDescription(text); 68 | 69 | return embed; 70 | } 71 | } 72 | 73 | module.exports = Module; 74 | -------------------------------------------------------------------------------- /lib/Discord/Modules/RunCommand.js: -------------------------------------------------------------------------------- 1 | const { ChannelType } = require('discord.js'); 2 | const Module = require('../Module'); 3 | const Logger = require('@YappyBots/addons').discord.logger; 4 | 5 | /** 6 | * @type {Logger} 7 | */ 8 | let logger; 9 | 10 | class RunCommandModule extends Module { 11 | constructor(bot) { 12 | super(bot); 13 | logger = new Logger(bot, 'command'); 14 | } 15 | 16 | get priority() { 17 | return 10; 18 | } 19 | 20 | /** 21 | * @inheritdoc 22 | */ 23 | run(msg, args, next, command) { 24 | const bot = this.bot; 25 | const perms = bot.permissions(msg); 26 | let cmd; 27 | 28 | if (bot.commands.has(command)) { 29 | cmd = bot.commands.get(command); 30 | } else if (bot.aliases.has(command)) { 31 | cmd = bot.commands.get(bot.aliases.get(command)); 32 | } else { 33 | return next(); 34 | } 35 | 36 | if (msg.channel.type === ChannelType.DM && cmd.conf.guildOnly) 37 | return cmd.commandError(msg, `You can only run **${cmd.help.name}** in a guild.`); 38 | 39 | const hasPermission = perms >= cmd.conf.permLevel; 40 | 41 | logger.message(msg); 42 | 43 | if (!hasPermission) 44 | return cmd.commandError(msg, `Insufficient permissions! Must be **${cmd._permLevelToWord(cmd.conf.permLevel)}** or higher`); 45 | 46 | try { 47 | let commandRun = cmd.run(msg, args); 48 | if (commandRun && commandRun.catch) { 49 | commandRun.catch((e) => { 50 | logger.error(msg, e); 51 | return cmd.commandError(msg, e); 52 | }); 53 | } 54 | } catch (e) { 55 | logger.error(msg, e); 56 | cmd.commandError(msg, e); 57 | } 58 | } 59 | } 60 | 61 | module.exports = RunCommandModule; 62 | -------------------------------------------------------------------------------- /lib/Discord/Modules/UnhandledError.js: -------------------------------------------------------------------------------- 1 | const Module = require('../Module'); 2 | 3 | class UnhandledErrorModule extends Module { 4 | run(msg, args, next, middleware, error) { 5 | if (!error) return; 6 | let embed = this.textToEmbed( 7 | `Yappy, the GitLab Monitor - Unhandled Error: \`${middleware ? middleware.constructor.name : msg.cleanContent}\``, 8 | '', 9 | '#CE0814' 10 | ); 11 | if (typeof error === 'string') embed.setDescription(error); 12 | 13 | Log.error(error); 14 | 15 | return msg.channel.send({ embeds: [embed] }); 16 | } 17 | } 18 | 19 | module.exports = UnhandledErrorModule; 20 | -------------------------------------------------------------------------------- /lib/Discord/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { GatewayIntentBits, Partials, Options } = require('discord.js'); 3 | const Client = require('./Client'); 4 | const Log = require('../Util/Log'); 5 | const bot = new Client({ 6 | name: 'Yappy, the GitLab Monitor', 7 | owner: '175008284263186437', 8 | 9 | allowedMentions: { repliedUser: true }, 10 | 11 | intents: [ 12 | GatewayIntentBits.Guilds, 13 | GatewayIntentBits.GuildMessages, 14 | // GatewayIntentBits.MessageContent, 15 | // GatewayIntentBits.GuildMembers, 16 | GatewayIntentBits.GuildMessageReactions, 17 | GatewayIntentBits.DirectMessages, 18 | // GatewayIntentBits.GuildPresences, 19 | ], 20 | partials: [Partials.Channel], 21 | makeCache: Options.cacheWithLimits({ 22 | ...Options.DefaultMakeCacheSettings, 23 | ReactionManager: 0, 24 | MessageManager: 50, 25 | GuildMemberManager: { 26 | maxSize: 100, 27 | keepOverLimit: (member) => member.id === bot.user.id, 28 | }, 29 | }), 30 | }); 31 | const logger = new (require('@YappyBots/addons').discord.logger)(bot, 'main'); 32 | const TOKEN = process.env.DISCORD_TOKEN; 33 | 34 | const initialization = require('../Models/initialization'); 35 | 36 | bot.booted = { 37 | date: new Date().toLocaleDateString(), 38 | time: new Date().toLocaleTimeString(), 39 | }; 40 | bot.statuses = ['Online', 'Connecting', 'Reconnecting', 'Idle', 'Nearly', 'Offline']; 41 | bot.statusColors = ['lightgreen', 'orange', 'orange', 'orange', 'green', 'red']; 42 | 43 | bot.on('ready', () => { 44 | Log.info('Bot | Logged In'); 45 | logger.log('Logged in', null, 'Green'); 46 | initialization(bot); 47 | }); 48 | bot.on('disconnect', (e) => { 49 | Log.warn(`Bot | Disconnected (${e.code}).`); 50 | logger.log('Disconnected', e.code, 'Orange'); 51 | process.exit(); 52 | }); 53 | bot.on('error', (e) => { 54 | Log.error(e); 55 | logger.log(e.message || 'An error occurred', e.stack || e, 'Red'); 56 | }); 57 | bot.on('warn', (e) => { 58 | Log.warn(e); 59 | logger.log(e.message || 'Warning', e.stack || e, 'Orange'); 60 | }); 61 | 62 | bot.on('messageCreate', (msg) => { 63 | try { 64 | bot.run(msg); 65 | } catch (e) { 66 | bot.emit('error', e); 67 | } 68 | }); 69 | 70 | bot.loadCommands(path.resolve(__dirname, 'Commands')); 71 | bot.loadModules(path.resolve(__dirname, 'Modules')); 72 | 73 | // === LOGIN === 74 | Log.info(`Bot | Logging in with prefix ${bot.prefix}...`); 75 | 76 | bot.login(TOKEN).catch((err) => { 77 | Log.error('Bot: Unable to log in'); 78 | Log.error(err); 79 | }); 80 | 81 | module.exports = bot; 82 | -------------------------------------------------------------------------------- /lib/Gitlab/Constants.js: -------------------------------------------------------------------------------- 1 | exports.Errors = { 2 | REQUIRE_QUERY: 'A query is required', 3 | NO_REPO_CONFIGURED: (e) => 4 | `Repository for this channel hasn't been configured. Please tell the server owner that they need to run \`${e.bot.prefix}conf set repo \`.`, 5 | REPO_ALREADY_INITIALIZED: (e) => `Repository \`${e.repo}\` is already initialized in this channel`, 6 | }; 7 | 8 | const api = `https://gitlab.com/api/v4`; 9 | 10 | exports.Endpoints = { 11 | projects: `${api}/projects`, 12 | Project: (projectID) => { 13 | if (projectID.repo) projectID = projectID.repo.replace(/\//g, '%2F'); 14 | const base = `${api}/projects/${projectID}`; 15 | return { 16 | toString: () => base, 17 | issues: `${base}/issues`, 18 | Issue: (issueID) => `${base}/issues/${issueID}`, 19 | MergeRequest: (mrID) => `${base}/merge_requests/${mrID}`, 20 | MergeRequests: (params) => `${base}/merge_requests/${params ? '?' : ''}`, 21 | }; 22 | }, 23 | groups: `${api}/projects`, 24 | Group: (group) => { 25 | const base = `${api}/groups/${group}`; 26 | return { 27 | toString: () => base, 28 | projects: `${base}/projects`, 29 | }; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /lib/Gitlab/EventHandler.js: -------------------------------------------------------------------------------- 1 | const get = require('lodash/get'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const bot = require('../Discord'); 5 | const Log = require('../Util/Log'); 6 | const parser = require('../Gitlab/parser'); 7 | 8 | class Events { 9 | constructor() { 10 | this.events = {}; 11 | this.eventDir = path.resolve(__dirname, './Events'); 12 | this.eventsList = new Map(); 13 | 14 | this.setup(); 15 | } 16 | 17 | setGitlab(gitlab) { 18 | this.gitlab = gitlab; 19 | } 20 | 21 | setup() { 22 | fs.readdir(this.eventDir, (err, files) => { 23 | if (err) throw err; 24 | 25 | files.forEach((file) => { 26 | let eventName = file.replace(`.js`, ``); 27 | try { 28 | let event = require(`./Events/${eventName}`); 29 | this.eventsList.set(eventName, new event(this.gitlab, bot)); 30 | Log.debug(`GitHub | Loaded Event ${eventName.replace(`-`, `/`)}`); 31 | } catch (e) { 32 | Log.error(`GitHub | Loaded Event ${eventName} ❌`); 33 | Log.error(e); 34 | } 35 | }); 36 | 37 | return; 38 | }); 39 | } 40 | 41 | use(data, eventName) { 42 | const action = data.object_attributes ? data.object_attributes.action : ''; 43 | const noteableType = data.object_attributes && data.object_attributes.noteable_type ? data.object_attributes.noteable_type.toLowerCase() : ''; 44 | eventName = eventName.replace(` Hook`, '').replace(/ /g, '_').toLowerCase(); 45 | let event = action || noteableType ? `${eventName}-${action || noteableType}` : eventName; 46 | try { 47 | event = this.eventsList.get(event) || this.eventsList.get('Unknown'); 48 | let text = event.text(data, eventName, action); 49 | return { 50 | embed: this.parseEmbed(event.embed(data, eventName, action), data), 51 | text: Array.isArray(text) ? text.join('\n') : text, 52 | }; 53 | } catch (e) { 54 | Log.error(e); 55 | } 56 | } 57 | 58 | parseEmbed(embed, data) { 59 | if (!embed) return null; 60 | 61 | switch (embed.color) { 62 | case 'success': 63 | embed.color = 0x3ca553; 64 | break; 65 | case 'warning': 66 | embed.color = 0xfb5432; 67 | break; 68 | case 'danger': 69 | case 'error': 70 | embed.color = 0xce0814; 71 | break; 72 | default: 73 | if (embed.color) embed.color = typeof embed.color === 'string' ? parseInt(`0x${embed.color.replace(`0x`, ``)}`, 16) : embed.color; 74 | break; 75 | } 76 | const avatar = data.user ? data.user.avatar_url : data.user_avatar; 77 | 78 | embed.author = { 79 | name: data.user ? data.user.username : data.user_username || data.user_name, 80 | icon_url: avatar && avatar.startsWith('/') ? `https://gitlab.com${avatar}` : avatar, 81 | }; 82 | embed.footer = { 83 | text: get(data, 'project.path_with_namespace') || parser.getRepo(get(data, 'repository.url')), 84 | }; 85 | embed.url = embed.url || (data.object_attributes && data.object_attributes.url) || (data.project && data.project.web_url); 86 | embed.timestamp = new Date(); 87 | 88 | if (embed.description) embed.description = embed.description.slice(0, 1000) + (embed.description.length > 1000 ? '...' : ''); 89 | 90 | return embed; 91 | } 92 | } 93 | 94 | module.exports = new Events(); 95 | -------------------------------------------------------------------------------- /lib/Gitlab/EventResponse.js: -------------------------------------------------------------------------------- 1 | class EventResponse { 2 | constructor(bot, gitlab, info) { 3 | this.gitlab = gitlab; 4 | this.bot = bot; 5 | this._info = info; 6 | } 7 | get info() { 8 | return this._info; 9 | } 10 | } 11 | 12 | module.exports = EventResponse; 13 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/Unknown.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class Unkown extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: `This response is shown whenever an event fired isn't found.`, 7 | }); 8 | } 9 | embed(data, eventName, actionName) { 10 | const action = actionName ? `/${actionName}` : ''; 11 | return { 12 | color: 'danger', 13 | title: `Repository sent unknown event: \`${eventName}${action}\``, 14 | description: `This most likely means the developers have not gotten to styling this event.\nYou may want to disable this event if you don't want it with \`GL! conf filter events disable ${eventName}${action}\``, 15 | }; 16 | } 17 | text(data, eventName, actionName) { 18 | const action = actionName ? `/${actionName}` : ''; 19 | return [ 20 | `🛑 An unknown event has been emitted.`, 21 | 'This most likely means the developers have not gotten to styling this event.', 22 | `The event in question was \`${eventName}${action}\``, 23 | ].join('\n'); 24 | } 25 | } 26 | 27 | module.exports = Unkown; 28 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/issue-close.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class IssueClose extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event gets fired when an issue is closed', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const issue = data.object_attributes; 12 | return { 13 | color: 0xe9642d, 14 | title: `Closed issue #${issue.iid}: \`${issue.title}\``, 15 | description: issue.description, 16 | }; 17 | } 18 | 19 | text(data) { 20 | const actor = data.user.name; 21 | const issue = data.object_attributes; 22 | return [`🛠 **${actor}** closed issue **#${issue.iid}** _${issue.title}_`, `<${issue.url}>`].join('\n'); 23 | } 24 | } 25 | 26 | module.exports = IssueClose; 27 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/issue-open.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class IssueOpen extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event gets fired when a new issue is created', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | let issue = data.object_attributes; 12 | return { 13 | color: 0xe9642d, 14 | title: `Opened issue #${issue.iid}: \`${issue.title}\``, 15 | description: issue.description, 16 | }; 17 | } 18 | 19 | text(data) { 20 | const actor = data.user.name; 21 | const issue = data.object_attributes; 22 | return [`🛠 **${actor}** opened issue **#${issue.iid}**`, ` ${issue.title}`, `<${issue.url}>`].join('\n'); 23 | } 24 | } 25 | 26 | module.exports = IssueOpen; 27 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/issue-reopen.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class IssueReopen extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event gets fired when an issue is reopened', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const issue = data.object_attributes; 12 | return { 13 | color: 0xe9642d, 14 | title: `Reopened issue #${issue.iid} \`${issue.title}\``, 15 | description: issue.description, 16 | }; 17 | } 18 | 19 | text(data) { 20 | const actor = data.user.name; 21 | const issue = data.object_attributes; 22 | return [`🛠 **${actor}** reopened issue **#${issue.iid}** _${issue.title}_`, `<${issue.url}>`].join('\n'); 23 | } 24 | } 25 | 26 | module.exports = IssueReopen; 27 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/issue-update.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class IssueUpdate extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event gets fired when an issue is updated', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const issue = data.object_attributes; 12 | return { 13 | color: 0xe9642d, 14 | title: `Updated issue #${issue.iid} \`${issue.title}\``, 15 | description: issue.description, 16 | }; 17 | } 18 | 19 | text(data) { 20 | const actor = data.user.name; 21 | const issue = data.object_attributes; 22 | return [`🛠 **${actor}** updated issue **#${issue.iid}** _${issue.title}_`, `<${issue.url}>`].join('\n'); 23 | } 24 | } 25 | 26 | module.exports = IssueUpdate; 27 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/issue.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class Issue extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event gets fired when a test issue event is executed', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const issue = data.object_attributes; 12 | 13 | return { 14 | color: 0xfffc00, 15 | title: `Issue #${issue.iid}: ${issue.title}`, 16 | description: 'A test issue event was fired from the GitLab integrations page', 17 | fields: [ 18 | { 19 | name: 'Description', 20 | value: `${issue.description.split('\n').slice(0, 4).join('\n').slice(0, 2000)}\n\u200B`, 21 | }, 22 | { 23 | name: 'State', 24 | value: issue.state[0].toUpperCase() + issue.state.slice(1), 25 | inline: true, 26 | }, 27 | { 28 | name: 'Created At', 29 | value: new Date(issue.created_at).toGMTString(), 30 | inline: true, 31 | }, 32 | { 33 | name: 'Labels', 34 | value: data.labels && data.labels.length ? data.labels.map((l) => `\`${l.title}\``).join(', ') : 'None', 35 | }, 36 | ], 37 | }; 38 | } 39 | 40 | text(data) { 41 | const { user: actor, object_attributes: issue } = data; 42 | 43 | return [`🤦‍♂️ **${actor.name}** tested an issue event, and issue **#${issue.iid}** was sent.`, `<${issue.url}>`].join('\n'); 44 | } 45 | } 46 | 47 | module.exports = Issue; 48 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/merge_request-approved.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class MergeRequestApproved extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event gets fired when a merge request is approved', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const mergeRequest = data.object_attributes; 12 | return { 13 | color: 0x149617, 14 | title: `Approved merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``, 15 | }; 16 | } 17 | 18 | text(data) { 19 | const actor = data.user.name; 20 | const issue = data.object_attributes; 21 | return [`✔️ **${actor}** approved merge request **#${issue.iid}** _${issue.title}_`, `<${issue.url}>`].join('\n'); 22 | } 23 | } 24 | 25 | module.exports = MergeRequestApproved; 26 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/merge_request-close.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class MergeRequestClose extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event gets fired when a new merge request is closed', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const mergeRequest = data.object_attributes; 12 | return { 13 | color: 0x149617, 14 | title: `Closed merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``, 15 | description: `${mergeRequest.description}\n\u200B`, 16 | fields: [ 17 | { 18 | name: 'Source', 19 | value: `${mergeRequest.source.name}/${mergeRequest.source_branch}`, 20 | inline: true, 21 | }, 22 | { 23 | name: 'Target', 24 | value: `${mergeRequest.target.name}/${mergeRequest.target_branch}`, 25 | inline: true, 26 | }, 27 | { 28 | name: 'Status', 29 | value: mergeRequest.work_in_progress ? 'WIP' : 'Finished', 30 | inline: true, 31 | }, 32 | ], 33 | }; 34 | } 35 | 36 | text(data) { 37 | const actor = data.user.name; 38 | const mergeRequest = data.object_attributes; 39 | return [`🛠 **${actor}** closed merge request **#${mergeRequest.iid}**`, ` ${mergeRequest.title}`, `<${mergeRequest.url}>`].join( 40 | '\n' 41 | ); 42 | } 43 | } 44 | 45 | module.exports = MergeRequestClose; 46 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/merge_request-merge.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class MergeRequestMerge extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event gets fired when a new merge request is merged', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const mergeRequest = data.object_attributes; 12 | const mergedCommitSha = mergeRequest.merge_commit_sha; 13 | return { 14 | color: 0x149617, 15 | title: `Merged merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``, 16 | description: `${mergeRequest.description}\n\u200B`, 17 | fields: [ 18 | { 19 | name: 'Source', 20 | value: `${mergeRequest.source.name}/${mergeRequest.source_branch}`, 21 | inline: true, 22 | }, 23 | { 24 | name: 'Target', 25 | value: `${mergeRequest.target.name}/${mergeRequest.target_branch}`, 26 | inline: true, 27 | }, 28 | { 29 | name: 'Merge Commit', 30 | value: mergedCommitSha ? `\`${mergedCommitSha.slice(0, 7)}\`` : '???', 31 | inline: true, 32 | }, 33 | ], 34 | }; 35 | } 36 | 37 | text(data) { 38 | const actor = data.user.name; 39 | const mergeRequest = data.object_attributes; 40 | return [`🛠 **${actor}** merged merge request **#${mergeRequest.iid}**`, ` ${mergeRequest.title}`, `<${mergeRequest.url}>`].join( 41 | '\n' 42 | ); 43 | } 44 | } 45 | 46 | module.exports = MergeRequestMerge; 47 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/merge_request-open.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | const UrlRegEx = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g; 3 | const RemoveUrlEmbedding = (url) => `<${url}>`; 4 | 5 | class MergeRequestOpen extends EventResponse { 6 | constructor(...args) { 7 | super(...args, { 8 | description: 'This event gets fired when a new merge request is opened', 9 | }); 10 | } 11 | 12 | embed(data) { 13 | const mergeRequest = data.object_attributes; 14 | const lastCommit = mergeRequest.last_commit; 15 | const lastCommitMessage = lastCommit.message.split('\n')[0].replace(UrlRegEx, RemoveUrlEmbedding); 16 | const lastCommitAuthor = lastCommit.author.name || data.user_username || data.user_name; 17 | return { 18 | color: 0x149617, 19 | title: `Opened merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``, 20 | description: `${mergeRequest.description}\n\u200B`, 21 | fields: [ 22 | { 23 | name: 'Source', 24 | value: `${mergeRequest.source.name}/${mergeRequest.source_branch}`, 25 | inline: true, 26 | }, 27 | { 28 | name: 'Target', 29 | value: `${mergeRequest.target.name}/${mergeRequest.target_branch}`, 30 | inline: true, 31 | }, 32 | { 33 | name: 'Status', 34 | value: mergeRequest.work_in_progress ? 'WIP' : 'Finished', 35 | inline: true, 36 | }, 37 | { 38 | name: 'Latest Commit', 39 | value: `[\`${lastCommit.id.slice(0, 7)}\`](${lastCommit.url}) ${lastCommitMessage} [${lastCommitAuthor}]`, 40 | }, 41 | ], 42 | }; 43 | } 44 | 45 | text(data) { 46 | const actor = data.user.name; 47 | const mergeRequest = data.object_attributes; 48 | return [`🛠 **${actor}** opened merge request **#${mergeRequest.iid}**`, ` ${mergeRequest.title}`, `<${mergeRequest.url}>`].join( 49 | '\n' 50 | ); 51 | } 52 | } 53 | 54 | module.exports = MergeRequestOpen; 55 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/merge_request-reopen.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | const UrlRegEx = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g; 3 | const RemoveUrlEmbedding = (url) => `<${url}>`; 4 | 5 | class MergeRequestReopen extends EventResponse { 6 | constructor(...args) { 7 | super(...args, { 8 | description: 'This event gets fired when a new merge request is reopened', 9 | }); 10 | } 11 | 12 | embed(data) { 13 | const mergeRequest = data.object_attributes; 14 | const lastCommit = mergeRequest.last_commit; 15 | const lastCommitMessage = lastCommit.message.split('\n')[0].replace(UrlRegEx, RemoveUrlEmbedding); 16 | const lastCommitAuthor = lastCommit.author.name || data.user_username || data.user_name; 17 | return { 18 | color: 0x149617, 19 | title: `Repened merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``, 20 | description: `${mergeRequest.description}\n\u200B`, 21 | fields: [ 22 | { 23 | name: 'Source', 24 | value: `${mergeRequest.source.name}/${mergeRequest.source_branch}`, 25 | inline: true, 26 | }, 27 | { 28 | name: 'Target', 29 | value: `${mergeRequest.target.name}/${mergeRequest.target_branch}`, 30 | inline: true, 31 | }, 32 | { 33 | name: 'Status', 34 | value: mergeRequest.work_in_progress ? 'WIP' : 'Finished', 35 | inline: true, 36 | }, 37 | { 38 | name: 'Latest Commit', 39 | value: `[\`${lastCommit.id.slice(0, 7)}\`](${lastCommit.url}) ${lastCommitMessage} [${lastCommitAuthor}]`, 40 | }, 41 | ], 42 | }; 43 | } 44 | 45 | text(data) { 46 | const actor = data.user.name; 47 | const mergeRequest = data.object_attributes; 48 | return [`🛠 **${actor}** updated merge request **#${mergeRequest.iid}**`, ` ${mergeRequest.title}`, `<${mergeRequest.url}>`].join( 49 | '\n' 50 | ); 51 | } 52 | } 53 | 54 | module.exports = MergeRequestReopen; 55 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/merge_request-unapproved.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class MergeRequestUnapproved extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event gets fired when a merge request is unapproved', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const mergeRequest = data.object_attributes; 12 | return { 13 | color: 0x149617, 14 | title: `Unapproved merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``, 15 | }; 16 | } 17 | 18 | text(data) { 19 | const actor = data.user.name; 20 | const issue = data.object_attributes; 21 | return [`❌ **${actor}** unapproved merge request **#${issue.iid}** _${issue.title}_`, `<${issue.url}>`].join('\n'); 22 | } 23 | } 24 | 25 | module.exports = MergeRequestUnapproved; 26 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/merge_request-update.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | const UrlRegEx = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g; 3 | const RemoveUrlEmbedding = (url) => `<${url}>`; 4 | 5 | class MergeRequestUpdate extends EventResponse { 6 | constructor(...args) { 7 | super(...args, { 8 | description: 'This event gets fired when a merge request is updated', 9 | }); 10 | } 11 | 12 | embed(data) { 13 | const mergeRequest = data.object_attributes; 14 | const lastCommit = mergeRequest.last_commit; 15 | const lastCommitMessage = lastCommit.message.split('\n')[0].replace(UrlRegEx, RemoveUrlEmbedding); 16 | const lastCommitAuthor = lastCommit.author.name || data.user_username || data.user_name; 17 | return { 18 | color: 0x149617, 19 | title: `updated merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``, 20 | description: `${mergeRequest.description}\n\u200B`, 21 | fields: [ 22 | { 23 | name: 'Source', 24 | value: `${mergeRequest.source.name}/${mergeRequest.source_branch}`, 25 | inline: true, 26 | }, 27 | { 28 | name: 'Target', 29 | value: `${mergeRequest.target.name}/${mergeRequest.target_branch}`, 30 | inline: true, 31 | }, 32 | { 33 | name: 'Status', 34 | value: mergeRequest.work_in_progress ? 'WIP' : 'Finished', 35 | inline: true, 36 | }, 37 | { 38 | name: 'Latest Commit', 39 | value: `[\`${lastCommit.id.slice(0, 7)}\`](${lastCommit.url}) ${lastCommitMessage} [${lastCommitAuthor}]`, 40 | }, 41 | ], 42 | }; 43 | } 44 | 45 | text(data) { 46 | const actor = data.user.name; 47 | const issue = data.object_attributes; 48 | return [`🛠 **${actor}** updated issue **#${issue.iid}** _${issue.title}_`, `<${issue.url}>`].join('\n'); 49 | } 50 | } 51 | 52 | module.exports = MergeRequestUpdate; 53 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/note-commit.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class NoteCommit extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event gets fired when someone comments on a commit', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const comment = data.object_attributes; 12 | const sha = comment.commit_id.slice(0, 7); 13 | return { 14 | color: 0x996633, 15 | title: `Commented on commit \`${sha}\``, 16 | description: comment.note, 17 | }; 18 | } 19 | 20 | text(data) { 21 | const actor = data.user.name; 22 | const comment = data.object_attributes; 23 | const sha = comment.commit_id.slice(0, 7); 24 | return [`**${actor}** commented on commit \`${sha}\``, ` ${comment.note.slice(0, 100).replace(/\n/g, '\n ')}`, `${comment.url}`].join( 25 | '\n' 26 | ); 27 | } 28 | } 29 | 30 | module.exports = NoteCommit; 31 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/note-issue.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class NoteIssue extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event gets fired when someone comments on an issue', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const comment = data.object_attributes; 12 | const issue = data.issue; 13 | return { 14 | color: 0x996633, 15 | title: `Commented on issue #${issue.iid}: \`${issue.title}\``, 16 | description: comment.note, 17 | }; 18 | } 19 | 20 | text(data) { 21 | const actor = data.user.name; 22 | const comment = data.object_attributes; 23 | const issue = data.issue; 24 | return [ 25 | `**${actor}** commented on issue **#${issue.iid}** _${issue.title}_`, 26 | ` ${comment.note.slice(0, 100).replace(/\n/g, '\n ')}`, 27 | `${comment.url}`, 28 | ].join('\n'); 29 | } 30 | } 31 | 32 | module.exports = NoteIssue; 33 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/note-mergerequest.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class NoteMergeRequest extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event gets fired when someone comments on a merge request', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const comment = data.object_attributes; 12 | const mergeRequest = data.merge_request; 13 | return { 14 | color: 0x996633, 15 | title: `Commented on merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``, 16 | description: comment.note, 17 | }; 18 | } 19 | 20 | text(data) { 21 | const actor = data.user.name; 22 | const comment = data.object_attributes; 23 | const mergeRequest = data.merge_request; 24 | return [ 25 | `**${actor}** commented on merge request **#${mergeRequest.iid}** _${mergeRequest.title}_`, 26 | ` ${comment.note.slice(0, 100).replace(/\n/g, '\n ')}`, 27 | `${comment.url}`, 28 | ].join('\n'); 29 | } 30 | } 31 | 32 | module.exports = NoteMergeRequest; 33 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/note-snippet.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class NoteSnippet extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event gets fired when someone comments on a code snippet', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const comment = data.object_attributes; 12 | const snippet = data.snippet; 13 | return { 14 | color: `#996633`, 15 | title: `Commented on snippet \`${snippet.title}\``, 16 | description: comment.note, 17 | }; 18 | } 19 | 20 | text(data) { 21 | const actor = data.user.name; 22 | const comment = data.object_attributes; 23 | const snippet = data.snippet; 24 | return [ 25 | `**${actor}** commented on snippet **#${snippet.title}**`, 26 | ` ${comment.note.slice(0, 100).replace(/\n/g, '\n ')}`, 27 | `${comment.url}`, 28 | ].join('\n'); 29 | } 30 | } 31 | 32 | module.exports = NoteSnippet; 33 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/pipeline.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class Pipeline extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: `This event is fired when a pipeline job finishes.`, 7 | }); 8 | } 9 | embed(data) { 10 | if (data.object_attributes.status !== 'success' && data.object_attributes.status !== 'failed') return; 11 | 12 | const branch = data.object_attributes.ref; 13 | const id = data.object_attributes.id; 14 | const result = data.object_attributes.status === 'success' ? 'passed' : 'failed'; 15 | const color = result === 'passed' ? 'success' : 'error'; 16 | const duration = data.object_attributes.duration; 17 | 18 | const url = `${data.project.web_url}/pipelines/${id}`; 19 | const description = `Pipeline [\`#${id}\`](${url}) of \`${branch}\` branch **${result}** in ${duration} seconds.`; 20 | 21 | return { 22 | color, 23 | description, 24 | }; 25 | } 26 | text(data) { 27 | if (data.object_attributes.status !== 'success' && data.object_attributes.status !== 'failed') return; 28 | 29 | const actor = data.user.username; 30 | const branch = data.object_attributes.ref; 31 | const id = data.object_attributes.id; 32 | const result = data.object_attributes.status === 'success' ? 'passed' : 'failed'; 33 | const icon = result === 'passed' ? ':white_check_mark:' : ':no_entry:'; 34 | const duration = data.object_attributes.duration; 35 | 36 | const msg = 37 | `Pipeline \`#${id}\` of \`${branch}\` branch created by *${actor}* **${result}** in ${duration} seconds. ${icon}\n` + 38 | `<${data.project.web_url}/pipelines/${id}>`; 39 | return msg; 40 | } 41 | } 42 | 43 | module.exports = Pipeline; 44 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/push.js: -------------------------------------------------------------------------------- 1 | const GetBranchName = require('../../Util').GetBranchName; 2 | const EventResponse = require('../EventResponse'); 3 | 4 | const UrlRegEx = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g; 5 | const RemoveUrlEmbedding = (url) => `<${url}>`; 6 | 7 | class Push extends EventResponse { 8 | constructor(...args) { 9 | super(...args, { 10 | description: `This event is fired when someone pushes commits to a branch.`, 11 | }); 12 | } 13 | embed(data) { 14 | if (!data.commits || !data.commits.length) return; 15 | 16 | const branch = GetBranchName(data.ref); 17 | const commits = data.commits; 18 | const commitCount = data.total_commits_count; 19 | let pretext = commits 20 | .map((commit) => { 21 | const message = (commitCount === 1 ? commit.message : commit.message.split('\n')[0]).replace(UrlRegEx, RemoveUrlEmbedding); 22 | const author = commit.author.name || data.user_username || data.user_name; 23 | const sha = commit.id.slice(0, 7); 24 | 25 | return `[\`${sha}\`](${commit.url}) ${message} [${author}]`; 26 | }) 27 | .slice(0, 5); 28 | const description = pretext.join('\n'); 29 | 30 | return { 31 | color: 0x7289da, 32 | title: `Pushed ${commitCount} ${commitCount !== 1 ? 'commits' : 'commit'} to \`${branch}\``, 33 | url: `${data.project.web_url}/compare/${data.before.slice(0, 7)}...${data.after.slice(0, 7)}`, 34 | description, 35 | }; 36 | } 37 | text(data) { 38 | if (!data.commits || !data.commits.length) return; 39 | 40 | const actor = data.user_username || data.user_name; 41 | const branch = GetBranchName(data.ref); 42 | const commits = data.commits || []; 43 | const commitCount = data.total_commits_count; 44 | 45 | if (!commitCount) return ''; 46 | 47 | let commitArr = commits 48 | .map((commit) => { 49 | let commitMessage = commit.message.replace(/\n/g, '\n ').replace(UrlRegEx, RemoveUrlEmbedding); 50 | return ` \`${commit.id.slice(0, 7)}\` ${commitMessage} [${commit.author.name || actor}]`; 51 | }) 52 | .slice(0, 5); 53 | 54 | return [ 55 | `⚡ **${actor}** pushed ${commitCount} ${commitCount !== 1 ? 'commits' : 'commit'} to \`${branch}\``, 56 | ...commitArr, 57 | `${data.project.web_url}/compare/${data.before.slice(0, 7)}...${data.after.slice(0, 7)}`, 58 | ].join('\n'); 59 | 60 | return msg; 61 | } 62 | } 63 | 64 | module.exports = Push; 65 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/tag_push.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class TagPush extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: `This event is fired when a tag is created/deleted`, 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const tag = data.ref ? data.ref.split('/')[2] : 'unknown'; 12 | const sha = data.checkout_sha ? data.checkout_sha.slice(0, 7) : null; 13 | const message = data.messsage || ''; 14 | const commitCount = data.total_commits_count; 15 | const isCreated = this.isCreated(data); 16 | 17 | return { 18 | color: 0xf0c330, 19 | title: `${this.isCreated(data) ? 'Created' : 'Deleted'} tag \`${tag}\` ${sha ? `from commit \`${sha}\`` : ''}${ 20 | isCreated ? ` with ${commitCount} ${commitCount !== 1 ? 'commits' : 'commit'}` : '' 21 | }`, 22 | url: isCreated ? `${data.project.web_url}/tags/${tag}` : null, 23 | description: message, 24 | }; 25 | } 26 | 27 | text(data) { 28 | const actor = data.user_username || data.user_name; 29 | const tag = data.ref ? data.ref.split('/')[2] : 'unknown'; 30 | const sha = data.checkout_sha ? data.checkout_sha.slice(0, 7) : null; 31 | const message = data.messsage || ''; 32 | const commitCount = data.total_commits_count; 33 | const isCreated = this.isCreated(data); 34 | 35 | return [ 36 | `⚡ **${actor}** ${isCreated ? 'created' : 'deleted'} tag \`${tag}\` ${sha ? `from commit \`${sha}\`` : ''}${ 37 | isCreated ? `with ${commitCount} ${commitCount !== 1 ? 'commits' : 'commit'}` : '' 38 | }`, 39 | ` ${message.split('\n')[0]}`, 40 | ` `, 41 | `${isCreated ? `<${data.project.web_url}/tags/${tag}>` : ''}`, 42 | ] 43 | .filter((e) => e !== '') 44 | .join('\n'); 45 | } 46 | 47 | isCreated(data) { 48 | return data.before === '0000000000000000000000000000000000000000'; 49 | } 50 | } 51 | 52 | module.exports = TagPush; 53 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/wiki_page-create.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class WikiPageCreate extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event is fired when a wiki page is created', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const page = data.object_attributes; 12 | return { 13 | color: 0x29bb9c, 14 | title: `Created wiki page \`${page.title}\``, 15 | description: page.content, 16 | }; 17 | } 18 | 19 | text(data) { 20 | const actor = data.user.name; 21 | const page = data.object_attributes; 22 | return [ 23 | `📰 **${actor}** created wiki page **${page.title}**`, 24 | (page.content ? ` ${page.content.slice(0, 100).replace(/\n/g, ' ')}\n` : '') + `<${page.url}>`, 25 | ].join('\n'); 26 | } 27 | } 28 | 29 | module.exports = WikiPageCreate; 30 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/wiki_page-delete.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class WikiPageDelete extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event is fired when a wiki page is deleted', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const page = data.object_attributes; 12 | return { 13 | color: 0x29bb9c, 14 | title: `Deleted wiki page \`${page.title}\``, 15 | }; 16 | } 17 | 18 | text(data) { 19 | const actor = data.user.name; 20 | const page = data.object_attributes; 21 | return [`📰 **${actor}** deleted wiki page **${page.title}**`].join('\n'); 22 | } 23 | } 24 | 25 | module.exports = WikiPageDelete; 26 | -------------------------------------------------------------------------------- /lib/Gitlab/Events/wiki_page-update.js: -------------------------------------------------------------------------------- 1 | const EventResponse = require('../EventResponse'); 2 | 3 | class WikiPageUpdate extends EventResponse { 4 | constructor(...args) { 5 | super(...args, { 6 | description: 'This event is fired when a wiki page is updated', 7 | }); 8 | } 9 | 10 | embed(data) { 11 | const page = data.object_attributes; 12 | return { 13 | color: 0x29bb9c, 14 | title: `Updated wiki page \`${page.title}\``, 15 | description: page.content, 16 | }; 17 | } 18 | 19 | text(data) { 20 | const actor = data.user.name; 21 | const page = data.object_attributes; 22 | return [ 23 | `📰 **${actor}** updated wiki page **${page.title}**`, 24 | ` ${page.content.slice(0, 100).replace(/\n/g, ' ')}`, 25 | `<${page.url}>`, 26 | ].join('\n'); 27 | } 28 | } 29 | 30 | module.exports = WikiPageUpdate; 31 | -------------------------------------------------------------------------------- /lib/Gitlab/index.js: -------------------------------------------------------------------------------- 1 | const EventHandler = require('./EventHandler'); 2 | const got = require('got'); 3 | const Constants = require('./Constants'); 4 | const parse = require('./parser'); 5 | 6 | /** 7 | * Gitlab class with custom helper methods, logs in immediatly if token is found 8 | */ 9 | class Gitlab { 10 | constructor() { 11 | EventHandler.setGitlab(this.gitlab); 12 | 13 | this.Constants = Constants; 14 | 15 | Log.info(`GitLab | Set up!`); 16 | } 17 | /** 18 | * Get GitLab repository information 19 | * @param {String} ownerOrId - Repo's owner or full repository name/url 20 | * @param {String} [name] - Repo's name, required if ownerOrId is repo's owner 21 | * @return {Promise} 22 | */ 23 | getRepo(ownerOrId, name) { 24 | const repoId = this._getRepoID(ownerOrId, name); 25 | 26 | return this._request(Constants.Endpoints.Project(repoId)); 27 | } 28 | 29 | /** 30 | * Get GitLab repository's issues 31 | * @param {String} ownerOrId - repo's owner or full repo name/url 32 | * @param {String} [name] - repo's name, required if ownerOrId is repo's owner 33 | * @param {Object} [params = {}] - api params 34 | * @return {Promise} 35 | */ 36 | getProjectIssues(ownerOrId, name, params) { 37 | const repoId = this._getRepoID(ownerOrId, name); 38 | 39 | return this._request(Constants.Endpoints.Project(repoId).issues, params); 40 | } 41 | 42 | /** 43 | * Get GitLab repository's specific issue 44 | * @param {String} ownerOrId - repo's owner or full repo name/url 45 | * @param {String} [name] - repo's name, required if ownerOrId is repo's owner 46 | * @param {String|Number} issueID - repo's issue 47 | * @return {Promise} 48 | */ 49 | getProjectIssue(ownerOrId, name, issueID) { 50 | if (typeof issueID === 'string') issueID = parseInt(issueID); 51 | const repoId = this._getRepoID(ownerOrId, name); 52 | 53 | return this._request(Constants.Endpoints.Project(repoId).Issue(issueID)); 54 | } 55 | 56 | /** 57 | * Get GitLab repository's issues 58 | * @param {String} ownerOrId - repo's owner or full repo name/url 59 | * @param {String} [name] - repo's name, required if ownerOrId is repo's owner 60 | * @param {Object} [params = {}] - api params 61 | * @return {Promise} 62 | */ 63 | getProjectMergeRequests(ownerOrId, name, params) { 64 | const repoId = this._getRepoID(ownerOrId, name); 65 | 66 | return this._request(Constants.Endpoints.Project(repoId).MergeRequests(), params); 67 | } 68 | 69 | /** 70 | * Get GitLab repository's specific issue 71 | * @param {String} ownerOrId - repo's owner or full repo name/url 72 | * @param {String} [name] - repo's name, required if ownerOrId is repo's owner 73 | * @param {String|Number} issueID - repo's issue 74 | * @return {Promise} 75 | */ 76 | getProjectMergeRequest(ownerOrId, name, issueID) { 77 | if (typeof issueID === 'string') issueID = parseInt(issueID); 78 | 79 | const repoId = this._getRepoID(ownerOrId, name); 80 | 81 | return this._request(Constants.Endpoints.Project(repoId).MergeRequest(issueID)); 82 | } 83 | 84 | /** 85 | * Search GitLab organizations 86 | * @param {String} query - query 87 | * @return {Promise} 88 | */ 89 | searchGroups(query) { 90 | return this._request(Constants.Endpoints.groups, { 91 | search: query, 92 | }); 93 | } 94 | 95 | /** 96 | * Get GitLab organization's public projects 97 | * @param {String} org - organization name / id 98 | * @return {Promise} 99 | */ 100 | getGroupProjects(org) { 101 | return this._request(Constants.Endpoints.Group(encodeURIComponent(org)).projects); 102 | } 103 | 104 | _getRepoID(ownerOrId, name) { 105 | const data = name && parse(`${ownerOrId}/${name}`); 106 | return encodeURIComponent(data ? data.repo : ownerOrId); 107 | } 108 | 109 | _request(url, searchParams) { 110 | return got(url.toString(), { searchParams, responseType: 'json' }); 111 | } 112 | } 113 | 114 | module.exports = new Gitlab(); 115 | -------------------------------------------------------------------------------- /lib/Gitlab/parser.js: -------------------------------------------------------------------------------- 1 | const regex = 2 | /^(?:(?:https?:\/\/|git@)((?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))(?:\/|:))?([\w\.@\:\/\-~]+?)(\.git)?$/i; 3 | 4 | /** 5 | * Gitlab repository 6 | * @typedef {Object} GitlabParsedRepository 7 | * @property {String} repo full repository name, including owner & group 8 | * @property {String} repository same as .repo 9 | * @property {String} owner repo owner 10 | * @property {String} group repo group, if any 11 | * @property {String} name repo name 12 | */ 13 | 14 | /** 15 | * Gitlab repository parser 16 | * @param {String} str input 17 | * @return {GitlabParsedRepository} output 18 | */ 19 | module.exports = (str) => { 20 | if (!str || typeof str !== 'string' || !str.length) return {}; 21 | 22 | const out = regex.exec(str); 23 | 24 | if (!out) return {}; 25 | 26 | const repo = (out[2] && out[2].split('/')) || []; 27 | const parsed = { 28 | host: out[1], 29 | repo: out[2], 30 | }; 31 | 32 | parsed.owner = repo[0]; 33 | parsed.group = repo.length > 2 ? repo.slice(1, repo.length - 1) : null; 34 | parsed.name = repo[repo.length - 1]; 35 | parsed.isGitlab = !parsed.host || parsed.host === 'gitlab.com'; 36 | 37 | return parsed; 38 | }; 39 | 40 | module.exports.getRepo = (str) => { 41 | const out = module.exports(str); 42 | 43 | return out && out.repo; 44 | }; 45 | -------------------------------------------------------------------------------- /lib/Models/Channel.js: -------------------------------------------------------------------------------- 1 | const bookshelf = require('.'); 2 | const Model = require('./Model'); 3 | 4 | require('./Guild'); 5 | require('./ChannelRepo'); 6 | require('./ChannelOrg'); 7 | 8 | class Channel extends Model { 9 | get tableName() { 10 | return 'channels'; 11 | } 12 | 13 | static get validKeys() { 14 | return ['repo', 'useEmbed']; 15 | } 16 | 17 | get casts() { 18 | return { 19 | useEmbed: 'boolean', 20 | 21 | eventsList: 'array', 22 | usersList: 'array', 23 | branchesList: 'array', 24 | }; 25 | } 26 | 27 | guild() { 28 | return this.belongsTo('Guild'); 29 | } 30 | 31 | repos() { 32 | return this.hasMany('ChannelRepo'); 33 | } 34 | 35 | orgs() { 36 | return this.hasMany('ChannelOrg'); 37 | } 38 | 39 | getRepos() { 40 | return this.related('repos').pluck('name'); 41 | } 42 | 43 | getOrgs() { 44 | return this.related('org').pluck('name'); 45 | } 46 | 47 | async addRepo(repo) { 48 | await this.related('repos').create({ 49 | name: repo, 50 | }); 51 | 52 | return this; 53 | } 54 | 55 | async addOrg(org) { 56 | await this.related('org').save({ 57 | name: org, 58 | }); 59 | 60 | return this; 61 | } 62 | 63 | static async find(channel, ...args) { 64 | let ch = await super.find(channel.id || channel, ...args); 65 | 66 | if (!ch && channel.id) ch = this.create(channel); 67 | 68 | return ch; 69 | } 70 | 71 | static create(channel) { 72 | if (!channel.guild) return Log.warn(`DB | Channels -- not creating channel \`${channel.id}\` w/o guild!`); 73 | 74 | Log.info(`DB | Channels + "${channel.guild?.name}"'s #${channel.name} (${channel.id})`); 75 | 76 | return this.forge({ 77 | id: channel.id, 78 | name: channel.name, 79 | 80 | guildId: channel.guild?.id, 81 | }).save(null, { 82 | method: 'insert', 83 | }); 84 | } 85 | 86 | /** 87 | * Delete channel 88 | * @param {external:Channel} channel 89 | * @param {boolean} [fail] 90 | */ 91 | static delete(channel, fail = true) { 92 | Log.info(`DB | Channels - "${channel.guild.name}"'s #${channel.name} (${channel.id})`); 93 | 94 | return this.forge({ 95 | id: channel.id, 96 | }).destroy({ 97 | require: fail, 98 | }); 99 | } 100 | 101 | static findByRepo(repo) { 102 | const r = repo.toLowerCase(); 103 | 104 | return this.query((qb) => qb.join('channel_repos', 'channel_repos.channel_id', 'channels.id').where('channel_repos.name', r)).fetchAll(); 105 | } 106 | 107 | static findByOrg(org) { 108 | const r = org.toLowerCase(); 109 | 110 | return this.query((qb) => qb.join('channel_orgs', 'channel_orgs.channel_id', 'channels.id').where('channel_orgs.name', r)).fetchAll(); 111 | } 112 | 113 | static async addRepoToChannel(channel, repo) { 114 | const ch = await this.find(channel); 115 | 116 | return ch && ch.addRepo(repo); 117 | } 118 | } 119 | 120 | module.exports = bookshelf.model('Channel', Channel); 121 | -------------------------------------------------------------------------------- /lib/Models/ChannelOrg.js: -------------------------------------------------------------------------------- 1 | const bookshelf = require('.'); 2 | const Model = require('./Model'); 3 | 4 | require('./Channel'); 5 | 6 | class ChannelOrg extends Model { 7 | get tableName() { 8 | return 'channel_orgs'; 9 | } 10 | 11 | channel() { 12 | return this.belongsTo('Channel'); 13 | } 14 | } 15 | 16 | module.exports = bookshelf.model('ChannelOrg', ChannelOrg); 17 | -------------------------------------------------------------------------------- /lib/Models/ChannelRepo.js: -------------------------------------------------------------------------------- 1 | const bookshelf = require('.'); 2 | const Model = require('./Model'); 3 | 4 | require('./Channel'); 5 | 6 | class ChannelRepo extends Model { 7 | get tableName() { 8 | return 'channel_repos'; 9 | } 10 | 11 | channel() { 12 | return this.belongsTo('Channel'); 13 | } 14 | } 15 | 16 | module.exports = bookshelf.model('ChannelRepo', ChannelRepo); 17 | -------------------------------------------------------------------------------- /lib/Models/Guild.js: -------------------------------------------------------------------------------- 1 | const bookshelf = require('.'); 2 | const Model = require('./Model'); 3 | 4 | require('./Channel'); 5 | 6 | class Guild extends Model { 7 | get tableName() { 8 | return 'guilds'; 9 | } 10 | 11 | static get validKeys() { 12 | return []; 13 | } 14 | 15 | channels() { 16 | return this.belongsTo('Channel'); 17 | } 18 | 19 | static create(guild) { 20 | Log.info(`DB | Guilds + Adding '${guild.name}' (${guild.id})`); 21 | 22 | return this.forge({ 23 | id: guild.id, 24 | name: guild.name, 25 | }).save(null, { 26 | method: 'insert', 27 | }); 28 | } 29 | 30 | /** 31 | * Delete guild 32 | * @param {external:Guild} guild 33 | * @param {boolean} [fail] 34 | */ 35 | static delete(guild, fail = true) { 36 | Log.info(`DB | Guilds - Deleting '${guild.name}' (${guild.id})`); 37 | 38 | return this.forge({ 39 | id: guild.id, 40 | }).destroy({ 41 | require: fail, 42 | }); 43 | } 44 | } 45 | 46 | module.exports = bookshelf.model('Guild', Guild); 47 | -------------------------------------------------------------------------------- /lib/Models/Model.js: -------------------------------------------------------------------------------- 1 | const bookshelf = require('.'); 2 | const _ = require('lodash'); 3 | 4 | module.exports = class Model extends bookshelf.Model { 5 | static find(id, withRelated) { 6 | return this.forge({ 7 | id, 8 | }).fetch({ 9 | require: false, 10 | withRelated, 11 | }); 12 | } 13 | 14 | parse(attrs) { 15 | const clone = _.mapKeys(attrs, function (value, key) { 16 | return _.camelCase(key); 17 | }); 18 | 19 | if (this.casts) 20 | Object.keys(this.casts).forEach((key) => { 21 | const type = this.casts[key]; 22 | const val = clone[key]; 23 | 24 | if (type === 'boolean' && val !== undefined) { 25 | clone[key] = !(val === 'false' || val == 0); 26 | } 27 | 28 | if (type === 'array') { 29 | try { 30 | clone[key] = JSON.parse(val) || []; 31 | } catch (err) { 32 | clone[key] = []; 33 | } 34 | } 35 | }); 36 | 37 | return clone; 38 | } 39 | 40 | format(attrs) { 41 | const clone = attrs; 42 | 43 | if (this.casts) 44 | Object.keys(this.casts).forEach((key) => { 45 | const type = this.casts[key]; 46 | const val = clone[key]; 47 | 48 | if (type === 'boolean' && val !== undefined) { 49 | clone[key] = Number(val === true || val === 'true'); 50 | } 51 | 52 | if (type === 'array' && val) { 53 | clone[key] = JSON.stringify(val); 54 | } 55 | }); 56 | 57 | return _.mapKeys(attrs, function (value, key) { 58 | return _.snakeCase(key); 59 | }); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /lib/Models/index.js: -------------------------------------------------------------------------------- 1 | const knex = require('knex')(require('../../knexfile')); 2 | 3 | const bookshelf = require('bookshelf')(knex); 4 | 5 | module.exports = bookshelf; 6 | -------------------------------------------------------------------------------- /lib/Models/initialization.js: -------------------------------------------------------------------------------- 1 | const { default: PQueue } = require('p-queue'); 2 | const Guild = require('./Guild'); 3 | const Channel = require('./Channel'); 4 | const { ChannelType } = require('discord.js'); 5 | 6 | const loaded = { guilds: false, channels: false }; 7 | const createQueue = () => 8 | new PQueue({ 9 | concurrency: 5, 10 | autoStart: false, 11 | }); 12 | 13 | const addGuilds = async (bot) => { 14 | const queue = createQueue(); 15 | const guilds = await Guild.fetchAll({}); 16 | 17 | queue.addAll(bot.guilds.cache.filter((g) => g && g.id && !guilds.get(g.id)).map((g) => () => Guild.create(g))); 18 | 19 | await queue.start(); 20 | }; 21 | 22 | const addChannels = async (bot) => { 23 | const queue = createQueue(); 24 | const channels = await Channel.fetchAll(); 25 | 26 | queue.addAll(bot.channels.cache.filter((ch) => ch && ch.id && !channels.get(ch.id)).map((ch) => () => Channel.create(ch))); 27 | 28 | await queue.start(); 29 | }; 30 | 31 | module.exports = async (bot) => { 32 | if (!loaded.guilds) { 33 | loaded.guilds = true; 34 | 35 | bot.on('guildDelete', async (guild) => { 36 | if (!guild || !guild.available) return; 37 | 38 | await Guild.delete(guild, false); 39 | }); 40 | 41 | bot.on('guildCreate', async (guild) => { 42 | if (!guild || !guild.available) return; 43 | if (await Guild.find(guild.id)) return; 44 | 45 | await Guild.create(guild); 46 | }); 47 | 48 | await addGuilds(bot); 49 | } 50 | 51 | if (!loaded.channels) { 52 | loaded.channels = true; 53 | 54 | bot.on('channelDelete', async (channel) => { 55 | if (!channel || channel.type !== ChannelType.GuildText) return; 56 | 57 | await Channel.delete(channel, false); 58 | }); 59 | 60 | bot.on('channelCreate', async (channel) => { 61 | if (!channel || channel.type !== ChannelType.GuildText) return; 62 | if (await Channel.find(channel.id)) return; 63 | 64 | await Channel.create(channel); 65 | }); 66 | 67 | await addChannels(bot); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /lib/Util/Branch.js: -------------------------------------------------------------------------------- 1 | module.exports = function GetBranchName(ref) { 2 | if (!ref) { 3 | return 'unknown'; 4 | } 5 | 6 | // Slice ref/heads and leave the rest, it should be a branch name 7 | // for example refs/heads/feature/4 -> feature/4 8 | return ref.split('/').slice(2).join('/'); 9 | }; 10 | -------------------------------------------------------------------------------- /lib/Util/Log.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const util = require('util'); 3 | const winston = require('winston'); 4 | const moment = require('moment'); 5 | const cleanStack = require('clean-stack'); 6 | const PrettyError = require('pretty-error'); 7 | const pe = new PrettyError(); 8 | 9 | const { BaseError: ShapeshiftBaseError } = require('@sapphire/shapeshift'); 10 | 11 | pe.alias(process.cwd(), '.'); 12 | pe.skipPackage('discord.js', 'ws'); 13 | 14 | pe.appendStyle({ 15 | 'pretty-error > trace > item': { 16 | marginBottom: 0, 17 | }, 18 | }); 19 | 20 | class Log { 21 | constructor() { 22 | this._colors = { 23 | error: 'red', 24 | warn: 'yellow', 25 | info: 'cyan', 26 | debug: 'green', 27 | message: 'white', 28 | verbose: 'grey', 29 | }; 30 | this.logger = new winston.Logger({ 31 | levels: { 32 | error: 0, 33 | warn: 1, 34 | info: 2, 35 | message: 3, 36 | verbose: 4, 37 | debug: 5, 38 | silly: 6, 39 | }, 40 | transports: [ 41 | new winston.transports.Console({ 42 | colorize: true, 43 | prettyPrint: true, 44 | timestamp: () => moment().format('MM/D/YY HH:mm:ss'), 45 | align: true, 46 | level: process.env.LOG_LEVEL || 'info', 47 | }), 48 | ], 49 | exitOnError: false, 50 | }); 51 | 52 | winston.addColors(this._colors); 53 | 54 | this.error = this.error.bind(this); 55 | this.warn = this.warn.bind(this); 56 | this.info = this.info.bind(this); 57 | this.verbose = this.verbose.bind(this); 58 | this.debug = this.debug.bind(this); 59 | this.silly = this.silly.bind(this); 60 | 61 | this._token = process.env.DISCORD_TOKEN; 62 | this._tokenRegEx = new RegExp(this._token, 'g'); 63 | } 64 | error(error, ...args) { 65 | if (error.stack) error.stack = cleanStack(error.stack); 66 | 67 | // Do not pretty-render validation errors, as that gets rid of the actual error message! 68 | // Winston also seems to break display 69 | if (error instanceof ShapeshiftBaseError) { 70 | console.error(error); 71 | return this; 72 | } 73 | 74 | if (error instanceof Error) error = pe.render(error); 75 | 76 | this.logger.error(error, ...args); 77 | return this; 78 | } 79 | warn(warn, ...args) { 80 | this.logger.warn(warn, ...args); 81 | return this; 82 | } 83 | info(...args) { 84 | this.logger.info(...args); 85 | return this; 86 | } 87 | verbose(...args) { 88 | this.logger.verbose(...args); 89 | return this; 90 | } 91 | debug(arg, ...args) { 92 | if (typeof arg === 'object') arg = util.inspect(arg, { depth: 0 }); 93 | 94 | this.logger.debug(arg, ...args); 95 | return this; 96 | } 97 | silly(...args) { 98 | this.logger.silly(...args); 99 | return this; 100 | } 101 | } 102 | 103 | module.exports = new Log(); 104 | -------------------------------------------------------------------------------- /lib/Util/MergeDefault.js: -------------------------------------------------------------------------------- 1 | module.exports = function merge(def, given) { 2 | if (!given) return def; 3 | for (const key in def) { 4 | if (!{}.hasOwnProperty.call(given, key)) { 5 | given[key] = def[key]; 6 | } else if (given[key] === Object(given[key])) { 7 | given[key] = merge(def[key], given[key]); 8 | } 9 | } 10 | 11 | return given; 12 | }; 13 | -------------------------------------------------------------------------------- /lib/Util/Pad.js: -------------------------------------------------------------------------------- 1 | const Pad = (string, length) => string + ' '.repeat(length - string.length); 2 | 3 | module.exports = Pad; 4 | -------------------------------------------------------------------------------- /lib/Util/YappyGitLab.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const path = require('path'); 3 | const Log = require('./Log'); 4 | 5 | class YappyGitLab { 6 | constructor() { 7 | this.directories = { 8 | root: path.resolve(__dirname, '../../'), 9 | Discord: path.resolve(__dirname, '../Discord'), 10 | DiscordCommands: path.resolve(__dirname, '../Discord/Commands'), 11 | Github: path.resolve(__dirname, '../Github'), 12 | Models: path.resolve(__dirname, '../Models'), 13 | Util: __dirname, 14 | }; 15 | 16 | this.git = { 17 | release: '???', 18 | commit: '???', 19 | }; 20 | 21 | this._setCommit().catch(Log.error); 22 | this._setRelease().catch(Log.error); 23 | } 24 | 25 | async execSync(command) { 26 | return new Promise((resolve, reject) => { 27 | exec( 28 | command, 29 | { 30 | cwd: this.directories.root, 31 | }, 32 | (err, stdout, stderr) => { 33 | if (err) return reject(stderr); 34 | resolve(stdout); 35 | } 36 | ); 37 | }); 38 | } 39 | 40 | async _setRelease() { 41 | try { 42 | this.git.release = await this.execSync(`git describe --abbrev=0 --tags`); 43 | } catch (e) { 44 | if (e.includes('fatal: No names found, cannot describe anything.')) this.git.release = 'Unreleased'; 45 | else Log.error(typeof e === 'object' ? e : new Error(e)); 46 | } 47 | } 48 | async _setCommit() { 49 | this.git.commit = (await this.execSync(`git rev-parse HEAD`)).replace(/\n/, ''); 50 | } 51 | } 52 | 53 | module.exports = new YappyGitLab(); 54 | -------------------------------------------------------------------------------- /lib/Util/filter.js: -------------------------------------------------------------------------------- 1 | const isFound = (data, item) => data.includes(item) || data.includes(item.split('/')[0]); 2 | 3 | module.exports = { 4 | whitelist: 5 | (data = []) => 6 | (item) => 7 | item ? isFound(data, item) : true, 8 | blacklist: 9 | (data = []) => 10 | (item) => 11 | item ? !isFound(data, item) : true, 12 | }; 13 | -------------------------------------------------------------------------------- /lib/Util/index.js: -------------------------------------------------------------------------------- 1 | const Pad = require('./Pad'); 2 | const MergeDefault = require('./MergeDefault'); 3 | const GetBranchName = require('./Branch'); 4 | 5 | module.exports = { Pad, MergeDefault, GetBranchName }; 6 | -------------------------------------------------------------------------------- /lib/Web.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const get = require('lodash/get'); 4 | const GitlabEventHandler = require('./Gitlab/EventHandler'); 5 | const bot = require('./Discord'); 6 | const addons = require('@YappyBots/addons'); 7 | 8 | const GetBranchName = require('./Util').GetBranchName; 9 | const filter = require('./Util/filter'); 10 | const parser = require('./Gitlab/parser'); 11 | 12 | const Channel = require('./Models/Channel'); 13 | const ChannelRepo = require('./Models/ChannelRepo'); 14 | 15 | const app = express(); 16 | const port = process.env.WEB_PORT || process.env.PORT || 8080; 17 | const ip = process.env.WEB_IP || process.env.IP || null; 18 | 19 | app.set('view engine', 'hbs'); 20 | 21 | app.use( 22 | bodyParser.urlencoded({ 23 | extended: true, 24 | limit: '5mb', 25 | }) 26 | ); 27 | 28 | app.use( 29 | bodyParser.json({ 30 | limit: '5mb', 31 | }) 32 | ); 33 | 34 | app.use((req, res, next) => { 35 | if (req.headers['content-type'] === 'application/x-www-form-urlencoded' && req.body && req.body.payload) { 36 | req.body = JSON.parse(req.body.payload); 37 | } 38 | next(); 39 | }); 40 | 41 | app.get('/', async (req, res) => { 42 | const repos = await ChannelRepo.count(); 43 | const status = bot.statuses[bot.ws.status]; 44 | const statusColor = bot.statusColors[bot.ws.status]; 45 | 46 | res.render('index', { 47 | bot, 48 | repos, 49 | status, 50 | statusColor, 51 | layout: 'layout', 52 | 53 | counts: { 54 | guilds: bot.guilds?.cache?.size ?? '???', 55 | channels: bot.channels?.cache?.size ?? '???', 56 | users: bot.users?.cache?.size ?? '???', 57 | }, 58 | }); 59 | }); 60 | 61 | app.post('/', async (req, res) => { 62 | const event = req.headers['x-gitlab-event']; 63 | const eventName = event && event.replace(` Hook`, '').replace(/ /g, '_').toLowerCase(); 64 | const data = req.body; 65 | 66 | if (!event || !data || (!data.project && !data.repository)) return res.status(403).send('Invalid data. Plz use Gitlab webhooks.'); 67 | 68 | const repo = get(data, 'project.path_with_namespace') || parser.getRepo(get(data, 'repository.url')); 69 | const channels = (repo && (await Channel.findByRepo(repo))) || []; 70 | 71 | const action = get(data, 'object_attributes.action'); 72 | const actionText = action ? `/${action}` : ''; 73 | 74 | Log.verbose(`GitLab | ${repo} - ${eventName}${actionText} (${channels.length} channels)`); 75 | 76 | res.send(`${repo} : Received ${eventName}${actionText}, emitting to ${channels.length} channels...`); 77 | 78 | const eventResponse = GitlabEventHandler.use(data, event); 79 | 80 | if (!eventResponse) return res.status(500).send('An error occurred when generating the Discord message'); 81 | if (!eventResponse.embed && !eventResponse.text) return Log.warn(`GitLab | ${repo} - ${eventName}${actionText} ignored`); 82 | 83 | const handleError = (resp, channel) => { 84 | const err = (resp && resp.body) || resp; 85 | const errors = ['Forbidden', 'Missing Access']; 86 | if (!res || !err) return; 87 | if (channel?.guild?.owner && (errors.includes(err.message) || (err.error && errors.includes(err.error.message)))) { 88 | channel.guild.owner.send(`**ERROR:** Yappy GitLab doesn't have permissions to read/send messages in ${channel}`); 89 | } else { 90 | channel.guild?.owner?.send( 91 | [ 92 | `**ERROR:** An error occurred when trying to read/send messages in ${channel}.`, 93 | "Please report this to the bot's developer\n", 94 | '```js\n', 95 | err, 96 | '\n```', 97 | ].join(' ') 98 | ); 99 | Log.error(err); 100 | } 101 | }; 102 | 103 | const actor = { 104 | name: get(data, 'user.username') || data.user_username, 105 | id: get(data, 'user.id') || data.user_id, 106 | }; 107 | const branch = data.ref ? GetBranchName(data.ref) : data.object_attributes && data.object_attributes.ref; 108 | 109 | channels.forEach(async (conf) => { 110 | const wantsEmbed = conf.get('useEmbed'); 111 | const channel = await bot.channels.fetch(conf.id).catch((err) => { 112 | if (err.message === 'Unknown Channel') { 113 | Log.warn(`Unable to fetch channel ${conf.id} -- ${err.name} ${err.message} (${err.status})`); 114 | } else { 115 | Log.error(err); 116 | } 117 | }); 118 | 119 | if (!channel) return; 120 | 121 | if ( 122 | !filter[conf.get('eventsType')](conf.get('eventsList'))(eventName + actionText) || 123 | !filter[conf.get('usersType')](conf.get('usersList'))(actor.name) || 124 | !filter[conf.get('branchesType')](conf.get('branchesList'))(branch) 125 | ) { 126 | return; 127 | } 128 | 129 | try { 130 | if (wantsEmbed) await channel.send({ embeds: [eventResponse.embed] }); 131 | else await channel.send(`**${repo}**: ${eventResponse.text}`); 132 | } catch (err) { 133 | handleError(err, channel); 134 | } 135 | }); 136 | }); 137 | 138 | app.use( 139 | addons.express.middleware( 140 | bot, 141 | { 142 | Channel: require('./Models/Channel'), 143 | Guild: require('./Models/Guild'), 144 | }, 145 | { 146 | CLIENT_ID: process.env.DISCORD_CLIENT_ID, 147 | CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET, 148 | host: process.env.BDPW_KEY ? 'https://yappy.dsev.dev/gitlab' : `http://localhost:${port}`, 149 | } 150 | ) 151 | ); 152 | 153 | app.use((err, req, res, next) => { 154 | if (err) Log.error(err); 155 | res.status(500); 156 | res.send(err.stack); 157 | }); 158 | 159 | app.listen(port, ip, () => { 160 | Log.info(`Express | Listening on ${ip || 'localhost'}:${port}`); 161 | }); 162 | 163 | module.exports = app; 164 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | global.Log = require('./Util/Log'); 4 | 5 | exports.Web = require('./Web'); 6 | exports.Models = require('./Models'); 7 | exports.Util = require('./Util'); 8 | if (process.env.IS_DOCKER !== 'true') exports.YappyGitLab = require('./Util/YappyGitLab'); 9 | exports.Discord = require('./Discord'); 10 | exports.Gitlab = require('./Gitlab'); 11 | 12 | process.on('unhandledRejection', Log.error); 13 | process.on('uncaughtException', Log.error); 14 | 15 | /** 16 | * Discord.JS's Client 17 | * @external {Client} 18 | * @see {@link https://discord.js.org/#/docs/main/master/class/Client} 19 | */ 20 | 21 | /** 22 | * Discord.JS's Guild 23 | * @external {Guild} 24 | * @see {@link https://discord.js.org/#/docs/main/master/class/Guild} 25 | */ 26 | 27 | /** 28 | * Discord.JS's Channel 29 | * @external {Channel} 30 | * @see {@link https://discord.js.org/#/docs/main/master/class/Channel} 31 | */ 32 | 33 | /** 34 | * Discord.JS's Message 35 | * @external {Message} 36 | * @see {@link https://discord.js.org/#/docs/main/master/class/Message} 37 | */ 38 | 39 | /** 40 | * Discord.JS's Collection 41 | * @external {Collection} 42 | * @see {@link https://discord.js.org/#/docs/main/master/class/Collection} 43 | */ 44 | 45 | /** 46 | * Discord.JS's Client Options 47 | * @external {ClientOptions} 48 | * @see {@link https://discord.js.org/#/docs/main/master/typedef/ClientOptions} 49 | */ 50 | 51 | /** 52 | * Discord.JS's Color Resolvable 53 | * @external {ColorResolvable} 54 | * @see {@link https://discord.js.org/#/docs/main/master/typedef/ColorResolvable} 55 | */ 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@YappyBots/GitLab", 3 | "version": "1.9.1", 4 | "description": "A GitLab repo monitor bot for Discord", 5 | "main": "lib/index.js", 6 | "private": true, 7 | "scripts": { 8 | "start": "node lib/index.js", 9 | "lint": "prettier --single-quote --trailing-comma es5 --print-width 150 --tab-width 4 --write lib db/migrations", 10 | "docs": "docgen --source lib --jsdoc .jsdoc.json --custom docs/index.yml --output docs/docs.json", 11 | "docs:test": "docgen --source lib --jsdoc .jsdoc.json --custom docs/index.yml", 12 | "db:migrate": "knex migrate:latest" 13 | }, 14 | "repository": { 15 | "url": "https://github.com/YappyBots/YappyGitLab", 16 | "type": "git" 17 | }, 18 | "author": "David Sevilla Martin ", 19 | "license": "MIT", 20 | "dependencies": { 21 | "@YappyBots/addons": "github:YappyBots/yappy-addons#1107d5d", 22 | "body-parser": "^2.2.0", 23 | "bookshelf": "^1.2.0", 24 | "bufferutil": "^4.0.9", 25 | "chalk": "^4.0.0", 26 | "clean-stack": "^3.0.0", 27 | "cookie-parser": "^1.4.6", 28 | "discord.js": "^14.19.3", 29 | "dotenv": "^16.0.0", 30 | "express": "^4.17.2", 31 | "got": "^11.8.6", 32 | "hbs": "^4.2.0", 33 | "jsondiffpatch": "^0.4.1", 34 | "knex": "^3.1.0", 35 | "moment": "^2.29.1", 36 | "moment-duration-format": "^2.3.2", 37 | "p-queue": "^6.3.0", 38 | "parse-github-url": "^1.0.2", 39 | "pretty-error": "^4.0.0", 40 | "punycode": "^2.1.1", 41 | "sqlite3": "^5.1.6", 42 | "winston": "^2.4.4", 43 | "zlib-sync": "^0.1.10" 44 | }, 45 | "devDependencies": { 46 | "prettier": "^3.5.3" 47 | }, 48 | "overrides": { 49 | "swag": { 50 | "handlebars": "^4.7.7" 51 | }, 52 | "bookshelf": { 53 | "knex": "$knex" 54 | }, 55 | "@YappyBots/addons": { 56 | "got": "$got" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /views/index.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | Yappy Gitlab Screenshot 6 |
7 |
8 |
9 |

10 | Introducing Yappy, the GitLab Monitor 11 |

12 |
13 |

14 | It's time to say hello to GitLab repo events right in your Discord server.
15 | Simply set up webhooks to a server, init repo in channel, and you're good-to-go! 16 |

17 |
18 |
19 |
20 |

21 | Guilds
22 | {{counts.guilds}} 23 |

24 |
25 |
26 |

27 | Channels
28 | {{counts.channels}} 29 |

30 |
31 |
32 |

33 | Users
34 | {{counts.users}} 35 |

36 |
37 |
38 |

39 | Repos
40 | {{repos}} 41 |

42 |
43 |
44 |
45 | 46 | Add Yappy GitLab 47 | 48 |
49 |
50 |
51 | -------------------------------------------------------------------------------- /views/layout.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{#if title}}{{title}}{{else}}Yappy, the GitLab Monitor{{/if}} 8 | 9 | 10 | 11 | 36 | 37 | 38 |
39 |
40 |
41 | 59 |
60 |
61 | 62 |
63 | {{{body}}} 64 |
65 | 66 |
67 | 76 |
77 |
78 | 79 | 80 | --------------------------------------------------------------------------------