├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── ATTRIBUTIONS.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── app.json ├── bot.js ├── components ├── express_webserver.js ├── onboarding.js ├── routes │ ├── feed.js │ ├── incoming_webhooks.js │ └── oauth.js ├── rtm_manager.js └── user_registration.js ├── demo └── scrape.js ├── example.env ├── netlify.toml ├── package-lock.json ├── package.json ├── public ├── 404.html ├── 500.html ├── _redirects ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── feed-logo.svg ├── login_error.html ├── login_success.html ├── maintenance.html ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png ├── mstile-70x70.png ├── rssbot.gif ├── rssbot.svg ├── rssbot_avatar.png ├── safari-pinned-tab.svg ├── site.webmanifest ├── styles.css └── styles.css.map ├── sass └── styles.scss ├── skills └── link_to_rss.js ├── utils ├── delete-team-data.js ├── get-channel-name.js ├── get-domain.js ├── get-feed-url.js ├── rss-link.js ├── scrape.js └── update-feed.js ├── vendor └── regex-weburl.js └── views ├── 404.hbs ├── 500.hbs ├── help.hbs ├── index.hbs ├── installation.hbs ├── layouts └── default.hbs ├── partials ├── footer.hbs ├── head.hbs └── header.hbs └── privacy.hbs /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | 3 | # EditorConfig is awesome: https://EditorConfig.org 4 | 5 | # top-most EditorConfig file 6 | root = true 7 | 8 | # Unix-style newlines with a newline ending every file 9 | [*] 10 | end_of_line = lf 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | charset = utf-8 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ericrallen] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: ericrallen 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: https://supporters.eff.org/donate/ 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .db 2 | .env 3 | node_modules 4 | components/routes/_* 5 | .sass-cache/* 6 | -------------------------------------------------------------------------------- /ATTRIBUTIONS.md: -------------------------------------------------------------------------------- 1 | 2 | # key-tree-store 3 | 4 | Copyright (c) Henrik Joreteg 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | # set-immediate-shim 13 | 14 | Copyright (c) Sindre Sorhus (sindresorhus.com) 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | # json-schema 23 | 24 | The "New" BSD License: 25 | ********************** 26 | 27 | Copyright (c) 2005-2018, The JS Foundation 28 | All rights reserved. 29 | 30 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 31 | 32 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 33 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 34 | * Neither the name of the JS Foundation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 35 | 36 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | 38 | 39 | # browser-request 40 | 41 | Copyright (c) 2014 Jason Smith Work , Jason Smith , maxogden 42 | 43 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 44 | 45 | http://www.apache.org/licenses/LICENSE-2.0 46 | 47 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 48 | 49 | 50 | # weak-map 51 | 52 | Copyright (c) 2011 Google Inc., Kris Kowal 53 | 54 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 55 | 56 | http://www.apache.org/licenses/LICENSE-2.0 57 | 58 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 59 | 60 | 61 | # scmp 62 | 63 | Copyright (c) 2014, Sean Lavine 64 | All rights reserved. 65 | 66 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 67 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 68 | * Neither the name of the scmp project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 69 | 70 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 71 | 72 | 73 | # spdx-exceptions 74 | 75 | spdx-exceptions.json (c) 2010-2015 Linux Foundation and its Contributors. 76 | 77 | spdx-exceptions.json is licensed under a Creative Commons Attribution 3.0 Unported License (CC BY 3.0) License. 78 | 79 | You should have received a copy of the license along with this work. If not, see https://creativecommons.org/licenses/by/3.0/legalcode. 80 | 81 | 82 | # botkit-studio-metrics 83 | 84 | Copyright (c) ben@howdy.ai 85 | 86 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 87 | 88 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 89 | 90 | 91 | # xmpp.js 92 | 93 | Copyright (c) 2017, xmpp.js 94 | 95 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 96 | 97 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 98 | 99 | 100 | # url-join 101 | 102 | Copyright (c) Rogier Schouten , Mike Deverell 103 | 104 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 105 | 106 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 107 | 108 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 109 | 110 | 111 | # ampersand-version 112 | 113 | Copyright (c) 2014 &yet, LLC and AmpersandJS contributors 114 | 115 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 116 | 117 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 118 | 119 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 120 | 121 | # array-next 122 | 123 | Copyright (c) Henrik Joreteg 124 | 125 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 126 | 127 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 128 | 129 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 130 | 131 | 132 | # botbuilder 133 | 134 | Copyright (c) 2016 Microsoft 135 | 136 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 137 | 138 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 139 | 140 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 141 | 142 | 143 | # botkit-studio-sdk 144 | 145 | Copyright (c) howdyai 146 | 147 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 148 | 149 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 150 | 151 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 152 | 153 | 154 | # ciscospark 155 | 156 | Copyright (c) 2016-2018 Cisco and/or its affiliates. All Rights Reserved. 157 | 158 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 159 | 160 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 161 | 162 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 163 | 164 | 165 | # concat-map 166 | 167 | Copyright (c) James Halliday 168 | 169 | Permission is hereby granted, free of charge, to any person obtaining a copy of 170 | this software and associated documentation files (the "Software"), to deal in 171 | the Software without restriction, including without limitation the rights to 172 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 173 | the Software, and to permit persons to whom the Software is furnished to do so, 174 | subject to the following conditions: 175 | 176 | The above copyright notice and this permission notice shall be included in all 177 | copies or substantial portions of the Software. 178 | 179 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 180 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 181 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 182 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 183 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 184 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 185 | 186 | 187 | # deprecate 188 | 189 | Copyright (c) Brian M. Carlson 190 | 191 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 192 | 193 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 194 | 195 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 196 | 197 | 198 | # envify 199 | 200 | Copyright (c) Hugh Kennedy (http://hughskennedy.com/) 201 | 202 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 203 | 204 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 205 | 206 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 207 | 208 | 209 | # es6-promisify 210 | 211 | Copyright (c) 2014 Mike Hall / Digital Design Labs 212 | 213 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 214 | 215 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 216 | 217 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 218 | 219 | # exit-hook 220 | 221 | "Copyright (c) Sindre Sorhus (sindresorhus.com) 222 | 223 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 224 | 225 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 226 | 227 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." 228 | 229 | 230 | # fast-json-stable-stringify 231 | 232 | Copyright (c) James Halliday 233 | 234 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 235 | 236 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 237 | 238 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 239 | 240 | 241 | # inquirer 242 | 243 | Copyright (c) 2012 Simon Boudrias 244 | 245 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 246 | 247 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 248 | 249 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 250 | 251 | # invert-kv 252 | 253 | Copyright (c) Sindre Sorhus (sindresorhus.com) 254 | 255 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 256 | 257 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 258 | 259 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 260 | 261 | 262 | 263 | # is-typedarray 264 | 265 | Copyright (c) Hugh Kennedy (http://hughsk.io/) 266 | 267 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 268 | 269 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 270 | 271 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 272 | 273 | 274 | # minimist 275 | 276 | Copyright (c) James Halliday 277 | 278 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 279 | 280 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 281 | 282 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 283 | 284 | 285 | # node-xmpp-client 286 | 287 | Copyright (c) 2016 node-xmpp 288 | 289 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 290 | 291 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 292 | 293 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 294 | 295 | # node-xmpp-core 296 | 297 | Copyright (c) 2016 node-xmpp 298 | 299 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 300 | 301 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 302 | 303 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 304 | 305 | 306 | 307 | # node-xmpp-tls-connect 308 | 309 | Copyright (c) 2016 node-xmpp 310 | 311 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 312 | 313 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 314 | 315 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 316 | 317 | 318 | # openurl 319 | 320 | Copyright (c) Axel Rauschmayer 321 | 322 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 323 | 324 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 325 | 326 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 327 | 328 | 329 | # pop-iterate 330 | 331 | Copyright (c) Kris Kowal 332 | 333 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 334 | 335 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 336 | 337 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 338 | 339 | # qbox 340 | 341 | Copyright (c) Arunoda Susiripala 342 | 343 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 344 | 345 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 346 | 347 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 348 | 349 | 350 | # readline2 351 | Copyright (c) 2014 Simon Boudrias 352 | 353 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 354 | 355 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 356 | 357 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 358 | 359 | 360 | # regenerator-runtime 361 | 362 | Copyright (c) 2014-present, Facebook, Inc. 363 | 364 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 365 | 366 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 367 | 368 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 369 | 370 | 371 | # rootpath 372 | 373 | Copyright (c) Fabrizio Moscon 374 | 375 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 376 | 377 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 378 | 379 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 380 | 381 | # wordwrap 382 | 383 | Copyright (c) James Halliday 384 | 385 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 386 | 387 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 388 | 389 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We welcome any and all improvements to `@RSS` to help ensure 2 | that it adds value to your Slack channels. 3 | 4 | More concrete developer guidelines will be posted in the near 5 | future. 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Eric Allen 4 | 5 | Copyright for the botkit framework used in this project is held by XOXCO, Inc, http://xoxco.com, http://howdy.ai. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @RSS bot 2 | 3 | ![Uptime Robot status](https://img.shields.io/uptimerobot/status/m782753434-a9c163b1ffe247099d3c6aa5.svg?style=flat-square) 4 | 5 | [![@RSS bot logo](public/favicon-32x32.png?raw=true)](https://www.rssbot.app/) 6 | [![Add to Slack](https://platform.slack-edge.com/img/add_to_slack.png)](https://www.rssbot.app/login) 7 | 8 | `@RSS bot` was built using [Botkit](https://botkit.ai). 9 | 10 | ![GIF of @RSS bot in asking user to add link to RSS feed](public/rssbot.gif) 11 | 12 | Once you add `@RSS bot` to a channel in your Slack Workspace, it 13 | will listen for any links that are posted. 14 | 15 | When a user posts a link, they will be asked if they want to 16 | add that link to the RSS Feed for that channel. 17 | 18 | Each channel that `@RSS bot` is in will get it's own RSS Feed. 19 | 20 | Any user can get the RSS Feed URL for the current channel by 21 | using the `/rssfeed` slash command. 22 | 23 | Users can ask `@RSS bot` to leave via the `/stoprss` 24 | slash command. 25 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@RSS bot", 3 | "description": "A Slack bot that generates channel-specific RSS feeds from shared links.", 4 | "repository": "https://github.com/InterwebAlchemy/scrape-rss-bot/", 5 | "keywords": ["node", "bots", "slack","botkit", "rss", "RSS bot"], 6 | "website": "https://www.rssbot.app/", 7 | "success_url":"/", 8 | "addons":[ 9 | { 10 | "plan": "mongolab", 11 | "as": "MONGODB" 12 | } 13 | ], 14 | "env": { 15 | "clientId": { 16 | "required": true, 17 | "description": "Client ID provided by Slack" 18 | }, 19 | "clientSecret": { 20 | "required": true, 21 | "description": "Client Secret provided by Slack" 22 | }, 23 | "clientSigningSecret": { 24 | "required": true, 25 | "description": "Client Signing Secret provided by Slack" 26 | }, 27 | "ANALYTICS": "FALSE", 28 | "HEROKU_APP_NAME": { 29 | "required": true 30 | }, 31 | "HEROKU_PARENT_APP_NAME": { 32 | "required": true 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | ______ ______ ______ __ __ __ ______ 3 | /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ 4 | \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ 5 | \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ 6 | \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ 7 | 8 | 9 | This is a sample Slack bot built with Botkit. 10 | 11 | This bot demonstrates many of the core features of Botkit: 12 | 13 | * Connect to Slack using the real time API 14 | * Receive messages based on "spoken" patterns 15 | * Reply to messages 16 | * Use the conversation system to ask questions 17 | * Use the built in storage system to store and retrieve information 18 | for a user. 19 | 20 | # RUN THE BOT: 21 | 22 | Create a new app via the Slack Developer site: 23 | 24 | -> http://api.slack.com 25 | 26 | Run your bot from the command line: 27 | 28 | clientId= clientSecret= PORT=<3000> node bot.js 29 | 30 | # USE THE BOT: 31 | 32 | Navigate to the built-in login page: 33 | 34 | https:///login 35 | 36 | This will authenticate you with Slack. 37 | 38 | If successful, your bot will come online and greet you. 39 | 40 | 41 | # EXTEND THE BOT: 42 | 43 | Botkit has many features for building cool and useful bots! 44 | 45 | Read all about it here: 46 | 47 | -> http://howdy.ai/botkit 48 | 49 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ 50 | 51 | const Botkit = require('botkit'); 52 | 53 | const bot_options = { 54 | clientId: process.env.clientId, 55 | clientSecret: process.env.clientSecret, 56 | clientSigningSecret: process.env.clientSigningSecret, 57 | scopes: [ 58 | 'bot', 59 | 'chat:write:bot', 60 | 'channels:history', 61 | 'channels:read', 62 | 'channels:write', 63 | 'commands', 64 | 'reactions:write', 65 | 'users:read', 66 | ], 67 | }; 68 | 69 | // Use a mongo database if specified, otherwise store in a JSON file local to the app. 70 | // Mongo is automatically configured when deploying to Heroku 71 | if (process.env.MONGODB_URI) { 72 | const mongoStorage = require('botkit-storage-mongo')({ mongoUri: process.env.MONGODB_URI, tables: [ 'links', 'feeds' ] }); 73 | 74 | bot_options.storage = mongoStorage; 75 | } else { 76 | bot_options.json_file_store = __dirname + '/.data/db/'; // store user data in a simple JSON format 77 | } 78 | 79 | // Create the Botkit controller, which controls all instances of the bot. 80 | const controller = Botkit.slackbot(bot_options); 81 | 82 | controller.startTicking(); 83 | 84 | // Set up an Express-powered webserver to expose oauth and webhook endpoints 85 | const webserver = require(__dirname + '/components/express_webserver.js')(controller); 86 | 87 | webserver.get('/', function(req, res){ 88 | res.render('index', { 89 | domain: req.get('host'), 90 | protocol: req.protocol, 91 | layout: 'layouts/default' 92 | }); 93 | }); 94 | 95 | webserver.get('/help', function(req, res){ 96 | res.render('help', { 97 | domain: req.get('host'), 98 | protocol: req.protocol, 99 | layout: 'layouts/default' 100 | }); 101 | }); 102 | 103 | webserver.get('/privacy', function(req, res){ 104 | res.render('privacy', { 105 | domain: req.get('host'), 106 | protocol: req.protocol, 107 | layout: 'layouts/default' 108 | }); 109 | }); 110 | 111 | // Set up a simple storage backend for keeping a record of customers 112 | // who sign up for the app via the oauth 113 | require(__dirname + '/components/user_registration.js')(controller); 114 | 115 | // Send an onboarding message when a new team joins 116 | require(__dirname + '/components/onboarding.js')(controller); 117 | 118 | const normalizedPath = require("path").join(__dirname, "skills"); 119 | 120 | require("fs").readdirSync(normalizedPath).forEach(function(file) { 121 | require("./skills/" + file)(controller); 122 | }); 123 | -------------------------------------------------------------------------------- /components/express_webserver.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | var cookieParser = require('cookie-parser'); 4 | var querystring = require('querystring'); 5 | var debug = require('debug')('botkit:webserver'); 6 | var http = require('http'); 7 | var hbs = require('express-hbs'); 8 | 9 | module.exports = function(controller) { 10 | 11 | /*function errorHandler (err, req, res, next) { 12 | if (res.headersSent) { 13 | return next(err); 14 | } 15 | 16 | res 17 | .status(500) 18 | .render('500', { 19 | domain: req.get('host'), 20 | protocol: req.protocol, 21 | layout: 'layouts/default' 22 | }); 23 | ; 24 | } 25 | 26 | function error404Handler (req, res) { 27 | res 28 | .status(404) 29 | .render('404', { 30 | domain: req.get('host'), 31 | protocol: req.protocol, 32 | layout: 'layouts/default' 33 | }); 34 | ; 35 | }*/ 36 | 37 | const port = process.env.PORT || 3000; 38 | 39 | var webserver = express(); 40 | 41 | webserver.use(function(req, res, next) { 42 | req.rawBody = ''; 43 | 44 | req.on('data', function(chunk) { 45 | req.rawBody += chunk; 46 | }); 47 | 48 | next(); 49 | }); 50 | 51 | webserver.use(cookieParser()); 52 | webserver.use(bodyParser.json()); 53 | webserver.use(bodyParser.urlencoded({ extended: true })); 54 | 55 | // set up handlebars ready for tabs 56 | webserver.engine('hbs', hbs.express4({ 57 | partialsDir: __dirname + '/../views/partials', 58 | })); 59 | 60 | webserver.set('view engine', 'hbs'); 61 | 62 | webserver.set('views', __dirname + '/../views/'); 63 | 64 | // Register sync helper 65 | hbs.registerHelper('processEnv', function(property, isEqualTo, exists) { 66 | if (exists) { 67 | return !!process.env[property]; 68 | } 69 | 70 | if (isEqualTo) { 71 | return process.env[property] === isEqualTo; 72 | } 73 | 74 | return process.env[property]; 75 | }); 76 | 77 | webserver.use(express.static('public')); 78 | 79 | var server = http.createServer(webserver); 80 | 81 | server.listen(port, null, function() { 82 | console.log(`Express webserver configured and listening at http://localhost:${port}`); 83 | }); 84 | 85 | // import all the pre-defined routes that are present in /components/routes 86 | var normalizedPath = require("path").join(__dirname, "routes"); 87 | 88 | require("fs").readdirSync(normalizedPath).forEach(function(file) { 89 | require("./routes/" + file)(webserver, controller); 90 | }); 91 | 92 | /*webserver.use(error404Handler); 93 | 94 | webserver.use(errorHandler);*/ 95 | 96 | controller.webserver = webserver; 97 | controller.httpserver = server; 98 | 99 | return webserver; 100 | } 101 | -------------------------------------------------------------------------------- /components/onboarding.js: -------------------------------------------------------------------------------- 1 | module.exports = function(controller) { 2 | controller.on('onboard', function(bot) { 3 | bot.startPrivateConversation({user: bot.config.createdBy},function(err,convo) { 4 | if (err) { 5 | console.log(err); 6 | } else { 7 | convo.say('Hey there! I\'m here to help you out.'); 8 | convo.say('Just `/invite` me to a channel so I can start building an RSS Feed when links are posted.'); 9 | convo.say('Use the `/rsshelp` if you have any questions.'); 10 | } 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /components/routes/feed.js: -------------------------------------------------------------------------------- 1 | const acceptableAgents = /^Feed(?:Validator|Press).+$/; 2 | 3 | const shouldRedirect = (req, res, next) => { 4 | const { teamId, channelId } = req.params; 5 | 6 | const agent = req.get('User-Agent'); 7 | 8 | const shouldRedirect = !acceptableAgents.test(agent) && process.env.ANALYTICS === 'TRUE' && process.env.FEEDPRESS_FEED_URL; 9 | 10 | if (shouldRedirect) { 11 | return res.redirect(302, `${process.env.FEEDPRESS_FEED_URL}/${teamId}-${channelId}`); 12 | } 13 | 14 | return next(); 15 | } 16 | 17 | const hasParams = (req, res, next) => { 18 | const { teamId, channelId } = req.params; 19 | 20 | // make sure we were sent the team id and channel id 21 | if (!teamId || !channelId) { 22 | return res 23 | .status(404) 24 | .render('404', { 25 | domain: req.get('host'), 26 | protocol: req.protocol, 27 | layout: 'layouts/default' 28 | }) 29 | ; 30 | } 31 | 32 | return next(); 33 | } 34 | 35 | module.exports = function(webserver, controller) { 36 | webserver.get('/feed/:teamId/:channelId', hasParams, shouldRedirect, function(req, res, next) { 37 | const { teamId, channelId } = req.params; 38 | 39 | controller.storage.feeds.get(`${teamId}::${channelId}`, function(err, cachedFeed) { 40 | if (err) { 41 | console.error('ERROR: Could not load cached feed:', err); 42 | } 43 | 44 | if (cachedFeed && cachedFeed.feed) { 45 | res 46 | .set('Content-Type', 'application/rss+xml') 47 | .status(200) 48 | .send(cachedFeed.feed) 49 | .end() 50 | ; 51 | 52 | return next(); 53 | } else { 54 | return res 55 | .status(404) 56 | .render('404', { 57 | domain: req.get('host'), 58 | protocol: req.protocol, 59 | layout: 'layouts/default' 60 | }) 61 | ; 62 | } 63 | }); 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /components/routes/incoming_webhooks.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('botkit:incoming_webhooks'); 2 | 3 | module.exports = function(webserver, controller) { 4 | 5 | debug('Configured /slack/receive url'); 6 | webserver.post('/slack/receive', function(req, res) { 7 | 8 | // NOTE: we should enforce the token check here 9 | 10 | // respond to Slack that the webhook has been received. 11 | res.status(200); 12 | 13 | // Now, pass the webhook into be processed 14 | controller.handleWebhookPayload(req, res); 15 | 16 | }); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /components/routes/oauth.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('botkit:oauth'); 2 | 3 | module.exports = function(webserver, controller) { 4 | 5 | var handler = { 6 | login: function(req, res) { 7 | res.redirect(controller.getAuthorizeURL()); 8 | }, 9 | oauth: function(req, res) { 10 | var code = req.query.code; 11 | var state = req.query.state; 12 | 13 | // we need to use the Slack API, so spawn a generic bot with no token 14 | var slackapi = controller.spawn({}); 15 | 16 | var opts = { 17 | client_id: controller.config.clientId, 18 | client_secret: controller.config.clientSecret, 19 | code: code 20 | }; 21 | 22 | slackapi.api.oauth.access(opts, function(err, auth) { 23 | 24 | if (err) { 25 | debug('Error confirming oauth', err); 26 | return res.redirect('/login_error.html'); 27 | } 28 | 29 | // what scopes did we get approved for? 30 | var scopes = auth.scope.split(/\,/); 31 | 32 | // use the token we got from the oauth 33 | // to call auth.test to make sure the token is valid 34 | // but also so that we reliably have the team_id field! 35 | slackapi.api.auth.test({token: auth.access_token}, function(err, identity) { 36 | 37 | if (err) { 38 | debug('Error fetching user identity', err); 39 | return res.redirect('/login_error.html'); 40 | } 41 | 42 | // Now we've got all we need to connect to this user's team 43 | // spin up a bot instance, and start being useful! 44 | // We just need to make sure this information is stored somewhere 45 | // and handled with care! 46 | 47 | // In order to do this in the most flexible way, we fire 48 | // a botkit event here with the payload so it can be handled 49 | // by the developer without meddling with the actual oauth route. 50 | 51 | auth.identity = identity; 52 | controller.trigger('oauth:success', [auth]); 53 | 54 | res.cookie('team_id', auth.team_id); 55 | res.cookie('bot_user_id', auth.bot.bot_user_id); 56 | res.redirect('/login_success.html'); 57 | 58 | }); 59 | 60 | 61 | }); 62 | } 63 | } 64 | 65 | 66 | // Create a /login link 67 | // This link will send user's off to Slack to authorize the app 68 | // See: https://github.com/howdyai/botkit/blob/master/readme-slack.md#custom-auth-flows 69 | debug('Configured /login url'); 70 | webserver.get('/login', handler.login); 71 | 72 | // Create a /oauth link 73 | // This is the link that receives the postback from Slack's oauth system 74 | // So in Slack's config, under oauth redirect urls, 75 | // your value should be https:///oauth 76 | debug('Configured /oauth url'); 77 | webserver.get('/oauth', handler.oauth); 78 | 79 | return handler; 80 | } 81 | -------------------------------------------------------------------------------- /components/rtm_manager.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('botkit:rtm_manager'); 2 | 3 | module.exports = function(controller) { 4 | 5 | var managed_bots = {}; 6 | 7 | // Capture the rtm:start event and actually start the RTM... 8 | controller.on('rtm:start', function(config) { 9 | var bot = controller.spawn(config); 10 | manager.start(bot); 11 | }); 12 | 13 | // 14 | controller.on('rtm_close', function(bot) { 15 | manager.remove(bot); 16 | }); 17 | 18 | // The manager object exposes some useful tools for managing the RTM 19 | var manager = { 20 | start: function(bot) { 21 | 22 | if (managed_bots[bot.config.token]) { 23 | debug('Start RTM: already online'); 24 | } else { 25 | bot.startRTM(function(err, bot) { 26 | if (err) { 27 | debug('Error starting RTM:', err); 28 | } else { 29 | managed_bots[bot.config.token] = bot.rtm; 30 | debug('Start RTM: Success'); 31 | } 32 | }); 33 | } 34 | }, 35 | stop: function(bot) { 36 | if (managed_bots[bot.config.token]) { 37 | if (managed_bots[bot.config.token].rtm) { 38 | debug('Stop RTM: Stopping bot'); 39 | managed_bots[bot.config.token].closeRTM() 40 | } 41 | } 42 | }, 43 | remove: function(bot) { 44 | debug('Removing bot from manager'); 45 | delete managed_bots[bot.config.token]; 46 | }, 47 | reconnect: function() { 48 | 49 | debug('Reconnecting all existing bots...'); 50 | controller.storage.teams.all(function(err, list) { 51 | 52 | if (err) { 53 | throw new Error('Error: Could not load existing bots:', err); 54 | } else { 55 | for (var l = 0; l < list.length; l++) { 56 | manager.start(controller.spawn(list[l].bot)); 57 | } 58 | } 59 | 60 | }); 61 | 62 | } 63 | } 64 | 65 | 66 | return manager; 67 | 68 | } 69 | -------------------------------------------------------------------------------- /components/user_registration.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('botkit:user_registration'); 2 | 3 | module.exports = function(controller) { 4 | 5 | /* Handle event caused by a user logging in with oauth */ 6 | controller.on('oauth:success', function(payload) { 7 | 8 | debug('Got a successful login!', payload); 9 | if (!payload.identity.team_id) { 10 | debug('Error: received an oauth response without a team id', payload); 11 | } 12 | controller.storage.teams.get(payload.identity.team_id, function(err, team) { 13 | if (err) { 14 | debug('Error: could not load team from storage system:', payload.identity.team_id, err); 15 | } 16 | 17 | var new_team = false; 18 | if (!team) { 19 | team = { 20 | id: payload.identity.team_id, 21 | createdBy: payload.identity.user_id, 22 | url: payload.identity.url, 23 | name: payload.identity.team, 24 | }; 25 | var new_team= true; 26 | } 27 | 28 | team.bot = { 29 | token: payload.bot.bot_access_token, 30 | user_id: payload.bot.bot_user_id, 31 | createdBy: payload.identity.user_id, 32 | app_token: payload.access_token, 33 | }; 34 | 35 | var testbot = controller.spawn(team.bot); 36 | 37 | testbot.api.auth.test({}, function(err, bot_auth) { 38 | if (err) { 39 | debug('Error: could not authenticate bot user', err); 40 | } else { 41 | team.bot.name = bot_auth.user; 42 | 43 | // add in info that is expected by Botkit 44 | testbot.identity = bot_auth; 45 | 46 | testbot.identity.id = bot_auth.user_id; 47 | testbot.identity.name = bot_auth.user; 48 | 49 | testbot.team_info = team; 50 | 51 | // Replace this with your own database! 52 | 53 | controller.storage.teams.save(team, function(err, id) { 54 | if (err) { 55 | debug('Error: could not save team record:', err); 56 | } else { 57 | if (new_team) { 58 | controller.trigger('create_team', [testbot, team]); 59 | } else { 60 | controller.trigger('update_team', [testbot, team]); 61 | } 62 | } 63 | }); 64 | } 65 | }); 66 | }); 67 | }); 68 | 69 | 70 | controller.on('create_team', function(bot, team) { 71 | 72 | debug('Team created:', team); 73 | 74 | // Trigger an event that will establish an RTM connection for this bot 75 | controller.trigger('rtm:start', [bot.config]); 76 | 77 | // Trigger an event that will cause this team to receive onboarding messages 78 | controller.trigger('onboard', [bot, team]); 79 | 80 | }); 81 | 82 | 83 | controller.on('update_team', function(bot, team) { 84 | 85 | debug('Team updated:', team); 86 | // Trigger an event that will establish an RTM connection for this bot 87 | controller.trigger('rtm:start', [bot]); 88 | 89 | }); 90 | 91 | } 92 | -------------------------------------------------------------------------------- /demo/scrape.js: -------------------------------------------------------------------------------- 1 | const metascraper = require('metascraper')([ 2 | require('metascraper-youtube')(), 3 | require('metascraper-soundcloud')(), 4 | require('metascraper-amazon')(), 5 | require('metascraper-author')(), 6 | require('metascraper-date')(), 7 | require('metascraper-description')(), 8 | require('metascraper-image')(), 9 | require('metascraper-clearbit-logo')(), 10 | require('metascraper-logo')(), 11 | require('metascraper-logo-favicon')(), 12 | require('metascraper-publisher')(), 13 | require('metascraper-title')(), 14 | require('metascraper-url')(), 15 | ]); 16 | 17 | const got = require('got'); 18 | 19 | ;(async () => { 20 | console.log(`Scraping ${process.argv[2]}...`); 21 | const { body: html, url } = await got(process.argv[2]); 22 | const metadata = await metascraper({ html, url }); 23 | console.log(metadata); 24 | })(); 25 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | # Environment Config 2 | 3 | # store your secrets and config variables in here 4 | # only invited collaborators will be able to see your .env values 5 | # reference these in your code with process.env.SECRET 6 | 7 | # botkit values 8 | clientId= 9 | clientSecret= 10 | clientSigningSecret= 11 | studio_token= 12 | 13 | # server values 14 | PORT= 15 | MONGODB_URI= 16 | 17 | # integrations 18 | APP_DOMAIN=www.rssbot.app 19 | ANALYTICS=FALSE # TRUE | FALSE 20 | GOOGLE_ANALYTICS_ID= 21 | FEEDPRESS_FEED_URL=http://feedpress.me 22 | FEEDPRESS_API_URL=https://api.feed.press 23 | FEEDPRESS_API_KEY= 24 | FEEDPRESS_API_TOKEN= 25 | FEEDPRESS_HOSTNAME= 26 | FEEDPRESS_HOSTNAME_ID= 27 | 28 | # note: .env is a shell file so there can’t be spaces around = 29 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # https://www.netlify.com/docs/continuous-deployment/#deploy-contexts 2 | 3 | [build] 4 | publish = "public" 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@interweb-alchemy/rss-bot", 3 | "version": "1.2.0", 4 | "description": "A bot for converting links in Slack to an RSS feed.", 5 | "main": "bot.js", 6 | "scripts": { 7 | "build": "sass ./sass/styles.scss ./public/styles.css", 8 | "start": "node bot.js" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.19.0", 12 | "body-parser": "^1.15.2", 13 | "botkit": "^0.7.0", 14 | "botkit-storage-mongo": "github:howdyai/botkit-storage-mongo", 15 | "cookie-parser": "^1.4.3", 16 | "debug": "^2.3.3", 17 | "express": "^4.14.0", 18 | "express-hbs": "^2.1.2", 19 | "got": "^9.6.0", 20 | "metascraper": "^5.3.0-alpha.11", 21 | "metascraper-amazon": "^5.3.0-alpha.11", 22 | "metascraper-audio": "^5.3.0-alpha.11", 23 | "metascraper-author": "^5.3.0-alpha.11", 24 | "metascraper-clearbit-logo": "^5.3.0-alpha.11", 25 | "metascraper-date": "^5.3.0-alpha.11", 26 | "metascraper-description": "^5.3.0-alpha.11", 27 | "metascraper-image": "^5.3.0-alpha.11", 28 | "metascraper-logo": "^5.3.0-alpha.11", 29 | "metascraper-logo-favicon": "^5.3.0-alpha.11", 30 | "metascraper-media-provider": "^5.3.0-alpha.11", 31 | "metascraper-publisher": "^5.3.0-alpha.11", 32 | "metascraper-readability": "^5.3.0-alpha.11", 33 | "metascraper-soundcloud": "^5.3.0-alpha.11", 34 | "metascraper-title": "^5.3.0-alpha.11", 35 | "metascraper-url": "^5.3.0-alpha.11", 36 | "metascraper-video": "^5.3.0-alpha.11", 37 | "metascraper-youtube": "^5.3.0-alpha.11", 38 | "qs": "^6.7.0", 39 | "querystring": "^0.2.0", 40 | "rss": "^1.2.2", 41 | "shortid": "^2.2.14", 42 | "wordfilter": "^0.2.6" 43 | }, 44 | "devDependencies": { 45 | "sass": "^1.20.1" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/InterwebAlchemy/scrape-rss-bot.git" 50 | }, 51 | "bugs": { 52 | "url": "https://github.com/InterwebAlchemy/scrape-rss-bot/issues" 53 | }, 54 | "homepage": "https://github.com/InterwebAlchemy/scrape-rss-bot", 55 | "keywords": [ 56 | "bots", 57 | "chatbots", 58 | "slack" 59 | ], 60 | "author": "eric@ericrallen.dev", 61 | "license": "MIT" 62 | } 63 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @RSS Bot Error! | A Slackbot that generates RSS Feeds 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 |
28 |

