├── .github └── CODEOWNERS ├── .gitignore ├── Caddyfile ├── LICENSE ├── README.md ├── config └── admin │ ├── integrations │ ├── github-webhook.js │ └── webhook-incoming │ │ └── uptimerobot-incoming.js │ └── layout │ └── content--home-body.html ├── docker-compose.yml ├── renovate.json └── sample.env /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @freecodecamp/ops 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | chat.freecodecamp.org { 2 | tls internal 3 | reverse_proxy rocketchat:21212 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, freeCodeCamp.org 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Usage: 2 | 3 | 1. Create a .env 4 | 5 | ```txt 6 | MONGO_URL=mongodb://urlforthedatabase:27017/rocketchat 7 | MONGO_OPLOG_URL=mongodb://urlforthedatabase:27017/local 8 | 9 | INSTANCE_IP= 10 | ``` 11 | 12 | 2. Replace the keys with required values 13 | 14 | 3. Run rocket-chat server 15 | 16 | ```console 17 | docker-compose config 18 | docker-compose up -d 19 | ``` 20 | 21 | Updating: 22 | 23 | We receive updates to the Rocket.Chat Docker images via `renovate` bot. Once an update PR is merged, run the ansible-playbook in the housekeeping repo to deploy fresh containers. 24 | -------------------------------------------------------------------------------- /config/admin/integrations/github-webhook.js: -------------------------------------------------------------------------------- 1 | /* exported Script */ 2 | 3 | String.prototype.capitalizeFirstLetter = function() { 4 | return this.charAt(0).toUpperCase() + this.slice(1); 5 | } 6 | 7 | /** 8 | * Adding a GitHub username to this list will suppress 9 | * all notifications generated by actions from that user. 10 | * Helpful for filtering out bot messages. 11 | */ 12 | const ignoredActors = ['renovate[bot]', 'gitpod-io[bot]', 'codesee-architecture-diagrams[bot]']; 13 | 14 | /** 15 | * Adding a branch name to list will suppress all notifications generated by 16 | * commits to that branch. NOTE: this will ignore any ref that ends with an 17 | * ignored branch name. 18 | */ 19 | 20 | const ignoredBranches = ['gh-pages']; 21 | 22 | const getLabelsField = (labels) => { 23 | let labelsArray = []; 24 | labels.forEach(function(label) { 25 | labelsArray.push(label.name); 26 | }); 27 | labelsArray = labelsArray.join(', '); 28 | return { 29 | title: 'Labels', 30 | value: labelsArray, 31 | short: labelsArray.length <= 40 32 | }; 33 | }; 34 | 35 | const githubEvents = { 36 | ping(request) { 37 | return { 38 | content: { 39 | text: '_' + request.content.hook.id + '_\n' + ':thumbsup: ' + request.content.zen 40 | } 41 | }; 42 | }, 43 | 44 | /* NEW OR MODIFY ISSUE */ 45 | issues(request) { 46 | const user = request.content.sender; 47 | 48 | // Ignore Authors 49 | if (ignoredActors.includes(user.login)) return; 50 | 51 | if (request.content.action == "opened" || request.content.action == "reopened" || request.content.action == "edited") { 52 | var body = request.content.action + " by: " + user.login; 53 | } else if (request.content.action == "labeled") { 54 | var body = "Current labels: " + getLabelsField(request.content.issue.labels).value; 55 | } else if (request.content.action == "assigned" || request.content.action == "unassigned") { 56 | // Note that the issues API only gives you one assignee. 57 | var body = "Current assignee: " + request.content.issue.assignee.login; 58 | } else if (request.content.action == "closed") { 59 | if (request.content.issue.closed_by) { 60 | var body = "Closed by: " + request.content.issue.closed_by.login; 61 | } else { 62 | var body = "Closed."; 63 | } 64 | } else { 65 | return { 66 | error: { 67 | success: false, 68 | message: 'Unsupported issue action' 69 | } 70 | }; 71 | } 72 | 73 | const action = request.content.action.capitalizeFirstLetter(); 74 | 75 | const text = '_' + request.content.repository.full_name + '_\n' + 76 | '**[' + action + ' issue ​#' + request.content.issue.number + 77 | ' - ' + request.content.issue.title + '](' + 78 | request.content.issue.html_url + ')**\n\n' + 79 | body; 80 | 81 | return { 82 | content: { 83 | text: text 84 | } 85 | }; 86 | }, 87 | 88 | /* COMMENT ON EXISTING ISSUE */ 89 | issue_comment(request) { 90 | const user = request.content.comment.user; 91 | 92 | // ignore renovate 93 | if (ignoredActors.includes(user.login)) return; 94 | 95 | if (request.content.action == "edited") { 96 | var action = "Edited comment "; 97 | } else { 98 | var action = "Comment " 99 | } 100 | 101 | const text = '_' + request.content.repository.full_name + '_\n' + 102 | '**[' + action + ' on issue ​#' + request.content.issue.number + 103 | ' - ' + request.content.issue.title + '](' + 104 | request.content.comment.html_url + ')**\n\n' + 'By: ' + 105 | user.login; 106 | 107 | return { 108 | content: { 109 | text: text 110 | } 111 | }; 112 | }, 113 | 114 | /* COMMENT ON COMMIT */ 115 | commit_comment(request) { 116 | const user = request.content.comment.user; 117 | 118 | if (ignoredActors.includes(user.login)) return; 119 | 120 | if (request.content.action == "edited") { 121 | var action = "Edited comment "; 122 | } else { 123 | var action = "Comment " 124 | } 125 | 126 | const text = '_' + request.content.repository.full_name + '_\n' + 127 | '**[' + action + ' on commit id ' + request.content.comment.commit_id + 128 | ' - ' + + '](' + 129 | request.content.comment.html_url + ')**\n\n' + 130 | 'By: ' + user.login 131 | 132 | return { 133 | content: { 134 | text: text 135 | } 136 | }; 137 | }, 138 | /* END OF COMMENT ON COMMIT */ 139 | 140 | /* PUSH TO REPO */ 141 | push(request) { 142 | var commits = request.content.commits; 143 | var multi_commit = "" 144 | var is_short = true; 145 | var changeset = 'Changeset'; 146 | if ( commits.length > 1 ) { 147 | var multi_commit = " [Multiple Commits]"; 148 | var is_short = false; 149 | var changeset = changeset + 's'; 150 | var output = []; 151 | } 152 | const user = request.content.sender; 153 | 154 | if (ignoredActors.includes(user.login)) return; 155 | 156 | var text = '**Pushed to ' + "["+request.content.repository.full_name+"]("+request.content.repository.url+"):" 157 | + request.content.ref.split('/').pop() + "**\n\n"; 158 | 159 | for (var i = 0; i < commits.length; i++) { 160 | var commit = commits[i]; 161 | var shortID = commit.id.substring(0,7); 162 | var a = '[' + shortID + '](' + commit.url + ') - ' + commit.message; 163 | if ( commits.length > 1 ) { 164 | output.push( a ); 165 | } else { 166 | var output = a; 167 | } 168 | } 169 | 170 | if (commits.length > 1) { 171 | text += output.reverse().join('\n'); 172 | } else { 173 | text += output; 174 | } 175 | 176 | return { 177 | content: { 178 | text: text 179 | } 180 | }; 181 | }, // End GitHub Push 182 | 183 | /* NEW PULL REQUEST */ 184 | pull_request(request) { 185 | const user = request.content.sender; 186 | 187 | if (ignoredActors.includes(user.login)) return; 188 | 189 | if (request.content.action == "opened" || request.content.action == "reopened" || request.content.action == "edited") { 190 | var body = request.content.action + ' by: ' + user.login; 191 | } else if (request.content.action == "synchronize") { 192 | var body = "A new commit was added by: " + user.login; 193 | } else if (request.content.action == "labeled") { 194 | var body = "Current labels: " + getLabelsField(request.content.pull_request.labels).value; 195 | } else if (request.content.action == "assigned" || request.content.action == "unassigned") { 196 | // Note that the issues API only gives you one assignee. 197 | var body = "Current assignee: " + request.content.pull_request.assignee.login; 198 | } else if (request.content.action == "closed") { 199 | if (request.content.pull_request.merged) { 200 | var body = "Merged by: " + request.content.pull_request.merged_by.login; 201 | } else { 202 | var body = "Closed."; 203 | } 204 | } else { 205 | return { 206 | error: { 207 | success: false, 208 | message: 'Unsupported pull request action' 209 | } 210 | }; 211 | } 212 | 213 | const action = request.content.action.capitalizeFirstLetter(); 214 | 215 | const text = '_' + request.content.repository.full_name + '_\n' + 216 | '**[' + action + ' pull request ​#' + request.content.pull_request.number + 217 | ' - ' + request.content.pull_request.title + '](' + 218 | request.content.pull_request.html_url + ')**\n\n' + 219 | body; 220 | 221 | return { 222 | content: { 223 | text: text 224 | } 225 | }; 226 | }, 227 | }; 228 | 229 | class Script { 230 | process_incoming_request({ request }) { 231 | const header = request.headers['x-github-event']; 232 | 233 | if (ignoredBranches.some(branch => request?.content?.ref?.endsWith(branch))) { 234 | return; 235 | } 236 | if (githubEvents[header]) { 237 | return githubEvents[header](request); 238 | } 239 | 240 | return { 241 | error: { 242 | success: false, 243 | message: 'Unsupported method' 244 | } 245 | }; 246 | } 247 | } -------------------------------------------------------------------------------- /config/admin/integrations/webhook-incoming/uptimerobot-incoming.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | This script is currently being used from https://github.com/freeCodeCamp/chat-config/tree/main/config/admin/integrations/ 4 | 5 | Attributions: 6 | @license MIT 7 | @version 0.2 8 | @author CrazyMax, https://github.com/crazy-max 9 | @updated 2018-10-14 10 | @link https://github.com/crazy-max/rocketchat-uptimerobot 11 | */ 12 | 13 | /* globals console, _, s */ 14 | 15 | const USERNAME = 'Uptime Robot'; 16 | const AVATAR_URL = 17 | 'https://raw.githubusercontent.com/crazy-max/rocketchat-uptimerobot/master/res/avatar.png'; 18 | 19 | const convertAlertDuration = (seconds) => { 20 | let days = Math.floor(seconds / (3600 * 24)); 21 | seconds -= days * 3600 * 24; 22 | let hours = Math.floor(seconds / 3600); 23 | seconds -= hours * 3600; 24 | let minutes = Math.floor(seconds / 60); 25 | seconds -= minutes * 60; 26 | 27 | let result = ''; 28 | if (days > 0) { 29 | result += ' ' + days + ' days'; 30 | } 31 | if (hours > 0) { 32 | result += ' ' + hours + ' hours'; 33 | } 34 | if (minutes > 0) { 35 | result += ' ' + minutes + ' minutes'; 36 | } 37 | if (seconds > 0) { 38 | result += ' ' + seconds + ' seconds'; 39 | } 40 | 41 | return result; 42 | }; 43 | 44 | /* exported Script */ 45 | class Script { 46 | /** 47 | * @params {object} request 48 | */ 49 | process_incoming_request({ request }) { 50 | let data = request.content; 51 | let attachmentColor = `#A63636`; 52 | let statusText = `DOWN`; 53 | let isUp = data.alertType === `2`; 54 | if (isUp) { 55 | attachmentColor = `#36A64F`; 56 | statusText = `UP`; 57 | } 58 | 59 | let attachmentText = `[${data.monitorFriendlyName}](https://uptimerobot.com/dashboard.php#${data.monitorID}) is ${statusText}`; 60 | 61 | if (isUp) { 62 | attachmentText += ` It was down for ${convertAlertDuration( 63 | data.alertDuration 64 | )}`; 65 | } else { 66 | attachmentText += ` Reason: ${data.alertDetails}`; 67 | } 68 | 69 | return { 70 | content: { 71 | username: USERNAME, 72 | icon_url: AVATAR_URL, 73 | text: `@here The uptime status for [${data.monitorFriendlyName}](https://uptimerobot.com/dashboard.php#${data.monitorID}) has changed.`, 74 | attachments: [ 75 | { 76 | text: attachmentText, 77 | color: attachmentColor 78 | } 79 | ] 80 | } 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /config/admin/layout/content--home-body.html: -------------------------------------------------------------------------------- 1 | 12 | 13 | 112 |
113 | 484 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | networks: 4 | web: 5 | external: true 6 | internal: 7 | external: false 8 | driver: bridge 9 | 10 | services: 11 | rocketchat: 12 | image: rocketchat/rocket.chat:6.5.1 13 | container_name: rocketchat 14 | restart: unless-stopped 15 | environment: 16 | PORT: 21212 17 | ROOT_URL: https://chat.freecodecamp.org 18 | MONGO_URL: ${MONGO_URL} 19 | MONGO_OPLOG_URL: ${MONGO_OPLOG_URL} 20 | INSTANCE_IP: ${INSTANCE_IP} 21 | networks: 22 | - internal 23 | ports: 24 | - 21212:21212 25 | 26 | caddy: 27 | image: caddy:2.7.6 28 | container_name: caddy 29 | restart: unless-stopped 30 | networks: 31 | - web 32 | - internal 33 | ports: 34 | - 80:80 35 | - 443:443 36 | volumes: 37 | - ./Caddyfile:/etc/caddy/Caddyfile 38 | depends_on: 39 | - rocketchat 40 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>freecodecamp/renovate-config"] 3 | } 4 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | COMPOSE_FILE=docker-compose.dev.yml 2 | PORT=3000 3 | ROOT_URL=http://localhost:3000 4 | ROCKETCHAT_VERSION=latest --------------------------------------------------------------------------------