├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── bot.py ├── pb_logging.py ├── reactbot.py ├── requirements.txt ├── response.py ├── runtime.txt ├── scripts ├── __init__.py ├── admin.py ├── catfact.py ├── coin.py ├── conch.py ├── flip.py ├── fortune.py ├── helloworld.py ├── help.py ├── poll.py ├── pugbomb.py ├── rage.py ├── talk.py └── unflip.py ├── start.py └── user_greeting.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /logs/* 2 | /secrets/* 3 | /config/* 4 | *.pyc 5 | # swap 6 | [._]*.s[a-v][a-z] 7 | [._]*.sw[a-p] 8 | [._]s[a-v][a-z] 9 | [._]sw[a-p] 10 | # session 11 | Session.vim 12 | # temporary 13 | .netrwhist 14 | *~ 15 | # auto-generated tag files 16 | tags 17 | database.db 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python start.py 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PantherBot 2 | PantherBot is a WebSocket Application that takes advantage of Slack's RTM and Web API to interact with the channels that it is a part of. 3 | 4 | # Using PantherBot 5 | PantherBot currently has a few commands available to use. These are broken up into action commands and logging commands. 6 | All action commands are prefaced with the "!" marker (ie. `!coin`). 7 | Logging commands are prefaced with the "$" marker (ie. `$log`). 8 | Some responses are hard coded to certain phrases (ie. `Hey PantherBot`). 9 | 10 | Current list of action commands: 11 | ``` 12 | !help 13 | !coin 14 | !helloworld 15 | !fortune 16 | !catfact 17 | !pugbomb 18 | !taskme 19 | !flip/!rage/!unflip 20 | ``` 21 | 22 | Current list of logging commands: 23 | ``` 24 | $log 25 | $admin 26 | $calendar add ; ; <Date in format YYYY-MM-DD> ; <Start time in format HH:mm> ; <End time in format HH:mm> ; <Description> ; <Location> 27 | ``` 28 | 29 | To use these commands, in any channel that PantherBot is both present in and may post, start your message with one of the above commands and fill in the arguments as necessary 30 | 31 | PantherBot also responds to a few custom messages as well, currently set to: 32 | ``` 33 | Hey PantherBot 34 | PantherBot ping 35 | rip PantherBot 36 | ``` 37 | As well as messaging them a custom message upon joining the Slack team. 38 | 39 | # Installing Dependencies 40 | PantherBot requires several python libraries to function. These can be easily installed with the `setup.bat` or `setup.sh` file. 41 | `setup.sh` should be run with elevated privileges to ensure dependencies install correctly (this is an issue for most unless you're using a virtualenv) (`sudo sh setup.sh` should be good enough). 42 | Likewise, pip may request your permission or a prompt if using the `setup.bat` file. If it fails, try using administrative privileges. 43 | 44 | # Setting up PantherBot 45 | PantherBot is currently relatively easy to set up in your Slack team. Follow the instructions below and it'll be up and running in no time! 46 | 47 | 1. Install [Python 2.7](https://www.python.org/downloads/) if not installed previously. 48 | 2. Run `setup.bat` (for Windows) or `setup.sh` (for macOS/Linux) to install dependencies and create the necessary folders. 49 | 3. Go to your Slack team settings and set up a bot configuration, be sure to give it some information, including a username! Copy the API token. 50 | 4. In the `secrets` folder, make a file called `slack_secret.txt` that contains **only** your API token. 51 | 5. Depending on your OS, run either `start.bat` (Windows) or `start.sh` (macOS and Linux). 52 | 6. The bot should become active in the slack team. Invite the bot into the channels you would like it to monitor (using the `/invite @username` command), and off it goes surveying the world! 53 | 7. If there are errors, consider re-running `setup.bat` or `setup.sh` in a higher privileged environment to be sure everything is installed properly. 54 | 9. The bot is set up. If you want to edit anything (like the posting name or icon), edit the config folder files to edit things such as: 55 | - `BOT_NAME`, `BOT_ICON_URL` (in bot.txt) 56 | - `SLACK`, `GOOGLECAL`, `LOGGING`, `GOOGLECALSECRET`, `NEWUSERGREETING`, `GREETING` (in settings.txt) 57 | - `ADMIN` (in admin.txt) 58 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module runs basically everything. 4 | 5 | Attributes: 6 | VERSION = "2.0.0" (String): Version Number: release.version_num.revision_num 7 | 8 | # Config Variables 9 | EMOJI_LIST (List): List of Strings for emojis to be added to announcements 10 | USER_LIST (JSON): List of users in JSON format 11 | TTPB (String): Config variable, sets channel for cleverbot integration 12 | GENERAL (Stirng): Config variable, sets channel for general's name 13 | LOG (boolean): Global Variable 14 | LOGC (boolean): Global Variable 15 | pbCooldown (int): Global Variable 16 | 17 | .. _Google Python Style Guide: 18 | http://google.github.io/styleguide/pyguide.html 19 | 20 | """ 21 | 22 | from slackclient import SlackClient 23 | 24 | import threading, websocket, json, re, time, codecs, random, os 25 | import scripts 26 | from scripts import commands 27 | 28 | from pb_logging import PBLogger 29 | import logging 30 | 31 | logger = PBLogger('Bot') 32 | 33 | class Bot(object): 34 | 35 | EMOJI_LIST = ["party-parrot", "venezuela-parrot", "star2", "fiesta-parrot", "wasfi_dust", "dab"] 36 | GENERAL_CHANNEL = "" 37 | TTPB = "talk-to-pantherbot" 38 | VERSION = "2.1.0" 39 | 40 | def __init__(self, token, bot_name=""): 41 | self.SLACK_CLIENT = None 42 | self.BOT_NAME = bot_name 43 | self.BOT_ICON_URL = "http://i.imgur.com/QKaLCX7.png" 44 | self.USER_LIST = None 45 | self.POLLING_LIST = dict() 46 | self.WEBSOCKET = None 47 | self.THREADS = [] 48 | self.COMMANDS_LIST = commands 49 | 50 | self.pb_cooldown = True 51 | # self.check_in_thread_ts 52 | 53 | self.create_slack_client(token) 54 | 55 | def create_slack_client(self, token): 56 | self.SLACK_CLIENT = SlackClient(token) 57 | 58 | # Returns a list of channel IDs searched for by name 59 | def channels_to_ids(self, channel_names): 60 | pub_channels = self.SLACK_CLIENT.api_call( 61 | "channels.list", 62 | exclude_archived=1 63 | ) 64 | pri_channels = self.SLACK_CLIENT.api_call( 65 | "groups.list", 66 | exclude_archived=1 67 | ) 68 | list_of_channels = [] 69 | for channel in pub_channels["channels"]: 70 | for num in range(0, len(channel_names)): 71 | if channel["name"].lower() == channel_names[num].lower(): 72 | list_of_channels.append(channel["id"]) 73 | # Same as above 74 | for channel in pri_channels["groups"]: 75 | for num in range(0, len(channel_names)): 76 | if channel["name"].lower() == channel_names[num].lower(): 77 | list_of_channels.append(channel["id"]) 78 | return list_of_channels 79 | 80 | # Send Message 81 | # Sends a message to the specified channel (looks up by channel name, unless is_id is True) 82 | def send_msg(self, channel, text, is_id=False, thread_ts=None): 83 | if is_id: 84 | channel_id = channel 85 | else: 86 | channel_id = self.channels_to_ids([channel])[0] 87 | if thread_ts is not None: 88 | response_json = self.SLACK_CLIENT.api_call( 89 | "chat.postMessage", 90 | channel=channel_id, 91 | text=text, 92 | username=self.BOT_NAME, 93 | icon_url=self.BOT_ICON_URL, 94 | thread_ts=thread_ts 95 | ) 96 | else: 97 | response_json = self.SLACK_CLIENT.api_call( 98 | "chat.postMessage", 99 | channel=channel_id, 100 | text=text, 101 | username=self.BOT_NAME, 102 | icon_url=self.BOT_ICON_URL 103 | ) 104 | logger.info("Message sent: ") 105 | return response_json 106 | 107 | def emoji_reaction(self, channel, ts, emoji): 108 | self.SLACK_CLIENT.api_call( 109 | "reactions.add", 110 | name=emoji, 111 | channel=channel, 112 | timestamp=ts 113 | ) 114 | logger.info("Reaction posted: " + emoji) 115 | 116 | def close(self): 117 | self.WEBSOCKET.keep_running = False 118 | logger.info("Closing Websocket...") -------------------------------------------------------------------------------- /pb_logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class _PBLogHandler(logging.StreamHandler): 5 | def __init__(self): 6 | logging.StreamHandler.__init__(self) 7 | fmt = '%(asctime)s - %(name)s - [%(levelname)s] - %(message)s' 8 | format_date = '%m/%d/%Y %I:%M:%S %p' 9 | formatter = logging.Formatter(fmt, format_date) 10 | self.setFormatter(formatter) 11 | 12 | 13 | class PBLogger: 14 | def __init__(self, name): 15 | self._logger = logging.getLogger(name) 16 | self._logger.setLevel(logging.INFO) 17 | self._logger.addHandler(_PBLogHandler()) 18 | 19 | # Imitating logger object functionality 20 | 21 | def set_level(self, level): 22 | self._logger.setLevel(level) 23 | 24 | def debug(self, s): 25 | self._logger.debug(s) 26 | 27 | def info(self, s): 28 | self._logger.info(s) 29 | 30 | def warning(self, s): 31 | self._logger.warning(s) 32 | 33 | def error(self, s): 34 | self._logger.error(s) 35 | 36 | def critical(self, s): 37 | self._logger.critical(s) 38 | -------------------------------------------------------------------------------- /reactbot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module runs basically everything. 4 | 5 | Attributes: 6 | VERSION = "2.0.0" (String): Version Number: release.version_num.revision_num 7 | 8 | # Config Variables 9 | EMOJI_LIST (List): List of Strings for emojis to be added to announcements 10 | USER_LIST (JSON): List of users in JSON format 11 | ADMIN (List): ["U25PPE8HH", "U262D4BT6", "U0LAMSXUM", "U3EAHHF40"] testing defaults 12 | TTPB (String): Config variable, sets channel for cleverbot integration 13 | GENERAL (Stirng): Config variable, sets channel for general's name 14 | LOG (boolean): Global Variable 15 | LOGC (boolean): Global Variable 16 | 17 | .. _Google Python Style Guide: 18 | http://google.github.io/styleguide/pyguide.html 19 | 20 | """ 21 | 22 | from slackclient import SlackClient 23 | import threading, websocket, json, re, time, codecs, random, logging 24 | import scripts 25 | from bot import Bot 26 | 27 | from pb_logging import PBLogger 28 | 29 | logger = PBLogger('ReactBot') 30 | 31 | class ReactBot(Bot): 32 | 33 | # Set the name for the logger 34 | # Add custom log handler to logger 35 | 36 | def __init__(self, token, bot_name=""): 37 | super(ReactBot, self).__init__(token, bot_name) 38 | self.connect_to_slack(token) 39 | 40 | self.annoyance = 0 # Used for the "No, this is PantherHackers" response 41 | 42 | def connect_to_slack(self, token): 43 | # Initiates connection to the server based on the token, receives websocket URL "bot_conn" 44 | logger.info("Starting RTM connection") 45 | self.BOT_CONN = self.SLACK_CLIENT.api_call( 46 | "rtm.start", 47 | token = token 48 | ) 49 | 50 | logger.info("Initializing info") 51 | self.initialize_info() 52 | 53 | # Creates WebSocketApp based on the URL returned by the RTM API 54 | # Assigns local methods to websocket methods 55 | logger.info("Initializing WebSocketApplication") 56 | self.WEBSOCKET = websocket.WebSocketApp(self.BOT_CONN["url"], 57 | on_message=self.on_message, 58 | on_error=self.on_error, 59 | on_close=self.on_close, 60 | on_open=self.on_open) 61 | 62 | def initialize_info(self): 63 | # Update current USER_LIST (since members may join while PantherBot is off, its safe to make an API call every initial run) 64 | # When database is implemented, this should be sure to cross reference the database's list with this so new users are added. 65 | self.USER_LIST = self.SLACK_CLIENT.api_call( 66 | "users.list" 67 | ) 68 | 69 | # Obtains the User Greeting for new users from the stored file 70 | with open("user_greeting.txt") as file: 71 | self.USER_GREETING = file.read() 72 | 73 | # List of polls for all channels 74 | self.POLLING_LIST = {} 75 | pub_channels = self.SLACK_CLIENT.api_call( 76 | "channels.list", 77 | exclude_archived=1 78 | ) 79 | pri_channels = self.SLACK_CLIENT.api_call( 80 | "groups.list", 81 | exclude_archived=1 82 | ) 83 | for c in pub_channels["channels"]: 84 | # Sets channel polling option to an array of format ["", [], "none"] 85 | self.POLLING_LIST[c["id"]] = ["",[],"none"] 86 | for c in pri_channels["groups"]: 87 | self.POLLING_LIST[c["id"]] = ["",[],"none"] 88 | 89 | # Update current General Channel (usually announcements) 90 | temp_list_of_channels = Bot.channels_to_ids(self, ["announcements"]) 91 | if not temp_list_of_channels: 92 | temp_list_of_channels = Bot.channels_to_ids(self, ["general"]) 93 | try: 94 | if Bot.GENERAL_CHANNEL is "": 95 | Bot.GENERAL_CHANNEL = temp_list_of_channels[0] 96 | except: 97 | logger.warning("The #general channel has been renamed to a non-default value") 98 | 99 | def on_message(self, ws, message): 100 | message_thread = threading.Thread(target=self.on_message_thread, args=(message,)) 101 | self.THREADS.append(message_thread) 102 | message_thread.start() 103 | return 104 | 105 | def on_message_thread(self, message): 106 | s = message.decode('utf-8') 107 | message_json = json.loads(unicode(s)) 108 | 109 | logger.info(message_json["type"].replace("_"," ").title()) 110 | 111 | try: 112 | if Bot.GENERAL_CHANNEL == message_json["channel"]: 113 | if "member_joined_channel" == message_json["type"]: 114 | self.message_user(message_json["user"], self.USER_GREETING) 115 | except: 116 | pass 117 | 118 | 119 | if "message" == message_json["type"]: 120 | if "subtype" in message_json: 121 | if message_json["subtype"] == "bot_message": 122 | #polling logic 123 | if "POLL_BEGIN" in message_json["text"]: 124 | self.POLLING_LIST[message_json["channel"]][0] = message_json["ts"] 125 | temp_options = self.POLLING_LIST[message_json["channel"]][1] 126 | for key in temp_options: 127 | Bot.emoji_reaction(self, message_json, temp_options[key].strip(":")) 128 | return 129 | 130 | # Announcement reactions 131 | self.react_announcement(message_json) 132 | 133 | # General commands 134 | # if riyansDenial(message_json): 135 | # return 136 | if self.other_message(message_json): 137 | return 138 | if self.command_message(message_json): 139 | return 140 | if self.admin_message(message_json): 141 | return 142 | 143 | def on_error(self, ws, error): 144 | return 145 | 146 | def on_close(self, ws): 147 | return 148 | 149 | def on_open(self, ws): 150 | return 151 | 152 | # message_json Message 153 | # Sends a message to the same channel that message_json originates from 154 | # Provide a username and icon URL to override the default ones 155 | def response_message(self, message_json, l, username=None, icon_url=None): 156 | for text in l: 157 | self.SLACK_CLIENT.api_call( 158 | "chat.postMessage", 159 | channel=message_json["channel"], 160 | text=text, 161 | username=username if username is not None else self.BOT_NAME, 162 | icon_url=icon_url if icon_url is not None else self.BOT_ICON_URL 163 | ) 164 | logger.debug("Message sent") 165 | 166 | # Command Messages are messages that begin with the `!` prefix 167 | # Returns True if a message_json or trigger was used in this method 168 | def command_message(self, message_json): 169 | # Checks if message starts with an exclamation point, and does the respective task 170 | if message_json["text"].startswith("!"): 171 | # Checks if the message is longer than a single character 172 | if len(message_json["text"]) <= 1: 173 | return False 174 | 175 | # Put all ! command parameters into an array 176 | args = message_json["text"].split() 177 | command_string = args[0][1:].lower() 178 | 179 | args.pop(0) # gets rid of the command 180 | 181 | # Checks if pattern differs from command messages 182 | # by containing digits or another "$" character 183 | if not command_string.isalpha(): 184 | return False 185 | 186 | if command_string == "version": 187 | self.response_message(message_json, [self.VERSION]) 188 | return True 189 | 190 | if command_string == "talk": 191 | ch = Bot.channels_to_ids(self, [self.TTPB]) 192 | c = ch[0] 193 | if message_json["channel"] != c: 194 | self.response_message(message_json, ["Talk to me in #" + self.TTPB]) 195 | return True 196 | 197 | # list that contains the message_json and args for all methods 198 | method_args = [] 199 | method_args.append(message_json) 200 | 201 | if command_string == "poll": 202 | method_args.append(self.polling_list[message_json["channel"]]) 203 | method_args.append(self.SLACK_CLIENT) 204 | 205 | if command_string == "pugbomb": 206 | method_args.append(self.pb_cooldown) 207 | 208 | if len(args) > 0: 209 | method_args.append(args) 210 | 211 | # This is in a try statement since it is checking if a module exists with the command_string name, 212 | # It makes the try statement that was previously around the `called_function` section below much smaller, 213 | # and also less likely to skip an error that should be printed to the console. 214 | try: 215 | # Check if the command is an admin command using the script's self-declaration method 216 | check_admin_function = getattr(self.COMMANDS_LIST[command_string], "is_admin_command") 217 | if check_admin_function(): 218 | self.response_message(message_json, ["Sorry, admin commands may only be used with the $ symbol (ie. `$admin`)"]) 219 | return True 220 | except: 221 | # If it fails, outputs that no command was found or syntax was broken, since all scripts must follow this convention. 222 | self.response_message(message_json, ["You seem to have used a function that doesnt exist, or used it incorrectly. See `!help` for a list of functions and parameters"]) 223 | return True 224 | 225 | # Finds the command with the name matching the text given, and executes it, assumed to exist because of above check 226 | called_function = getattr(self.COMMANDS_LIST[command_string], "run") 227 | script_response = called_function(*method_args) 228 | if script_response.status_code is 0: 229 | self.response_message(message_json, script_response.messages_to_send) 230 | else: 231 | error_cleanup = getattr(script_response.module_called, "error_cleanup") 232 | script_response = error_cleanup(script_response.status_code) 233 | self.response_message(message_json, script_response.messages_to_send) 234 | if script_response.special_condition: 235 | special_condition_function = getattr(script_response.module_called, "special_condition") 236 | special_condition_function(self) 237 | return True 238 | 239 | return False 240 | 241 | # Admin Messages are messages that begin with the `$` prefix 242 | # Returns True if a message_json or trigger was used in this method 243 | def admin_message(self, message_json): 244 | # Repeats above except for admin commands 245 | if message_json["text"].startswith("$"): 246 | # Checks if message is longer than "$" 247 | if len(message_json["text"]) > 1: 248 | args = message_json["text"].split() 249 | command_string = args[0][1:].lower() 250 | args.pop(0) 251 | 252 | # Checks if pattern differs from admin commands 253 | # by containing digits or another "$" character 254 | if not command_string.isalpha(): 255 | return False 256 | 257 | # list that contains the message_json and args for all methods 258 | method_args = [] 259 | method_args.append(message_json) 260 | method_args.append(args) 261 | method_args.append(self.SLACK_CLIENT) 262 | method_args.append(self) 263 | method_args.append(self.response_message) 264 | 265 | # This is in a try statement since it is checking if a module exists with the command_string name, 266 | # It makes the try statement that was previously around the `called_function` section below much smaller, 267 | # and also less likely to skip an error that should be printed to the console. 268 | try: 269 | # Check if the command is an admin command using the script's self-declaration method 270 | check_admin_function = getattr(self.COMMANDS_LIST[command_string], "is_admin_command") 271 | if not check_admin_function(): 272 | self.response_message(message_json, ["Sorry, normal commands should be used with the `!` prefix (ie `!coin`)"]) 273 | return True 274 | except: 275 | # If it fails, outputs that no command was found or syntax was broken, since all scripts must follow this convention. 276 | self.response_message(message_json, ["You seem to have used a function that doesnt exist, or used it incorrectly. See `!help` for a list of functions and parameters"]) 277 | return True 278 | 279 | user_info = self.SLACK_CLIENT.api_call( 280 | "users.info", 281 | user = message_json["user"] 282 | ) 283 | if user_info["user"]["is_admin"] is not True: 284 | self.response_message(message_json, ["You don't seem to be an authorized user to use these commands."]) 285 | return True 286 | 287 | # Finds the command with the name matching the text given, and executes it, assumed to exist because of above check 288 | called_function = getattr(self.COMMANDS_LIST[command_string], "run") 289 | script_response = called_function(*method_args) 290 | if script_response.status_code is 0: 291 | self.response_message(message_json, script_response.messages_to_send) 292 | else: 293 | error_cleanup = getattr(script_response.module_called, "error_cleanup") 294 | script_response = error_cleanup(script_response.status_code) 295 | self.response_message(message_json, script_response.messages_to_send) 296 | if script_response.special_condition: 297 | special_condition_function = getattr(script_response.module_called, "special_condition") 298 | special_condition_function(self) 299 | return True 300 | 301 | return False 302 | 303 | # Other Messages are messages that don't follow standard conventions (such as "Hey PantherBot!") 304 | # Returns True if a message_json or trigger was used in this method 305 | def other_message(self, message_json): 306 | if "subtype" not in message_json: 307 | message_txt = message_json["text"].lower() 308 | try: 309 | if re.match(".*panther +hackers.*", str(message_txt)): 310 | rebukes = ["No, this is PantherHackers.", "_No,_ this is PantherHackers.", "_*NO, THIS IS PANTHERHACKERS!!*_"] # List of responses in increasing intensity 311 | reaction_image_urls = ["https://s14.postimg.org/od6weheap/annoyance_level_0.png", "https://s14.postimg.org/zcs3q3k5d/annoyance_level_1.png", "https://s14.postimg.org/4vc8yjp2p/annoyance_level_2.png"] # List of avatars to temporarily switch to 312 | 313 | # Reset annoyance to 0 when it reaches 2 314 | if (self.annoyance >= len(rebukes)): 315 | self.annoyance = 0 316 | 317 | rebuke = rebukes[self.annoyance] 318 | image_url = reaction_image_urls[self.annoyance] 319 | self.annoyance += 1 # Increase annoyance each time this is triggered 320 | 321 | self.response_message(message_json, [rebuke], icon_url=image_url) 322 | 323 | return True 324 | elif message_txt == "hey pantherbot": 325 | # returns user info that said hey 326 | # TODO make this use USER_LIST 327 | temp_user = self.SLACK_CLIENT.api_call( 328 | "users.info", 329 | user = message_json["user"] 330 | ) 331 | logger.info("We did it reddit") 332 | self.response_message(message_json, ["Hello, " + temp_user["user"]["profile"]["first_name"] + "! :tada:"]) 333 | return True 334 | 335 | elif message_txt == "pantherbot ping": 336 | self.response_message(message_json, ["PONG"]) 337 | return True 338 | 339 | elif message_txt == ":rip: pantherbot" or message_txt == "rip pantherbot": 340 | self.response_message(message_json, [":rip:"]) 341 | return True 342 | 343 | # No response was necessary 344 | return False 345 | except: 346 | logger.error("Error with checking in other_message: likely the message contained unicode characters") 347 | elif "subtype" in message_json: 348 | if message_json["subtype"] == "channel_leave" or message_json["subtype"] == "group_leave": 349 | return True 350 | else: 351 | return False 352 | return False 353 | 354 | def message_user(self, user_id, text): 355 | dm_json = self.SLACK_CLIENT.api_call( 356 | "im.open", 357 | user=user_id 358 | ) 359 | self.send_msg(dm_json["channel"]["id"], text, is_id=True) 360 | 361 | # Reacts to all messages posted in the GENERAL channel with a pre-defined list of emojis 362 | def react_announcement(self, message_json): 363 | if self.GENERAL_CHANNEL != "" and message_json["channel"] == self.GENERAL_CHANNEL: 364 | temp_list = list(self.EMOJI_LIST) 365 | Bot.emoji_reaction(self, message_json["channel"], message_json["ts"], "pantherbot") 366 | for x in range(0, 3): 367 | num = random.randrange(0, len(temp_list)) 368 | Bot.emoji_reaction(self, message_json["channel"], message_json["ts"], temp_list.pop(num)) 369 | 370 | def riyans_denial(self, message_json): 371 | if "U0LJJ7413" in message_json["user"]: 372 | if message_json["text"][:1] in ["!", "$"] or message_json["text"].lower() in ["hey pantherbot", "pantherbot ping"]: 373 | self.response_message(message_json, ["No."]) 374 | return True 375 | return False 376 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | slackclient 2 | google-api-python-client 3 | upsidedown 4 | httplib2 5 | logging 6 | websocket-client 7 | chatterbot 8 | pyaml 9 | praw 10 | -------------------------------------------------------------------------------- /response.py: -------------------------------------------------------------------------------- 1 | class Response(): 2 | def __init__(self, module_called, status_code=0, messages_to_send=None, special_condition=False): 3 | self.module_called = module_called 4 | self.status_code = status_code 5 | self.messages_to_send = messages_to_send 6 | self.special_condition = special_condition 7 | # So... Python is incredibly dumb and smart at the same time, and will remember mutable objects during runtime 8 | # This is because Python only runs the def statement once, and only evaluates that once, and therefor could remember 9 | # messages_to_send out of thin air. 10 | if messages_to_send is None: self.messages_to_send = [] -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-2.7.13 2 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | commands = {} 3 | for module_file in os.listdir(os.path.dirname(__file__)): 4 | mod_name = module_file[:-3] 5 | mod_ext = module_file[-3:] 6 | 7 | if module_file == '__init__.py' or mod_ext != '.py': 8 | continue 9 | module = __import__(mod_name, locals(), globals()) 10 | get_alias = getattr(module, "return_alias") 11 | list_of_alias = get_alias() 12 | for alias in list_of_alias: 13 | commands[alias] = module 14 | -------------------------------------------------------------------------------- /scripts/admin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os, time, json 4 | import sys 5 | from response import Response 6 | from slackclient import SlackClient 7 | 8 | from pb_logging import PBLogger 9 | 10 | logger = PBLogger("Admin") 11 | 12 | def run(message_json, args, sc, bot, rmsg): 13 | response_obj = Response(sys.modules[__name__]) 14 | if args[0].lower() == "list": 15 | logger.info("Admin command list") 16 | response_obj.messages_to_send.append("The current admin commands are `list`, `add`, and `inactive-list`") 17 | elif args[0].lower() == "add": 18 | logger.info("Add admin") 19 | args.pop(0) 20 | admin_add(message_json, args, sc, bot, rmsg) 21 | # This requires the user's token be authed as admin (legacy, not bot tokens bypass this need) 22 | elif args[0].lower() == "inactive-list": 23 | logger.info("Inactive list") 24 | args.pop(0) 25 | response_obj = compile_inactive_list(message_json, bot, response_obj) 26 | else: 27 | logger.info("No admin command called for") 28 | response_obj.messages_to_send.append("The argument `" + args[0] + "` is not a proper admin command.") 29 | return response_obj 30 | 31 | def return_alias(): 32 | alias_list = ["admin"] 33 | return alias_list 34 | 35 | # Temporary function to add Admins for testing purposes 36 | def admin_add(message_json, args, sc, bot, rmsg): 37 | for id in args: 38 | bot.ADMIN.append(id) 39 | rmsg(message_json, ["User ID " + id + " has been temporarily added to the admin list"]) 40 | logger.info("User ID " + id + "has been temporarily added to the admin list") 41 | 42 | def compile_inactive_list(message_json, bot, response_obj): 43 | list_of_emails = check_user_status(bot) 44 | response_obj.messages_to_send.append("Sending list to you in a DM.") 45 | message_string = "List of emails:\n" 46 | for email in list_of_emails: 47 | message_string += email + "\n" 48 | bot.message_user(message_json["user"], message_string) 49 | return response_obj 50 | 51 | # Checks the user list for activity in the last 14 days, and return a list of emails. 52 | def check_user_status(bot): 53 | temp_list = bot.SLACK_CLIENT.api_call( 54 | "team.billableInfo" 55 | ) 56 | if temp_list["ok"] is not True: 57 | logger.error("User token likely not authed with admin scope. Please update use a legacy token, or use the proper OAuth request.") 58 | return ["List is unobtainable with current token auth scope."] 59 | data = temp_list["billable_info"] 60 | list_of_marked_users = [] 61 | list_of_emails = [] 62 | for user in data: 63 | if data[user]["billing_active"] is False: 64 | list_of_marked_users.append(user) 65 | 66 | for user in bot.BOT_CONN["users"]: 67 | if user["id"] in list_of_marked_users: 68 | try: 69 | list_of_emails.append(user["profile"]["email"]) 70 | except: 71 | logger.info("No email available for user: " + user["name"]) 72 | 73 | logger.info("User list checked. " + str(len(list_of_marked_users)) + " have been marked as inactive in the last 14 days.") 74 | return list_of_emails 75 | 76 | def is_admin_command(): 77 | return True 78 | 79 | def help_preview(): 80 | return "$admin add" 81 | 82 | def help_text(): 83 | return "$admin allows for moderator intervention to the live instance of PantherBot. Currently this just means adding more admins to the list, but that will change in the future." -------------------------------------------------------------------------------- /scripts/catfact.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import urllib2, json 5 | import sys 6 | from response import Response 7 | 8 | from pb_logging import PBLogger 9 | logger = PBLogger("CatFact") 10 | 11 | def run(response, args=None): 12 | response_obj = Response(sys.modules[__name__]) 13 | catfact_url = "https://catfact.ninja/fact" 14 | 15 | try: 16 | raw_response = urllib2.urlopen(catfact_url).read() 17 | parsed_response = json.loads(raw_response) 18 | 19 | response_obj.messages_to_send.append(parsed_response["fact"]) 20 | 21 | except Exception as e: 22 | logger.error("Error in catfacts: " + str(e)) 23 | response_obj.status_code = -1 24 | 25 | return response_obj 26 | 27 | def return_alias(): 28 | alias_list = ["catfact"] 29 | return alias_list 30 | 31 | def return_alias(): 32 | alias_list = ["catfact"] 33 | return alias_list 34 | 35 | def error_cleanup(error_code): 36 | response_obj = Response(sys.modules[__name__]) 37 | if error_code is -1: 38 | response_obj.messages_to_send.append("Cat facts can't be returned right now :sob:") 39 | else: 40 | response_obj.messages_to_send.append("An unknown error occured. Error code: " + error_code) 41 | return response_obj 42 | 43 | def is_admin_command(): 44 | return False 45 | 46 | def help_preview(): 47 | return "!catfact" 48 | 49 | def help_text(): 50 | return "Catfact returns a random catfact from a lovely API that crashes all the time. Praise Lord Mittens. Ask about Mo's cats." 51 | -------------------------------------------------------------------------------- /scripts/coin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import random 4 | import sys 5 | from response import Response 6 | 7 | from pb_logging import PBLogger 8 | logger = PBLogger("Coin") 9 | 10 | # flips a coin 11 | def run(response): 12 | response_obj = Response(sys.modules[__name__]) 13 | l = ["Heads", "Tails"] 14 | m = random.randrange(0, 2) 15 | response_obj.messages_to_send.append(l[m]) 16 | logger.info(l[m]) 17 | return response_obj 18 | 19 | def return_alias(): 20 | alias_list = ["coin"] 21 | return alias_list 22 | 23 | def is_admin_command(): 24 | return False 25 | 26 | def help_preview(): 27 | return "!coin" 28 | 29 | def help_text(): 30 | return "Flips a coin and returns the result... Sometimes you wonder if people even read these. Heads they don't." -------------------------------------------------------------------------------- /scripts/conch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import random 4 | import sys 5 | from response import Response 6 | 7 | from pb_logging import PBLogger 8 | logger = PBLogger("Conch") 9 | 10 | def run(response, args): 11 | response_obj = Response(sys.modules[__name__]) 12 | lower_args = [arg.lower() for arg in args] 13 | if "or" in lower_args: 14 | l = ["Neither."] 15 | else: 16 | l = ["Yes.", "No.", "Maybe someday.", "I don't think so.", "Follow the seahorse.", "Try asking again."] 17 | m = random.randrange(0, len(l)) 18 | response_obj.messages_to_send.append(l[m]) 19 | logger.info(l[m]) 20 | return response_obj 21 | 22 | def return_alias(): 23 | alias_list = ["conch", "magicconch"] 24 | return alias_list 25 | 26 | def is_admin_command(): 27 | return False 28 | 29 | def help_preview(): 30 | return "!conch <Optional:String>" 31 | 32 | def help_text(): 33 | return "Performs the questioning ritual of the conch from Spongebob Season 3 Episode 42a. All hail the magic conch!" -------------------------------------------------------------------------------- /scripts/flip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import upsidedown, sys 4 | from response import Response 5 | 6 | from pb_logging import PBLogger 7 | logger = PBLogger("Flip") 8 | 9 | # flips text using upsidedown module and has a donger for emohasis 10 | def run(response, args=[]): 11 | response_obj = Response(sys.modules[__name__]) 12 | toFlip = '' 13 | donger = '(╯°□°)╯︵' 14 | if len(args) >= 0: 15 | for n in range(0, len(args)): 16 | toFlip += args[n] + " " 17 | 18 | if toFlip == '': 19 | toFlip = unicode('┻━┻', "utf-8") 20 | 21 | try: 22 | donger = unicode(donger, "utf-8") 23 | logger.info(toFlip[:15 or len(toFlip)] + "...") 24 | flippedmsg = upsidedown.transform(toFlip) 25 | response_obj.messages_to_send.append(donger + flippedmsg) 26 | except Exception as e: 27 | logger.error(str(e)) 28 | response_obj.status_code = -1 29 | return response_obj 30 | 31 | def return_alias(): 32 | alias_list = ["flip"] 33 | return alias_list 34 | 35 | def error_cleanup(error_code): 36 | response_obj = Response(sys.modules[__name__]) 37 | if error_code is -1: 38 | response_obj.messages_to_send.append("Sorry, there seems to have been an error while flipping. Take this donger instead: (╯°□°)╯︵┻━┻") 39 | else: 40 | response_obj.messages_to_send.append("An unknown error occured. Error code: " + error_code) 41 | return response_obj 42 | 43 | def is_admin_command(): 44 | return False 45 | 46 | def help_preview(): 47 | return "!flip <Optional:String>" 48 | 49 | def help_text(): 50 | return unicode("Flips your message because sometimes you just really need someone to know that they messed up... Or your DM is having you face a dragon and want to spare your favorite character. (╯°□°)╯︵┻━┻", "utf-8") -------------------------------------------------------------------------------- /scripts/fortune.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import urllib2, json 4 | import sys 5 | from response import Response 6 | 7 | from pb_logging import PBLogger 8 | logger = PBLogger("Fortune") 9 | 10 | # returns a random "fortune" 11 | def run(response, args=None): 12 | response_obj = Response(sys.modules[__name__]) 13 | fortune_url = "https://9bj8bks4p3.execute-api.us-west-2.amazonaws.com/prod/fortunefortoday-get-cookie" # This was the URL I found in the JS code on the website 14 | 15 | try: 16 | # get fortune 17 | raw_response = urllib2.urlopen(fortune_url).read() 18 | parsed_response = json.loads(raw_response) 19 | fortune = parsed_response['body-json']['GetCookieResult'] # The JSON returned is pretty weird, this is where it stores the actual fortune 20 | 21 | except Exception as e: 22 | fortune = "Unable to reach Fortune Telling api" 23 | logger.error("Unable to reach Fortune Telling API: " + str(e)) 24 | 25 | # make api call 26 | response_obj.messages_to_send.append(fortune) 27 | return response_obj 28 | 29 | def return_alias(): 30 | alias_list = ["fortune"] 31 | return alias_list 32 | 33 | def is_admin_command(): 34 | return False 35 | 36 | def help_preview(): 37 | return "!fortune" 38 | 39 | def help_text(): 40 | return "Returns a random fortune from an inconsistent API. Responses are slow, just give it some time Jim!" 41 | -------------------------------------------------------------------------------- /scripts/helloworld.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Template: Include this so this file supports unicode characters in it. 4 | 5 | # Always import these two pieces at least 6 | import sys 7 | from response import Response 8 | 9 | # Import these so that you can use the logger functions 10 | # so that you can log things 11 | import logging 12 | from pb_logging import PBLogger 13 | 14 | # If you need to print something that will be used for official logging purposes 15 | # use this logger class using the custom handler PBHandler from the log_handler class 16 | logger = PBLogger('Hello World') 17 | 18 | # 'run' should have all your primary logic, and must exist 19 | def run(response, args=None): # response is always given to you, good for checking on user info or something unique to a message object, args is optional but your code will not run if given arguments and this is not present, or if your function may not take args, set it to None or [] depending on your needs 20 | # This is your response_obj, it should be returned at the end of your script (or logic that calls for it to end early) 21 | # See response.py at the root of the project directory to see how it works in more depth 22 | response_obj = Response(sys.modules[__name__]) 23 | # do your logic here 24 | message = "Hello World:" 25 | if args is not None: 26 | for x in range(0, len(args)): # For loop that goes from the second element of args (to skip the command) to the last element 27 | message += " " + args[x] # adds a space and the next argument from the message call 28 | 29 | # You should have all messages you wish sent to chat in the `messages_to_send` field. 30 | # It is by default an empty array, so you may append messages to it easily. 31 | logger.info(message) 32 | response_obj.messages_to_send.append(message) 33 | # Your script may run into user error! In such a case, you can define the `status_code` field of response_obj to something not 0 34 | # If it is not 0, the scripts `error_cleanup` method will be called after the current method finishes. 35 | possible_error = 0 36 | if possible_error is not 0: 37 | response_obj.status_code = -1 38 | return response_obj 39 | 40 | def return_alias(): 41 | alias_list = ["helloworld"] 42 | return alias_list 43 | 44 | # Called before the script's main function (ie `helloworld()` above) to describe the purpose of the function 45 | # If admins are the only ones allowed to call this script's functionality, this should return True 46 | def is_admin_command(): 47 | return False 48 | 49 | # Called after the main function of the script should it return a status code that is not 0 50 | def error_cleanup(error_code): 51 | response_obj = Response(sys.modules[__name__]) 52 | if error_code is -1: 53 | response_obj.messages_to_send.append("AHHHH! FIRE! Explain your error here, especially if it is user error") 54 | 55 | # You can log an error this way 56 | logger.error("Error: FIRE!") 57 | else: 58 | # If for some reason your script returned a status_code of something you weren't expecting, this is here to catch that 59 | response_obj.messages_to_send.append("An unknown error occured. Error code: " + error_code) 60 | 61 | # You can log an error here too 62 | logger.error("Error: " + error_code) 63 | return response_obj 64 | 65 | # All commands require a help_preview and help_text command that describe their expected usage format and functionality. 66 | def help_preview(): 67 | return "!helloworld <Optional:String>" 68 | 69 | def help_text(): 70 | return "Returns your string with 'Hello World:' as a prefix." -------------------------------------------------------------------------------- /scripts/help.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | from response import Response 6 | 7 | from pb_logging import PBLogger 8 | from scripts import commands 9 | logger = PBLogger("Help") 10 | 11 | # help script that details the list of commands 12 | def run(response, args=None): 13 | response_obj = Response(sys.modules[__name__]) 14 | 15 | #Default call to !help 16 | if args is None: 17 | commands_help_text = "PantherBot works by prefacing commands with \"!\"\nCommands:\n```" 18 | commands_help_text += "!version\n" 19 | 20 | list_of_used_commands = [] 21 | for value in commands.values(): 22 | if value not in list_of_used_commands: 23 | list_of_used_commands.append(value) 24 | try: 25 | get_help_preview = getattr(value, "help_preview") 26 | commands_help_text += get_help_preview() + "\n" 27 | except: 28 | logger.error("Module " + str(value) + " has no help_preview function") 29 | commands_help_text += "```\nAdmins can use `$` commands\n" 30 | commands_help_text += "Got suggestions for PantherBot? Fill out our typeform to leave your ideas! https://goo.gl/rEb0B7" 31 | 32 | response_obj.messages_to_send.append(commands_help_text) 33 | logger.info("Help response") 34 | return response_obj 35 | elif len(args) is 1: 36 | command_help_text = "Help info for command: `" + args[0] + "`\n" 37 | try: 38 | get_command_help = getattr(commands[args[0]], "help_text") 39 | command_help_text += get_command_help() 40 | except: 41 | response_obj.status_code = -1 42 | return response_obj 43 | response_obj.messages_to_send.append(command_help_text) 44 | return response_obj 45 | elif len(args) > 1: 46 | response_obj.status_code = -2 47 | return response_obj 48 | 49 | 50 | def error_cleanup(error_code): 51 | response_obj = Response(sys.modules[__name__]) 52 | if error_code is -1: 53 | response_obj.messages_to_send.append("That is an invalid command name or alias for said command. Please use `!help` for the full list of commands.") 54 | logger.error("Error: User requested unavailable command") 55 | elif error_code is -2: 56 | response_obj.messages_to_send.append("Multiple commands are not supported.") 57 | logger.error("Error: User requested multiple commands") 58 | else: 59 | response_obj.messages_to_send.append("An unknown error occured. Error code: " + error_code) 60 | logger.error("Error: Unknown error code" + error_code) 61 | return response_obj 62 | 63 | def return_alias(): 64 | alias_list = ["help", "h"] 65 | return alias_list 66 | 67 | def is_admin_command(): 68 | return False 69 | 70 | def help_preview(): 71 | return "!help <Optional:Command>" 72 | 73 | def help_text(): 74 | return "Returns the list of commands PantherBot can do, as well as specific command help. You're using the command so you kind of know how it works, but did you know that PantherBot has been rewritten about 4 times now?" -------------------------------------------------------------------------------- /scripts/poll.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | STARTING_MESSAGE = "Sorry, a poll is already being set up in this channel. To cancel a poll that has not begun yet, say `!poll end`" 5 | BUSY_MESSAGE = "Sorry, a poll is already in progress in this channel. Please have the person hosting the poll cancel it, or ask an admin to end it using `!poll end`" 6 | 7 | import sys 8 | from response import Response 9 | 10 | def run(response, options, sc, args): 11 | response_obj = Response(sys.modules[__name__]) 12 | if args[0] == "begin": 13 | """ 14 | set options[0] to the timestamp of the poll(that will be set by start's posted message (see bot.py)), and options[1] to an array that contains the polling options, and options[2] to the status of the poll (none, starting, ongoing, ended). 15 | options = ["timestamp",[polling_options],"status"] 16 | """ 17 | if options[2] == "none": 18 | args.pop(0) # get rid of the "begin" element 19 | return begin(response, options, args) # logic for starting 20 | 21 | elif options[2] == "starting": 22 | response_obj.messages_to_send.append(STARTING_MESSAGE) 23 | 24 | elif options[2] == "ongoing": 25 | response_obj.messages_to_send.append(BUSY_MESSAGE) 26 | 27 | elif options[2] == "ended": 28 | response_obj.messages_to_send.append(BUSY_MESSAGE) 29 | return response_obj 30 | 31 | elif args[0] == "start": 32 | if options[2] == "none": 33 | response_obj.messages_to_send.append("Sorry, a poll has not been started on this channel. To start a poll, type `!poll begin`") 34 | 35 | elif options[2] == "starting": 36 | return start(response, options, args) 37 | 38 | elif options[2] == "ongoing": 39 | response_obj.messages_to_send.append(BUSY_MESSAGE) 40 | 41 | elif options[2] == "ended": 42 | response_obj.messages_to_send.append(BUSY_MESSAGE) 43 | return response_obj 44 | 45 | elif args[0] == "end": 46 | return end(response, options, sc, args) 47 | elif args[0] == "results": 48 | return results(response,options,sc,args) 49 | response_obj.messages_to_send.append("You seemed to have used this method incorrectly... see !help to see how to use it") 50 | return response_obj 51 | 52 | def return_alias(): 53 | alias_list = ["poll"] 54 | return alias_list 55 | 56 | def begin(response, options, args): 57 | op = [] 58 | polling_options = [] 59 | option_string = "" 60 | for arg in args: 61 | option_string = option_string + arg + " " 62 | if ";" in arg: 63 | polling_options.append(option_string[:-2]) 64 | option_string = "" 65 | if len(polling_options) > 10: 66 | response_obj.messages_to_send.append("Sorry, I'm currently capped at ten options right now, what kind of poll are you looking to do?") 67 | return response_obj 68 | message = "Options are:\n" 69 | message = message + '\n'.join(polling_options) 70 | options[1] = polling_options 71 | options[2] = "starting" 72 | response_obj.messages_to_send.append("Does this look correct?\n" + message, "If this is correct, type `!poll start` to begin your poll, or `!poll end` to cancel") 73 | return response_obj 74 | 75 | 76 | def start(response, options, args): 77 | arr_of_emojis = [":one:",":two:",":three:",":four:",":five:",":six:",":seven:",":eight:",":nine:",":keycap_ten:"] 78 | ops = options[1] 79 | options[1] = dict() 80 | options[2] = "ongoing" 81 | message = "" 82 | for index, op in enumerate(ops): 83 | options[1][op] = arr_of_emojis[index] 84 | for key in options[1]: 85 | message = message + key + ": " + options[1][key] + "\n" 86 | response_obj.messages_to_send.append("Poll starting", "POLL_BEGIN " + response["channel"] + ";\n" + message) 87 | return response_obj 88 | 89 | def end(response, options, sc, args): 90 | if options[2] in ["none", "starting"]: 91 | options[2] = "none" 92 | response_obj.messages_to_send.append("Poll cancelled") 93 | return response_obj 94 | 95 | elif options[2] == "ongoing": 96 | options[2] = "none" 97 | response_obj.messages_to_send.append("Poll concluded", results(response,options,sc,args)) 98 | 99 | else: 100 | options[2] = "none" 101 | response_obj.messages_to_send.append(results(response,options,sc,args)) 102 | return response_obj 103 | 104 | 105 | def results(response, options, sc, args): 106 | r = sc.api_call( 107 | "reactions.get", 108 | channel=response["channel"], 109 | timestamp=options[0], 110 | full="true" 111 | ) 112 | 113 | reac_dict = dict() 114 | winner = "" 115 | count = 0 116 | for reaction in r["message"]["reactions"]: 117 | reac_dict[reaction["name"]] = reaction["count"] - 1 118 | 119 | message = "Results:\n" 120 | for key in options[1]: 121 | message = message + key + ": " + str(reac_dict[options[1][key].strip(":")]) + "\n" 122 | 123 | response_obj.messages_to_send.append(message) 124 | return response_obj 125 | 126 | def is_admin_command(): 127 | return False 128 | 129 | def help_preview(): 130 | return "!poll <begin/start/end/results> [arguments followed by a `;`]" 131 | 132 | def help_text(): 133 | return "To set up a poll, use `!poll begin (Option 1); (Option 2); ...; (Option n);` where n is <= 10\nTo begin the poll, use `!poll start`\nTo end the poll, use `!poll end`\nTo review the results of the last poll, use `!poll results`\nThis command needs to be rewritten... Go poke Braxton." -------------------------------------------------------------------------------- /scripts/pugbomb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import praw 4 | import sys 5 | from response import Response 6 | 7 | def run(response, pb_cooldown, args=None): 8 | response_obj = Response(sys.modules[__name__]) 9 | if pb_cooldown is False: 10 | response_obj.messages_to_send.append("Sorry, pugbomb is on cooldown.") 11 | return response_obj 12 | 13 | if args is None: 14 | response_obj.status_code = -3 15 | return response_obj 16 | 17 | try: 18 | num = round(int(args[0])) 19 | except: 20 | # not a number 21 | response_obj.status_code = -1 22 | return response_obj 23 | 24 | if num > 10: 25 | num = 10 26 | elif num <= 0: 27 | # not more than 0 28 | response_obj.status_code = -2 29 | return response_obj 30 | 31 | reddit = praw.Reddit(client_id='aGpQJujCarDHWA', 32 | client_secret='fkA9lp0NDx23B_qdFezTeGyGKu8', 33 | user_agent='my user agent', 34 | password='PHGSU2017', 35 | username='Panther_Bot') 36 | 37 | pug_urls=[] 38 | for submission in reddit.subreddit('pugs').hot(limit=num): 39 | if 'imgur' in submission.url and not '.jpg' in submission.url and not '.png' in submission.url and not '.jpeg' in submission.url: 40 | submission.url+=('.jpg') 41 | 42 | pug_urls.append(submission.url) 43 | 44 | pug_urls.append(""" 45 | `Having issues viewing pugs? Try Preferences > Messages > 'Even if theyre larger than 2MB'` 46 | """) 47 | response_obj.messages_to_send = pug_urls 48 | response_obj.special_condition = True 49 | return response_obj 50 | 51 | def return_alias(): 52 | alias_list = ["pugbomb"] 53 | return alias_list 54 | 55 | def error_cleanup(error_code): 56 | response_obj = Response(sys.modules[__name__]) 57 | if error_code is -1: 58 | response_obj.messages_to_send.append("Sorry, you didn't enter a number!") 59 | elif error_code is -2: 60 | response_obj.messages_to_send.append("Sorry, I can't give you negative pugs.") 61 | elif error_code is -3: 62 | response_obj.messages_to_send.append("Please enter a number for Pugbomb!") 63 | else: 64 | response_obj.messages_to_send.append("An unknown error occured. Error code: " + str(error_code)) 65 | return response_obj 66 | 67 | def set_cooldown(bot): 68 | if bot.pb_cooldown is True: 69 | bot.pb_cooldown = False 70 | 71 | def special_condition(bot): 72 | set_cooldown(bot) 73 | 74 | def is_admin_command(): 75 | return False 76 | 77 | def help_preview(): 78 | return "!pugbomb" 79 | 80 | def help_text(): 81 | return "RETURNS YOU BEAUTIFUL REDDIT PUGS. Pugs are my favorite." 82 | -------------------------------------------------------------------------------- /scripts/rage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import upsidedown 4 | import sys 5 | from response import Response 6 | 7 | from pb_logging import PBLogger 8 | logger = PBLogger("Rage") 9 | 10 | # flips text using upsidedown module and has a donger for emohasis 11 | def run(response, args=[]): 12 | response_obj = Response(sys.modules[__name__]) 13 | toFlip = '' 14 | donger = '(ノಠ益ಠ)ノ彡' 15 | for n in range(0, len(args)): 16 | toFlip += args[n] + " " 17 | 18 | if toFlip == '': 19 | toFlip = unicode('┻━┻', "utf-8") 20 | 21 | try: 22 | donger = unicode(donger, "utf-8") 23 | logger.info(toFlip[:15 or len(toFlip)] + "...") 24 | flippedmsg = upsidedown.transform(toFlip) 25 | response_obj.messages_to_send.append(donger + flippedmsg) 26 | except Exception as e: 27 | logger.error("Error in flip: " + str(e)) 28 | response_obj.messages_to_send.append("Sorry, I can't seem to flip right now, or you gave an invalid argument") 29 | return response_obj 30 | 31 | def return_alias(): 32 | alias_list = ["rage"] 33 | return alias_list 34 | 35 | def is_admin_command(): 36 | return False 37 | 38 | def help_preview(): 39 | return "!rage <Optional:String>" 40 | 41 | def help_text(): 42 | return "Rage flips the text or table because you really want the world to know that you're upset." -------------------------------------------------------------------------------- /scripts/talk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from chatterbot import ChatBot 5 | from chatterbot.trainers import ChatterBotCorpusTrainer 6 | import sys 7 | from response import Response 8 | 9 | def run(response, args=[]): 10 | response_obj = Response(sys.modules[__name__]) 11 | cb = ChatBot('PantherBot') 12 | cb.set_trainer(ChatterBotCorpusTrainer) 13 | cb.train( 14 | "chatterbot.corpus.english" 15 | ) 16 | try: 17 | response_obj.messages_to_send.append(cb.get_response(" ".join(args)).text) 18 | except: 19 | response_obj.messages_to_send.append("I'm feeling sick... come back later") 20 | return response_obj 21 | 22 | def return_alias(): 23 | alias_list = ["talk"] 24 | return alias_list 25 | 26 | def is_admin_command(): 27 | return False 28 | 29 | def help_preview(): 30 | return "!talk <String>" 31 | 32 | def help_text(): 33 | return "You can hold a conversation with PantherBot (although the quality and snarkiness might not be to your liking) by prefacing your sentences with `!talk`" -------------------------------------------------------------------------------- /scripts/unflip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | from response import Response 6 | 7 | from pb_logging import PBLogger 8 | logger = PBLogger("Unflip") 9 | 10 | #"unflips" text 11 | def run(response, args=[]): 12 | response_obj = Response(sys.modules[__name__]) 13 | toUnFlip = '' 14 | for n in range(0, len(args)): 15 | toUnFlip += args[n] + " " 16 | 17 | if toUnFlip == "": 18 | toUnFlip = unicode('┬──┬', "utf-8") 19 | 20 | try: 21 | donger = "ノ( º _ ºノ)" 22 | donger = unicode(donger, "utf-8") 23 | logger.info(toUnFlip[:15 or len(toUnFlip)] + "...") 24 | response_obj.messages_to_send.append(toUnFlip + donger) 25 | except Exception as e: 26 | logger.error("Error in flip: " + str(e)) 27 | response_obj.status_code = -1 28 | return response_obj 29 | 30 | def return_alias(): 31 | alias_list = ["unflip"] 32 | return alias_list 33 | 34 | def error_cleanup(error_code): 35 | response_obj = Response(sys.modules[__name__]) 36 | if error_code is -1: 37 | response_obj.messages_to_send.append("Sorry, there seems to have been an error while unflipping. Take this donger instead: (╯°□°)╯︵┻━┻") 38 | else: 39 | response_obj.messages_to_send.append("An unknown error occured. Error code: " + error_code) 40 | return response_obj 41 | 42 | def is_admin_command(): 43 | return False 44 | 45 | def help_preview(): 46 | return "!unflip <Optional:String>" 47 | 48 | def help_text(): 49 | return "Restore order to the world by placing the table or text back down. I'm proud of you citizen!" -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | import threading, time, logging, os, sys, codecs 2 | 3 | from bot import Bot 4 | from reactbot import ReactBot 5 | 6 | # 7 | from pb_logging import PBLogger 8 | 9 | if __name__ == "__main__": 10 | # See here for logging documentation https://docs.python.org/2/howto/logging.html 11 | 12 | # Set the name for the logger 13 | # Add custom log handler to logger 14 | logger = PBLogger('PantherBot') 15 | 16 | 17 | # Checks if the system's encoding type is utf-8 and changes it to utf-8 if it isnt (its not on Windows by default) # noqa: 501 18 | if sys.stdout.encoding != 'utf-8': 19 | sys.stdout = codecs.getwriter('utf-8')(sys.stdout, 'strict') 20 | if sys.stderr.encoding != 'utf-8': 21 | sys.stderr = codecs.getwriter('utf-8')(sys.stderr, 'strict') 22 | 23 | # Initializes our primary bot 24 | # This is the reactive bot known as "PantherBot," and is responsible for all message detection and immediate reactions 25 | logger.info("Initializing bot") 26 | token = os.environ.get('PB_TOKEN') 27 | 28 | # List of all bots running in current process. 29 | BOT_LIST = [] 30 | react_bot = ReactBot(token, bot_name="PantherBot") 31 | BOT_LIST.append(react_bot) 32 | bot_thread = threading.Thread(target=react_bot.WEBSOCKET.run_forever, kwargs={"ping_interval":30, "ping_timeout":10}) 33 | logger.info("Beginning thread") 34 | bot_thread.start() 35 | 36 | proactive_bot = Bot(token, bot_name="PantherBot") 37 | count_interval = 0 38 | 39 | while True: 40 | try: 41 | time.sleep(1) 42 | count_interval += 1 43 | if count_interval % 600 is 0: 44 | for b in BOT_LIST: 45 | if b.WEBSOCKET != None: 46 | b.pb_cooldown = True 47 | if count_interval % 86400 is 0: 48 | logger.info("Proactive still alive") 49 | if count_interval >= 86400: 50 | count_interval = 0 51 | except KeyboardInterrupt: 52 | logger.info("Keyboard Interrupt") 53 | react_bot.close() 54 | break 55 | -------------------------------------------------------------------------------- /user_greeting.txt: -------------------------------------------------------------------------------- 1 | Hey new user! Welcome to PantherHackers, we hope you have a lovely stay in our Slack. Here on Slack all PH members can communicate as much as they like, its like a Discord server if you've ever heard of that. We update our members regularly with things happening on campus and events we are hosting in #announcements, but we are a fluid community that meets up all over campus, so check out #random for anything that might be interesting. 2 | A few ground rules, do not use the "@channel" modifier in large channels like #random, they will be removed. If you need to ping people in #random, please use the "@here" modifier, but use it with sense. Please refrain from spamming channels, and be sure to post things in the relevant channel if possible (cross posting to #random is fine). If you have any computer related questions, #hack-help is your best bet. 3 | I'm a bot created by other PH members. Sometimes I freak out and make mistakes, if you have any problems with me, be sure to post in #pantherbot-dev. If you have any questions about PH, message @braxton (the Slack manager). 4 | Think PantherBot is cool? Check it out on GitHub at https://github.com/PantherHackers/PantherBot 5 | Happy Hacking! 6 | --------------------------------------------------------------------------------