Sorry, @RSS bot couldn't find what you're looking for.

29 |
30 | 33 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @RSS Bot Error! | A Slackbot that generates RSS Feeds 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 |
28 |

Sorry, @RSS bot encountered an issue and can't respond right now.

29 |
30 | 33 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | # https://www.netlify.com/docs/redirects/ 2 | 3 | # Redirect default Netlify subdomain to primary domain 4 | https://loving-khorana-a9913a.netlify.com/* https://public.rssbot.app/:splat 301! 5 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterwebAlchemy/scrape-rss-bot/65ce81ad3732408e6995ad4ffaead2d34ae86671/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterwebAlchemy/scrape-rss-bot/65ce81ad3732408e6995ad4ffaead2d34ae86671/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterwebAlchemy/scrape-rss-bot/65ce81ad3732408e6995ad4ffaead2d34ae86671/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2f6c8f 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterwebAlchemy/scrape-rss-bot/65ce81ad3732408e6995ad4ffaead2d34ae86671/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterwebAlchemy/scrape-rss-bot/65ce81ad3732408e6995ad4ffaead2d34ae86671/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterwebAlchemy/scrape-rss-bot/65ce81ad3732408e6995ad4ffaead2d34ae86671/public/favicon.ico -------------------------------------------------------------------------------- /public/feed-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/login_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @RSS Bot Error! | A Slackbot that generates RSS Feeds 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 30 |
31 |

Sorry, but something went wrong with your login.

32 |
33 | 37 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /public/login_success.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | @RSS Bot Installed | A Slackbot that generates RSS Feeds 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 41 | 42 | 43 | 53 |
54 |

Success! Your new bot is waiting!

55 |
56 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /public/maintenance.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @RSS Bot Maintenance | A Slackbot that generates RSS Feeds 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 |
26 |

Sorry, @RSS bot is taking a nap for some maintenance right now.

27 |

Check @RSS bot's status here.

28 |

If you need help get in touch with @RSSSlackBot on Twitter.

29 |
30 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterwebAlchemy/scrape-rss-bot/65ce81ad3732408e6995ad4ffaead2d34ae86671/public/mstile-144x144.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterwebAlchemy/scrape-rss-bot/65ce81ad3732408e6995ad4ffaead2d34ae86671/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterwebAlchemy/scrape-rss-bot/65ce81ad3732408e6995ad4ffaead2d34ae86671/public/mstile-310x150.png -------------------------------------------------------------------------------- /public/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterwebAlchemy/scrape-rss-bot/65ce81ad3732408e6995ad4ffaead2d34ae86671/public/mstile-310x310.png -------------------------------------------------------------------------------- /public/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterwebAlchemy/scrape-rss-bot/65ce81ad3732408e6995ad4ffaead2d34ae86671/public/mstile-70x70.png -------------------------------------------------------------------------------- /public/rssbot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterwebAlchemy/scrape-rss-bot/65ce81ad3732408e6995ad4ffaead2d34ae86671/public/rssbot.gif -------------------------------------------------------------------------------- /public/rssbot.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/rssbot_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterwebAlchemy/scrape-rss-bot/65ce81ad3732408e6995ad4ffaead2d34ae86671/public/rssbot_avatar.png -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 24 | 26 | 28 | 30 | 32 | 36 | 39 | 41 | 43 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RSS bot", 3 | "short_name": "RSS bot", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /public/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body, 6 | html { 7 | margin: 0; 8 | padding: 0; 9 | font-size: 18px; 10 | font-family: sans-serif; 11 | background-color: #FFF; 12 | } 13 | 14 | body { 15 | padding: 2%; 16 | } 17 | 18 | .wrapper { 19 | max-width: 1000px; 20 | margin: 0 auto; 21 | } 22 | 23 | .box { 24 | border: 2px solid #CCC; 25 | padding: 1rem calc(1rem - 2px); 26 | margin-bottom: 1rem; 27 | } 28 | .box h1, .box h2, .box h3 { 29 | margin-top: 0; 30 | } 31 | 32 | footer { 33 | text-align: center; 34 | } 35 | 36 | .hero { 37 | text-align: center; 38 | margin-bottom: 70px; 39 | } 40 | .hero h1 { 41 | font-size: 4rem; 42 | } 43 | 44 | article { 45 | max-width: 720px; 46 | margin: 0 auto; 47 | } 48 | 49 | a { 50 | color: #2F6C8F; 51 | } 52 | 53 | .copyurl { 54 | width: 100%; 55 | font-size: 1.5rem; 56 | } 57 | 58 | div.input label { 59 | font-weight: bold; 60 | font-size: smaller; 61 | } 62 | 63 | .addon { 64 | display: flex; 65 | border: 1px solid #999; 66 | border-radius: 6px; 67 | padding: 5px; 68 | background: #F0F0F0; 69 | } 70 | .addon textarea, .addon input { 71 | flex-grow: 1; 72 | border: 0; 73 | background: transparent; 74 | } 75 | .addon button { 76 | flex-grow: 0; 77 | background: transparent; 78 | border: 1px solid #999; 79 | border-radius: 6px; 80 | font-weight: bold; 81 | } 82 | .addon button.textarea { 83 | align-self: flex-start; 84 | padding: 0.5rem; 85 | } 86 | .addon button:hover { 87 | background: #FFF; 88 | color: #2F6C8F; 89 | } 90 | 91 | div.hr { 92 | border: 1px dashed #ccc; 93 | width: 10%; 94 | margin: 4rem auto; 95 | height: 1px; 96 | } 97 | 98 | a.button { 99 | border: 2px solid #2F6C8F; 100 | font-weight: bold; 101 | margin: 0; 102 | border-radius: 3px; 103 | display: inline-block; 104 | padding: 0.5rem 2rem; 105 | text-align: center; 106 | text-decoration: none; 107 | box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.2); 108 | background-color: #FFF; 109 | transition: box-shadow 0.1s linear; 110 | } 111 | a.button:hover { 112 | box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1); 113 | } 114 | 115 | #header { 116 | text-align: center; 117 | } 118 | #header .wrapper { 119 | display: flex; 120 | flex-direction: row; 121 | justify-content: center; 122 | } 123 | #header .slack-button { 124 | margin-top: 24px; 125 | } 126 | 127 | #footer { 128 | margin-top: 60px; 129 | } 130 | 131 | /*# sourceMappingURL=styles.css.map */ 132 | -------------------------------------------------------------------------------- /public/styles.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["../sass/styles.scss"],"names":[],"mappings":"AAEA;EACE;;;AAGF;AAAA;EAEE;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;;;AAGF;EACE;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;;;AAGF;EACE,OArDU;;;AAwDZ;EACE;EACA;;;AAIA;EACE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA,OA/FM;;;AAoGZ;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;;AAEA;EAEE;;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;;AAIJ;EACE","file":"styles.css"} -------------------------------------------------------------------------------- /sass/styles.scss: -------------------------------------------------------------------------------- 1 | $bot_color: #2F6C8F; 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | body, 8 | html { 9 | margin: 0; 10 | padding: 0; 11 | font-size: 18px; 12 | font-family: sans-serif; 13 | background-color: #FFF; 14 | } 15 | 16 | body { 17 | padding: 2%; 18 | } 19 | 20 | .wrapper { 21 | max-width: 1000px; 22 | margin: 0 auto; 23 | } 24 | 25 | .box { 26 | border: 2px solid #CCC; 27 | padding: 1rem calc(1rem - 2px); 28 | margin-bottom: 1rem; 29 | 30 | h1, h2, h3 { 31 | margin-top: 0; 32 | } 33 | } 34 | 35 | footer { 36 | text-align: center; 37 | } 38 | 39 | .hero { 40 | text-align: center; 41 | margin-bottom: 70px; 42 | 43 | h1 { 44 | font-size: 4rem; 45 | } 46 | } 47 | 48 | article { 49 | max-width: 720px; 50 | margin: 0 auto; 51 | } 52 | 53 | a { 54 | color: $bot_color; 55 | } 56 | 57 | .copyurl { 58 | width: 100%; 59 | font-size: 1.5rem; 60 | } 61 | 62 | div.input { 63 | label { 64 | font-weight: bold; 65 | font-size: smaller; 66 | } 67 | } 68 | 69 | .addon { 70 | display: flex; 71 | border: 1px solid #999; 72 | border-radius: 6px; 73 | padding: 5px; 74 | background: #F0F0F0; 75 | 76 | textarea, input { 77 | flex-grow: 1; 78 | border: 0; 79 | background: transparent; 80 | } 81 | 82 | button { 83 | flex-grow: 0; 84 | background: transparent; 85 | border: 1px solid #999; 86 | border-radius: 6px; 87 | font-weight: bold; 88 | 89 | &.textarea { 90 | align-self: flex-start; 91 | padding: 0.5rem; 92 | } 93 | 94 | &:hover { 95 | background: #FFF; 96 | color: $bot_color; 97 | } 98 | } 99 | } 100 | 101 | div.hr { 102 | border: 1px dashed #ccc; 103 | width: 10%; 104 | margin: 4rem auto; 105 | height: 1px; 106 | } 107 | 108 | a.button { 109 | border: 2px solid $bot_color; 110 | font-weight: bold; 111 | // font-size: 4rem; 112 | margin: 0; 113 | border-radius: 3px; 114 | display: inline-block; 115 | padding: 0.5rem 2rem; 116 | text-align: center; 117 | text-decoration: none; 118 | // color: #FFF; 119 | box-shadow: 5px 5px 5px rgba(0,0,0,0.2); 120 | background-color: #FFF; 121 | transition: box-shadow 0.1s linear; 122 | 123 | &:hover { 124 | // background-color: rgba($bot_color, 0.1); 125 | box-shadow: 1px 1px 5px rgba(0,0,0,0.1); 126 | } 127 | } 128 | 129 | #header { 130 | text-align: center; 131 | 132 | .wrapper { 133 | display: flex; 134 | flex-direction: row; 135 | justify-content: center; 136 | } 137 | 138 | .slack-button { 139 | margin-top: 24px; 140 | } 141 | } 142 | 143 | #footer { 144 | margin-top: 60px; 145 | 146 | .sub-footer { 147 | font-size: 12px; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /skills/link_to_rss.js: -------------------------------------------------------------------------------- 1 | const shortId = require('shortid'); 2 | 3 | const scrape = require('../utils/scrape'); 4 | const getFeed = require('../utils/get-feed-url'); 5 | const getChannel = require('../utils/get-channel-name'); 6 | const updateFeed = require('../utils/update-feed'); 7 | const deleteTeamData = require('../utils/delete-team-data'); 8 | 9 | const GLOBAL_URL_REGEX = /<(https?:\/\/[-A-Z0-9._~:\/?#[\]@!$&'()*+,;=]+)>/igm; 10 | const URL_REGEX = /<(https?:\/\/[-A-Z0-9._~:\/?#[\]@!$&'()*+,;=]+)>/i; 11 | 12 | const URLS_TO_ADD = []; 13 | 14 | const processQueue = () => { 15 | if (URLS_TO_ADD.length) { 16 | const { url, message, channelName } = URLS_TO_ADD.shift(); 17 | 18 | addUrlToFeed(url, message, channelName); 19 | } 20 | } 21 | 22 | const addUrlToFeed = (url, message, channelName) => { 23 | const content = { 24 | attachments:[ 25 | { 26 | title: `Would you like to add ${url} to the #${channelName} RSS Feed?`, 27 | callback_id: 'ADD_TO_RSS', 28 | attachment_type: 'default', 29 | actions: [ 30 | { 31 | "name": "add_to_rss", 32 | "text": `+ Add to RSS Feed`, 33 | "value": `${message.ts}:|:${url}`, 34 | "type": "button", 35 | }, 36 | { 37 | "name": "not_to_rss", 38 | "text": `No Thanks`, 39 | "value": 'NO', 40 | "type": "button", 41 | }, 42 | ] 43 | } 44 | ] 45 | }; 46 | 47 | // check to see if we are in a thread 48 | if (message.thread_ts) { 49 | // if we are, let's grab the thread's timestamp, so we can reply inline 50 | const { thread_ts } = message; 51 | 52 | // reply ephemerally inline 53 | bot.whisper(message, Object.assign({}, { token: bot.config.bot.app_token, thread_ts, as_user: true }, content)); 54 | // otherwise just reply as normal 55 | } else { 56 | bot.whisper(message, Object.assign({}, { token: bot.config.bot.app_token, as_user: true }, content)); 57 | } 58 | }; 59 | 60 | module.exports = function(controller) { 61 | controller.on('app_uninstalled', function(bot, message) { 62 | const teamId = message.team; 63 | 64 | deleteTeamData(controller, teamId); 65 | }); 66 | 67 | controller.on('bot_channel_join', function(bot, message) { 68 | getChannel(bot, message, (channelName, channelId) => { 69 | bot.reply(message, `Hey there! I\'m here to generate an RSS Feed from links posted to #${channelName}.`); 70 | bot.reply(message, `*RSS Feed for #${channelName}*: <${getFeed(bot.team_info.id, channelId)}>`); 71 | bot.reply(message, 'You can get the feed URL for this channel at any time by using the `/rssfeed` command.'); 72 | bot.reply(message, `If you get tired of my RSS Feed, use \`/rssquit\` to ask me to stop generating a feed for #${channelName}.`); 73 | bot.reply(message, 'If you need help, use `/rsshelp`.'); 74 | 75 | updateFeed(controller, bot, bot.team_info.id, channelId); 76 | }); 77 | }); 78 | 79 | controller.on('slash_command',function(bot, message) { 80 | getChannel(bot, message, (channelName, channelId) => { 81 | const { command } = message; 82 | 83 | if (command === '/rssfeed') { 84 | if (channelName && channelId) { 85 | bot.api.users.conversations({ user: bot.config.bot.user_id }, (err, { channels }) => { 86 | if (err) { 87 | console.error('ERROR:', err); 88 | 89 | // TODO: Add ephemeral error message for user 90 | 91 | return; 92 | } 93 | 94 | const botChannels = channels.map(({ id }) => id); 95 | 96 | if (!botChannels.includes(channelId)) { 97 | console.warn('WARNING:', `Tried to get feed URL from #${channelName}, but @RSS bot is not in channel`); 98 | 99 | bot.replyPrivate(message, `Sorry, I'm not in this channel, but if you \`/invite @RSS bot\` I can start creating an RSS Feed for #${channelName}.`); 100 | } else { 101 | bot.replyPrivate(message, `*#${channelName} RSS Feed*: <${getFeed(bot.team_info.id, channelId)}>`); 102 | } 103 | }); 104 | } else { 105 | bot.replyPrivate(message, `Sorry, this doesn't appear to be a channel. If you need any help please reach out to .`); 106 | } 107 | } else if (command === '/rssquit') { 108 | if (channelName && channelId) { 109 | bot.api.channels.kick({ token: bot.config.bot.app_token, channel: channelId, user: bot.config.bot.user_id }, (err, response) => { 110 | if (err) { 111 | bot.replyPrivate(message, 'I\'m sorry. It looks like your account doesn\'t have permission to kick users. Please contact your nearest Slack admin to have me removed from this channel.'); 112 | } 113 | 114 | controller.storage.feeds.delete(`${bot.team_info.id}::${channelId}`, (err) => { 115 | if (err) { 116 | console.error('ERROR: could not delete feed:', err); 117 | } 118 | 119 | console.log(`Deleting feed ${bot.team_info.id}::${channelId} #${channelName}...`); 120 | 121 | controller.storage.links.find({ channelId, teamId: bot.team_info.id }, (err, links) => { 122 | if (err) { 123 | console.error(`ERROR: could not delete links for #${channelName}:`, err); 124 | } 125 | 126 | links.forEach((link) => { 127 | controller.storage.links.delete(link.id, (err) => { 128 | if (err) { 129 | console.error('ERROR: could not delete old link:', err); 130 | } 131 | }); 132 | }); 133 | 134 | bot.replyAcknowledge(); 135 | }); 136 | }); 137 | 138 | // TODO: clean up feed from FeedPress if they add /feeds/delete.json endpoint 139 | }); 140 | } else { 141 | bot.replyPrivate(message, `Sorry, this doesn't appear to be a channel. If you need any help please reach out to .`); 142 | } 143 | } else if (command === '/rsshelp') { 144 | bot.replyPrivate(message, `Hey there, \`/invite\` me to a channel to get started. If you need any help please reach out to .`); 145 | } 146 | }); 147 | }); 148 | 149 | controller.hears(['<(https?:\\/\\/[-A-Za-z0-9\\._~:\\/?#[\\]@!$&\'()\\*\\+,;=]+)>'], 'ambient', function(bot, message) { 150 | getChannel(bot, message, (channelName, channelId) => { 151 | // TODO: replace with native String.prototype.matchAll() when possible 152 | const checkForMultipleUrls = message.text.match(GLOBAL_URL_REGEX); 153 | 154 | checkForMultipleUrls.forEach((foundUrl) => { 155 | const url = URL_REGEX.exec(foundUrl)[1]; 156 | 157 | if (url) { 158 | URLS_TO_ADD.push({ url, message, channelName }); 159 | } 160 | }); 161 | 162 | processQueue(); 163 | }); 164 | }); 165 | 166 | controller.on('message_action', function(bot, message) { 167 | const channel = message.raw_message.channel; 168 | const timestamp = message.raw_message.message.ts; 169 | 170 | bot.api.users.conversations({ user: bot.config.bot.user_id }, (err, { channels }) => { 171 | if (err) { 172 | console.error('ERROR:', err); 173 | 174 | // TODO: Add ephemeral error message for user 175 | 176 | return; 177 | } 178 | 179 | if (message.callback_id === 'ADD_TO_FEED') { 180 | const botChannels = channels.map(({ id }) => id); 181 | 182 | bot.api.channels.history({ token: bot.config.bot.app_token, channel: channel.id, latest: timestamp, count: 1, inclusive: true }, (err, messageResponse) => { 183 | if (err) { 184 | console.error('ERROR:', err); 185 | 186 | // TODO: add ephemeral error message for user 187 | 188 | return; 189 | } 190 | 191 | const originalMessage = messageResponse.messages[0]; 192 | 193 | if (!botChannels.includes(channel.id)) { 194 | console.warn('WARNING:', `Tried to add message from channel #${channel.name}, but @RSS is not in channel`); 195 | 196 | // TODO: figure out why this message can't self-destruct 197 | bot.replyInteractive(message, `Sorry, I'm not in this channel, but if you \`/invite @RSS\` I can start creating an RSS Feed for #${channel.name}.`); 198 | } else { 199 | // TODO: DRY up the handling of adding items to the feed 200 | 201 | // TODO: replace with native String.prototype.matchAll() when possible 202 | const checkForMultipleUrls = originalMessage.text.match(GLOBAL_URL_REGEX); 203 | 204 | if (!checkForMultipleUrls) { 205 | // TODO: figure out why this message can't self-destruct 206 | bot.replyInteractive(message, `Sorry, there don't seem to be any URLs in that message to add to the RSS Feed.`); 207 | } else { 208 | checkForMultipleUrls.forEach((foundUrl) => { 209 | const url = URL_REGEX.exec(foundUrl)[1]; 210 | 211 | controller.storage.links.find({ url, timestamp, channelId: channel.id }, (err, link) => { 212 | if (err) { 213 | console.error('ERROR:', err); 214 | } 215 | 216 | if (link.length) { 217 | bot.replyInteractive(message, `Sorry, that link has already been added to the feed.`); 218 | } else { 219 | scrape(url) 220 | .then(({ description, logo, image, video, audio, ...meta}) => { 221 | 222 | const date = Date.now(); 223 | const guid = shortId.generate(); 224 | 225 | let formattedDescription = (description) ? `

${description}

` : ''; 226 | 227 | if (originalMessage && originalMessage.text) { 228 | bot.api.users.info({ user: originalMessage.user }, (err, sharedBy) => { 229 | if (err) { 230 | console.error('ERROR:', 'Could not find user name for message'); 231 | } 232 | 233 | let user; 234 | 235 | if (sharedBy && sharedBy.user) { 236 | user = sharedBy.user.profile.display_name; 237 | } 238 | 239 | const formattedMessageText = originalMessage.text.replace(GLOBAL_URL_REGEX, '$1'); 240 | 241 | formattedDescription = `

From #${channel.name}:

${(user) ? `@${user}: ` : ''}${formattedMessageText}

${formattedDescription}`; 242 | 243 | formattedDescription = `${formattedDescription}

Read More

`; 244 | 245 | if (image) { 246 | formattedDescription = `

${formattedDescription}`; 247 | } 248 | 249 | const item = Object.assign({}, meta, { categories: [`#${channel.name}`], date, guid, description: formattedDescription }); 250 | 251 | const link = { 252 | id: guid, 253 | url, 254 | timestamp, 255 | teamId: bot.team_info.id, 256 | shareDate: date, 257 | channelName: channel.name, 258 | channelId: channel.id, 259 | item, 260 | }; 261 | 262 | controller.storage.links.save(link, function(err, id) { 263 | if (err) { 264 | debug('Error: could not save link record:', err); 265 | } 266 | 267 | updateFeed(controller, bot, bot.team_info.id, channel.id); 268 | 269 | bot.api.reactions.add({ channel: channel.id, name: 'book', timestamp }); 270 | }); 271 | }); 272 | } 273 | }) 274 | .catch((error) => { 275 | console.error('ERROR: error scraping url:', error); 276 | console.log(message); 277 | }) 278 | ; 279 | } 280 | }); 281 | }); 282 | } 283 | } 284 | }); 285 | } 286 | }); 287 | }); 288 | 289 | controller.on('interactive_message_callback', (bot, message) => { 290 | if (message.callback_id === 'ADD_TO_RSS') { 291 | const value = message.actions[0].value; 292 | 293 | if (value === 'NO') { 294 | bot.replyInteractive(message, { 295 | 'response_type': 'ephemeral', 296 | 'text': '', 297 | 'replace_original': true, 298 | 'delete_original': true 299 | }); 300 | 301 | processQueue(); 302 | } else { 303 | const [ timestamp, url ] = value.split(':|:'); 304 | 305 | getChannel(bot, message, (channelName, channelId) => { 306 | bot.api.channels.history({ token: bot.config.bot.app_token, channel: channelId, latest: timestamp, count: 1, inclusive: true }, (err, messageResponse) => { 307 | if (err) { 308 | console.error('ERROR: ', err); 309 | 310 | // TODO: add ephemeral error message for user 311 | 312 | return; 313 | } 314 | 315 | bot.replyInteractive(message, `Adding to RSS Feed...`); 316 | 317 | scrape(url) 318 | .then(({ description, logo, image, video, audio, ...meta}) => { 319 | 320 | const date = Date.now(); 321 | const guid = shortId.generate(); 322 | 323 | let formattedDescription = (description) ? `

${description}

` : ''; 324 | 325 | const originalMessage = messageResponse.messages[0]; 326 | 327 | if (originalMessage && originalMessage.text) { 328 | bot.api.users.info({ user: originalMessage.user }, (err, sharedBy) => { 329 | if (err) { 330 | console.error('ERROR:', 'Could not find user name for message'); 331 | } 332 | 333 | let user; 334 | 335 | if (sharedBy && sharedBy.user) { 336 | user = sharedBy.user.profile.display_name; 337 | } 338 | 339 | const formattedMessageText = originalMessage.text.replace(GLOBAL_URL_REGEX, '$1'); 340 | 341 | formattedDescription = `

From #${channelName}:

${(user) ? `@${user}: ` : ''}${formattedMessageText}

${formattedDescription}`; 342 | 343 | formattedDescription = `${formattedDescription}

Read More

`; 344 | 345 | if (image) { 346 | formattedDescription = `

${formattedDescription}`; 347 | } 348 | 349 | const item = Object.assign({}, meta, { categories: [`#${channelName}`], date, guid, description: formattedDescription }); 350 | 351 | const link = { 352 | id: guid, 353 | url, 354 | timestamp, 355 | teamId: bot.team_info.id, 356 | shareDate: date, 357 | channelName, 358 | channelId, 359 | item, 360 | }; 361 | 362 | controller.storage.links.save(link, function(err, id) { 363 | if (err) { 364 | debug('Error: could not save link record:', err); 365 | } 366 | 367 | let timeoutInterval = 1500; 368 | let nextIteration; 369 | 370 | bot.replyInteractive(message, `:+1: I've added this link to the <${getFeed(bot.team_info.id, channelId)}|#${channelName} RSS Feed>.`); 371 | 372 | bot.api.reactions.add({ channel: message.channel, name: 'book', timestamp }); 373 | 374 | if (URLS_TO_ADD.length) { 375 | timeoutInterval = 500; 376 | 377 | nextIteration = processQueue(); 378 | } 379 | 380 | setTimeout(function() { 381 | bot.replyInteractive(message, { 382 | 'response_type': 'ephemeral', 383 | 'text': '', 384 | 'replace_original': true, 385 | 'delete_original': true 386 | }); 387 | 388 | if (typeof nextIteration === 'function') { 389 | nextIteration(); 390 | } else { 391 | updateFeed(controller, bot, bot.team_info.id, channelId); 392 | } 393 | }, timeoutInterval); 394 | }); 395 | }); 396 | } 397 | }) 398 | .catch((error) => { 399 | console.error('ERROR: error scraping url:', error); 400 | console.log(message); 401 | }) 402 | ; 403 | }); 404 | }); 405 | } 406 | } 407 | }); 408 | } 409 | -------------------------------------------------------------------------------- /utils/delete-team-data.js: -------------------------------------------------------------------------------- 1 | module.exports = function(controller, teamId) { 2 | controller.storage.teams.delete(teamId, (err) => { 3 | if (err) { 4 | console.error(`ERROR: could not delete team ${teamId}:`, err); 5 | } 6 | 7 | controller.storage.feeds.find({}, (err, feeds) => { 8 | if (err) { 9 | console.error(`ERROR: could not find feeds for team ${teamId}`, err); 10 | } 11 | 12 | feeds.forEach((feed) => { 13 | if (feed.id.split('::')[0] === teamId) { 14 | controller.storage.feeds.delete(feed.id, (err) => { 15 | if (err) { 16 | console.error(`ERROR: could not delete feed ${teamId}::${feed.id}:`, err); 17 | } else { 18 | if (process.env.ANALYTICS === 'TRUE') { 19 | const pingFeedRequest = { 20 | method: 'GET', 21 | url: `${process.env.FEEDPRESS_API_URL}/feeds/ping.json`, 22 | data: stringify({ 23 | feed: `${teamId}-${channelId}`, 24 | }), 25 | params: { 26 | key: process.env.FEEDPRESS_API_KEY, 27 | token: process.env.FEEDPRESS_API_TOKEN, 28 | feed: `${teamId}-${channelId}`, 29 | }, 30 | }; 31 | 32 | axios(pingFeedRequest) 33 | .then(({ data }) => { 34 | if (data.errors && data.errors.length) { 35 | data.errors.forEach((err) => console.error('ERROR:', err)); 36 | } 37 | }) 38 | .catch((error) => { 39 | console.error('ERROR: could not ping feed for refresh:', error); 40 | }) 41 | ; 42 | } 43 | } 44 | }); 45 | } 46 | }); 47 | }); 48 | 49 | controller.storage.links.find({ teamId }, (err, links) => { 50 | if (err) { 51 | console.error(`ERROR: could not delete links for team ${teamId}:`, err); 52 | } 53 | 54 | links.forEach((link) => { 55 | controller.storage.links.delete(link.id, (err) => { 56 | if (err) { 57 | console.error(`ERROR: could not delete link ${teamId}::${link.id}`); 58 | } 59 | }); 60 | }); 61 | }); 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /utils/get-channel-name.js: -------------------------------------------------------------------------------- 1 | module.exports = function(bot, message, callback) { 2 | bot.api.channels.info({ channel: message.channel }, function(err, response) { 3 | if (err) { 4 | console.error('ERROR: could not retrieve channel info', err); 5 | } 6 | 7 | if (response.channel) { 8 | callback(response.channel.name, response.channel.id); 9 | } else { 10 | callback(); 11 | } 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /utils/get-domain.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return process.env.APP_DOMAIN || `${process.env.HEROKU_APP_NAME}.herokuapp.com`; 3 | } 4 | -------------------------------------------------------------------------------- /utils/get-feed-url.js: -------------------------------------------------------------------------------- 1 | const getFeed = require('./rss-link'); 2 | 3 | module.exports = (teamId, channelId) => { 4 | if (!teamId || !channelId) { 5 | return ''; 6 | } 7 | 8 | const usingFeedPress = (process.env.ANALYTICS === 'TRUE'); 9 | const usingHostName = (usingFeedPress && process.env.FEEDPRESS_HOSTNAME); 10 | 11 | return (usingHostName) ? `${process.env.FEEDPRESS_HOSTNAME}/${teamId}-${channelId}` : getFeed(teamId, channelId); 12 | }; 13 | -------------------------------------------------------------------------------- /utils/rss-link.js: -------------------------------------------------------------------------------- 1 | const getDomain = require('./get-domain'); 2 | 3 | module.exports = function(teamId, channelId = '') { 4 | if (!teamId || !channelId) { 5 | return ''; 6 | } 7 | 8 | return `https://${getDomain()}/feed/${teamId}/${channelId}`; 9 | } 10 | -------------------------------------------------------------------------------- /utils/scrape.js: -------------------------------------------------------------------------------- 1 | const metascraper = require('metascraper')([ 2 | require('metascraper-youtube')(), 3 | require('metascraper-soundcloud')(), 4 | require('metascraper-amazon')(), 5 | require('metascraper-author')(), 6 | require('metascraper-date')(), 7 | require('metascraper-description')(), 8 | require('metascraper-image')(), 9 | require('metascraper-clearbit-logo')(), 10 | require('metascraper-logo')(), 11 | require('metascraper-logo-favicon')(), 12 | require('metascraper-publisher')(), 13 | require('metascraper-title')(), 14 | require('metascraper-url')(), 15 | ]); 16 | 17 | const got = require('got'); 18 | 19 | const URL_REGEX = require('../vendor/regex-weburl'); 20 | 21 | module.exports = async (target = '') => { 22 | if (target.length && URL_REGEX.test(target)) { 23 | console.log(`Scraping ${target}...`); 24 | 25 | const { body: html, url } = await got(target); 26 | 27 | const meta = await metascraper({ html, url }); 28 | 29 | return meta; 30 | } else { 31 | console.error(`ERROR: Could not scrape ${target}...`); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /utils/update-feed.js: -------------------------------------------------------------------------------- 1 | const RSS = require('rss'); 2 | const axios = require('axios'); 3 | const { stringify } = require('qs'); 4 | 5 | const getDefaultFeed = require('./rss-link'); 6 | const getFeed = require('./get-feed-url'); 7 | const getDomain = require('./get-domain'); 8 | 9 | const NEW_SORT = -1; 10 | const OLD_SORT = 1; 11 | 12 | const FEED_SORT = NEW_SORT; // -1 Newest First; 1 Oldest First 13 | const FEED_ITEMS = 10; 14 | 15 | // This is a gross hack until botkit-storage-mongo supports passing options for 16 | // our collection.find() query to sort and limit our queries 17 | const trimAndSortLinks = (links, sort = {}, limit = FEED_ITEMS) => { 18 | let newLinks = [...links]; 19 | 20 | if (Object.keys(sort).length) { 21 | const [ sortField, sortOrder ] = Object.entries(sort)[0]; 22 | 23 | newLinks = newLinks.sort((a, b) => (sortOrder === NEW_SORT) ? b[sortField] - a[sortField] : a[sortField] - b[sortField]); 24 | } 25 | 26 | if (limit) { 27 | return newLinks.splice(0, limit); 28 | } 29 | 30 | return newLinks; 31 | }; 32 | 33 | const getPubDate = (links) => { 34 | const newestIndex = (FEED_SORT === OLD_SORT) ? links.length - 1 : 0; 35 | 36 | return (links.length) ? links[newestIndex].shareDate : Date.now(); 37 | }; 38 | 39 | module.exports = (controller, bot, teamId, channelId) => { 40 | const plainFeedUrl = getDefaultFeed(teamId, channelId); 41 | const feedUrl = getFeed(teamId, channelId); 42 | 43 | bot.api.channels.info({ channel: channelId }, function(err, { channel }) { 44 | controller.storage.feeds.get(`${teamId}::${channelId}`, (err, oldFeed) => { 45 | const postsQuery = { 46 | teamId, 47 | channelId, 48 | }; 49 | 50 | controller.storage.links.find(postsQuery, function(err, feedLinks) { 51 | if (err) { 52 | console.error('ERROR: could not retrieve links for feed:', err); 53 | 54 | return; 55 | } 56 | 57 | const links = trimAndSortLinks(feedLinks, { shareDate: FEED_SORT }, FEED_ITEMS); 58 | 59 | // gather categories from all posts 60 | // concat them into a single Array 61 | // convert them to a Set to deduplicate the Array 62 | // convert the Set back to an Array because that's what we actually need 63 | // NOTE: at present, only the #channel is used as a category, but we 64 | // may need to support other categories in the future 65 | const categories = (links.length) ? [...new Set([].concat.apply([], links.map(({ item: { categories }}) => categories)))] : []; 66 | 67 | controller.storage.teams.get(teamId, function(err, team) { 68 | const { url: teamUrl, name: teamName } = team; 69 | 70 | const feedTitle = `#${channel.name} ${teamName} Slack RSS Feed`; 71 | 72 | const feedDescription = `Links posted in the #${channel.name} channel in the ${teamName} Slack and gathered by @RSS bot.`; 73 | 74 | const pubDate = getPubDate(links); 75 | 76 | const feed = new RSS({ 77 | title: feedTitle, 78 | description: feedDescription, 79 | feed_url: plainFeedUrl, 80 | site_url: teamUrl, 81 | categories, 82 | pubDate, 83 | ttl: 60, 84 | language: 'en-US', 85 | custom_namespaces: { 86 | 'webfeeds': 'http://webfeeds.org/rss/1.0', 87 | 'sy': 'http://web.resource.org/rss/1.0/modules/syndication/', 88 | }, 89 | custom_elements: [ 90 | { 'webfeeds:logo': `${getDomain()}/feed-logo.svg` }, 91 | { 'webfeeds:accentColor': '#2F6C8F' }, 92 | { 'sy:updatePeriod': 'hourly' }, 93 | { 'sy:updateFrequency': 1 }, 94 | ] 95 | }); 96 | 97 | feed.generator = `Aggregated by https://www.rssbot.app | ${feed.generator}`; 98 | 99 | links.forEach((link) => { 100 | feed.item(link.item); 101 | }); 102 | 103 | const feedXml = feed.xml(); 104 | 105 | const cache = { 106 | id: `${teamId}::${channelId}`, 107 | url: feedUrl, 108 | feed: feedXml, 109 | }; 110 | 111 | controller.storage.feeds.save(cache, function(err, id) { 112 | if (err) { 113 | console.error('ERROR: could not cache feed:', err); 114 | } 115 | 116 | console.log(`${teamId}::${channelId} #${channel.name} feed cached.`); 117 | 118 | // clean up old links 119 | feedLinks.forEach((link) => { 120 | if (links.indexOf(link) === -1) { 121 | controller.storage.links.delete(link.id, (err) => { 122 | if (err) { 123 | console.error('ERROR: could not delete old link:', err); 124 | } 125 | }); 126 | } 127 | }); 128 | 129 | if (!oldFeed) { 130 | console.log(`No previous feed found for ${teamId}::${channelId} #${channel.name}...`); 131 | 132 | if (process.env.ANALYTICS === 'TRUE') { 133 | console.log(`Registering ${teamId}::${channelId} #${channel.name} feed with FeedPress...`); 134 | 135 | const createFeedRequest = { 136 | method: 'POST', 137 | baseURL: process.env.FEEDPRESS_API_URL, 138 | url: '/feeds/create.json', 139 | params: { 140 | key: process.env.FEEDPRESS_API_KEY, 141 | token: process.env.FEEDPRESS_API_TOKEN, 142 | alias: `${teamId}-${channelId}`, 143 | url: plainFeedUrl, 144 | }, 145 | data: stringify({ 146 | url: plainFeedUrl, 147 | alias: `${teamId}-${channelId}`, 148 | }), 149 | }; 150 | 151 | axios(createFeedRequest) 152 | .then((response) => { 153 | const { data } = response; 154 | 155 | if (data.errors && data.errors.length) { 156 | data.errors.forEach((err) => console.error('ERROR:', err)); 157 | console.log(response); 158 | } else { 159 | console.log(data); 160 | } 161 | 162 | if (process.env.FEEDPRESS_HOSTNAME && process.env.FEEDPRESS_HOSTNAME_ID) { 163 | console.log(`Registering ${process.env.FEEDPRESS_HOSTNAME}/${teamId}/${channelId} with FeedPress...`); 164 | 165 | const createHostnameRequest = { 166 | method: 'POST', 167 | url: `${process.env.FEEDPRESS_API_URL}/hostnames/record.json`, 168 | data: stringify({ 169 | hostname_id: process.env.FEEDPRESS_HOSTNAME_ID, 170 | type: 'feed', 171 | destination: `${teamId}-${channelId}`, 172 | path: `/${teamId}-${channelId}` 173 | }), 174 | params: { 175 | key: process.env.FEEDPRESS_API_KEY, 176 | token: process.env.FEEDPRESS_API_TOKEN, 177 | hostname_id: process.env.FEEDPRESS_HOSTNAME_ID, 178 | path: `/${teamId}-${channelId}`, 179 | type: 'feed', 180 | destination: `${teamId}-${channelId}`, 181 | }, 182 | }; 183 | 184 | axios(createHostnameRequest) 185 | .then(({ data }) => { 186 | if (data.errors && data.errors.length) { 187 | data.errors.forEach((err) => console.error('ERROR:', err)); 188 | } 189 | }) 190 | .catch((error) => { 191 | console.error('ERROR: Could not register feed with hostname:', error); 192 | }) 193 | ; 194 | } 195 | }) 196 | .catch((error) => { 197 | console.error('ERROR: Could not create feed with FeedPress:', error); 198 | }) 199 | ; 200 | } 201 | } else { 202 | const pingFeedRequest = { 203 | method: 'GET', 204 | url: `${process.env.FEEDPRESS_API_URL}/feeds/ping.json`, 205 | data: stringify({ 206 | feed: `${teamId}-${channelId}`, 207 | }), 208 | params: { 209 | key: process.env.FEEDPRESS_API_KEY, 210 | token: process.env.FEEDPRESS_API_TOKEN, 211 | feed: `${teamId}-${channelId}`, 212 | }, 213 | }; 214 | 215 | axios(pingFeedRequest) 216 | .then(({ data }) => { 217 | if (data.errors && data.errors.length) { 218 | data.errors.forEach((err) => console.error('ERROR:', err)); 219 | } 220 | }) 221 | .catch((error) => { 222 | console.error('ERROR: could not ping feed for refresh:', error); 223 | }) 224 | ; 225 | } 226 | }); 227 | }); 228 | }); 229 | }); 230 | }); 231 | }; 232 | -------------------------------------------------------------------------------- /vendor/regex-weburl.js: -------------------------------------------------------------------------------- 1 | // borrowed from: https://gist.github.com/dperini/729294 2 | 3 | // 4 | // Regular Expression for URL validation 5 | // 6 | // Author: Diego Perini 7 | // Created: 2010/12/05 8 | // Updated: 2018/09/12 9 | // License: MIT 10 | // 11 | // Copyright (c) 2010-2018 Diego Perini (http://www.iport.it) 12 | // 13 | // Permission is hereby granted, free of charge, to any person 14 | // obtaining a copy of this software and associated documentation 15 | // files (the "Software"), to deal in the Software without 16 | // restriction, including without limitation the rights to use, 17 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | // copies of the Software, and to permit persons to whom the 19 | // Software is furnished to do so, subject to the following 20 | // conditions: 21 | // 22 | // The above copyright notice and this permission notice shall be 23 | // included in all copies or substantial portions of the Software. 24 | // 25 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 27 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 29 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 30 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 31 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 32 | // OTHER DEALINGS IN THE SOFTWARE. 33 | // 34 | // the regular expression composed & commented 35 | // could be easily tweaked for RFC compliance, 36 | // it was expressly modified to fit & satisfy 37 | // these test for an URL shortener: 38 | // 39 | // http://mathiasbynens.be/demo/url-regex 40 | // 41 | // Notes on possible differences from a standard/generic validation: 42 | // 43 | // - utf-8 char class take in consideration the full Unicode range 44 | // - TLDs have been made mandatory so single names like "localhost" fails 45 | // - protocols have been restricted to ftp, http and https only as requested 46 | // 47 | // Changes: 48 | // 49 | // - IP address dotted notation validation, range: 1.0.0.0 - 223.255.255.255 50 | // first and last IP address of each class is considered invalid 51 | // (since they are broadcast/network addresses) 52 | // 53 | // - Added exclusion of private, reserved and/or local networks ranges 54 | // - Made starting path slash optional (http://example.com?foo=bar) 55 | // - Allow a dot (.) at the end of hostnames (http://example.com.) 56 | // - Allow an underscore (_) character in host/domain names 57 | // - Check dot delimited parts length and total length 58 | // - Made protocol optional, allowed short syntax // 59 | // 60 | // Compressed one-line versions: 61 | // 62 | // Javascript regex version 63 | // 64 | // /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i 65 | // 66 | // PHP version (uses % symbol as delimiter) 67 | // 68 | // %^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\x{00a1}-\x{ffff}][a-z0-9\x{00a1}-\x{ffff}_-]{0,62})?[a-z0-9\x{00a1}-\x{ffff}]\.)+(?:[a-z\x{00a1}-\x{ffff}]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$%iuS 69 | // 70 | module.exports = new RegExp( 71 | "^" + 72 | // protocol identifier (optional) 73 | // short syntax // still required 74 | "(?:(?:(?:https?|ftp):)?\\/\\/)" + 75 | // user:pass BasicAuth (optional) 76 | "(?:\\S+(?::\\S*)?@)?" + 77 | "(?:" + 78 | // IP address exclusion 79 | // private & local networks 80 | "(?!(?:10|127)(?:\\.\\d{1,3}){3})" + 81 | "(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" + 82 | "(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" + 83 | // IP address dotted notation octets 84 | // excludes loopback network 0.0.0.0 85 | // excludes reserved space >= 224.0.0.0 86 | // excludes network & broacast addresses 87 | // (first & last IP address of each class) 88 | "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + 89 | "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + 90 | "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + 91 | "|" + 92 | // host & domain names, may end with dot 93 | // can be replaced by a shortest alternative 94 | // (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+ 95 | "(?:" + 96 | "(?:" + 97 | "[a-z0-9\\u00a1-\\uffff]" + 98 | "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + 99 | ")?" + 100 | "[a-z0-9\\u00a1-\\uffff]\\." + 101 | ")+" + 102 | // TLD identifier name, may end with dot 103 | "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + 104 | ")" + 105 | // port number (optional) 106 | "(?::\\d{2,5})?" + 107 | // resource path (optional) 108 | "(?:[/?#]\\S*)?" + 109 | "$", "i" 110 | ); 111 | -------------------------------------------------------------------------------- /views/404.hbs: -------------------------------------------------------------------------------- 1 |

2 |

Sorry, @RSS bot couldn't find what you're looking for.

3 |
4 | 7 | -------------------------------------------------------------------------------- /views/500.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Sorry, @RSS bot encountered an issue and can't respond right now.

3 |
4 | 7 | -------------------------------------------------------------------------------- /views/help.hbs: -------------------------------------------------------------------------------- 1 |
2 |

@RSS bot Support

3 |
4 | 25 | -------------------------------------------------------------------------------- /views/index.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Hi there! I make RSS feeds for Slack Channels.

3 |
4 |
5 |

Tired of missing links in your Slack Channels? Just add me to a channel and whenever a user shares a link they'll be asked if they want to add it to the channel's RSS Feed.

6 |

Once you add the feed to your RSS Reader of choice, you'll never miss a link again.

7 |

Get @RSS bot

8 |

Use the button below to add @RSS bot to your Slack workspace.

9 |

10 | 11 | Add to Slack 12 | 13 |

14 |

Need an RSS Reader?

15 |

Here are some popular choices for reading your RSS feeds:

16 | 25 |
26 | -------------------------------------------------------------------------------- /views/installation.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Botkit Starter Kit

3 |
4 | 5 |
6 |

7 | ✅ Your Botkit app is correctly deployed and running! 8 | Connect it to Slack to complete the setup. 9 |

10 | 11 | 12 |
13 | 14 |
15 |

Create a Slack Application

16 | 17 |

18 | To bring this bot online, create a new Slack application on the Slack developer site. 19 | You will get a client id and client secret. 20 | Make sure to copy and paste those values below for safe keeping. 21 |

22 | Open Slack developer site 23 | 24 |

25 | 26 | 27 |

28 | 29 |

30 | 31 | 32 |

33 | 34 |
35 | 36 |
37 | 38 |

Configure Features

39 | 40 |

41 | Botkit uses 4 features of Slack's API: Bot Users, Interactive Components, Event Subscriptions, and OAuth. 42 | You will need to configure each. 43 |

44 | 45 |

1. Bot User

46 | 47 |

48 | Click on the Slack application's "Bot Users" tab. Enter a Display name and a default username. 49 | Also enable the option labeled "Always Show My Bot as Online." 50 | Make sure to save your changes. 51 |

52 | 53 |

54 | This option will create a username and identity for your bot so that it can join channels 55 | and appear as a robotic team member. 56 |

57 | 58 |
59 | 60 |

2. Interactive Components

61 | 62 |

63 | Click on the Slack application's "Interactive Components" tab. 64 | Enter this URL in the "Request URL" field, and then save your changes. 65 |

66 | 67 |

68 | This option will allow your bot to send and receive interactive messages 69 | with buttons, menus and dialog boxes. 70 |

71 | 72 |
73 | 74 |
75 | 76 | 77 |
78 |
79 | 80 |
81 | 82 |

3. Event Subscriptions

83 | 84 |

85 | Click on the Slack application's "Event Subscriptions" tab. 86 | Click to enable events, then specify this URL in the "Request URL" field. 87 |

88 | 89 |
90 | 91 |
92 | 93 | 94 |
95 |
96 | 97 | 98 |

99 | After setting the URL, scroll down to the "Subscribe to Bot Events" section. 100 | Here, you will select 4 different messaging events:

101 | 102 |
    103 |
  1. message.channels
  2. 104 |
  3. message.groups
  4. 105 |
  5. message.im
  6. 106 |
  7. message.mpim
  8. 107 |
108 | 109 |

110 | You may also want to enable other events, but these 4 are required for your bot send and receive basic messages. 111 |

112 | 113 |
114 | 115 |

4. OAuth & Permissions

116 | 117 |

118 | Click on the Slack application's "OAuth & Permissions" tab. 119 | Scroll down to "Redirect URLs" and click "Add a new Redirect URL." 120 | Enter this URL, and click "Save URLs" 121 | 122 |

123 | 124 |
125 | 126 | 127 |
128 |
129 | 130 |
131 | 132 |

You are now finished with the Slack developer site! You can close that tab.

133 | 134 | 135 |
136 | 137 |
138 | 139 |

Edit the .env File

140 | 141 |

142 | The final step that will bring your bot online is to add the client id and client secret collected 143 | in step 1 to this projects .env file. 144 | Be careful not to change the other lines in this file! 145 |

146 | 147 |
148 | 149 |
150 | 151 | 152 |
153 |
154 | 155 | {{#glitch_domain}} 156 |

Edit your .env file

157 | {{/glitch_domain}} 158 |
159 | 160 |
161 |

Ready to Connect!

162 | 163 |

164 | Once you have values in your .env file, and Slack has been configured correctly, your bot is ready to connect. 165 | Restart this application and reload this page - you'll see an "Add to Slack" button that will install your new bot! 166 |

167 | 168 |
169 | 170 | 171 | 172 | 189 | -------------------------------------------------------------------------------- /views/layouts/default.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{> head}} 4 | 5 | {{>header}} 6 | {{{body}}} 7 | {{> footer}} 8 | 9 | 10 | -------------------------------------------------------------------------------- /views/partials/footer.hbs: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /views/partials/head.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#if (processEnv 'ANALYTICS' 'TRUE' false)}} 3 | {{#if (processEnv 'GOOGLE_ANALYTICS_ID' '' true)}} 4 | 5 | 6 | 13 | {{/if}} 14 | {{/if}} 15 | 16 | 17 | 18 | 19 | @RSS Bot | A Slackbot that generates RSS Feeds 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /views/partials/header.hbs: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /views/privacy.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Privacy

3 |
4 |
5 |

@RSS bot only gathers information that you explicitly share with it via Slack.

6 |

Your team ID and channel ID are used to construct the URL for each channel's feed.

7 |

RSS feeds are publicly-accessible to anyone with the feed URL.

8 |

The display name of the user who originally shared the link will appear in the RSS feed entry for that link along with any text they posted with the link.

9 |

Our RSS feeds use analytics tracking via FeedPress to keep track of subscriber counts. We do not store any analytics data from FeedPress.

10 |

We only store the last 10 links shared in each channel.

11 | {{#if (processEnv 'ANALYTICS' 'TRUE' false)}} 12 | {{#if (processEnv 'GOOGLE_ANALYTICS_ID' '' true)}} 13 |

This site uses Google Analytics to keep track of traffic numbers.

14 | {{/if}} 15 | {{/if}} 16 |

Questions?

17 |
    18 |
  1. Email Support
  2. 19 |
  3. Tweet @RSSSlackBot
  4. 20 |
  5. File a GitHub Issue
  6. 21 |
22 |
23 | --------------------------------------------------------------------------------