├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── botnet ├── __init__.py ├── cli.py ├── codes.py ├── config.py ├── helpers.py ├── logging.py ├── manager.py ├── message.py ├── modules │ ├── __init__.py │ ├── base.py │ ├── baseresponder.py │ ├── builtin │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── anonpost.py │ │ ├── c3.py │ │ ├── countdown.py │ │ ├── exception_monitor.py │ │ ├── github.py │ │ ├── irc.py │ │ ├── links.py │ │ ├── markov.py │ │ ├── meta.py │ │ ├── quotes.py │ │ ├── reactions.py │ │ ├── reddit.py │ │ ├── reminders.py │ │ ├── sed.py │ │ ├── seen.py │ │ ├── tell.py │ │ ├── tv.py │ │ └── vatsim.py │ ├── lib │ │ ├── __init__.py │ │ ├── cache.py │ │ ├── decorators.py │ │ └── network.py │ ├── mixins.py │ └── utils.py ├── signals.py └── wrappers.py ├── docs ├── detailed_help_styleguide.md └── writing_external_modules.md ├── examples ├── simple_module.py └── tests.py ├── setup.py └── tests ├── conftest.py ├── modules ├── builtin │ ├── quotes │ ├── test_admin.py │ ├── test_countdown.py │ ├── test_github.py │ ├── test_irc.py │ ├── test_meta.py │ ├── test_quotes.py │ ├── test_reminders.py │ ├── test_sed.py │ └── test_tell.py ├── mixins │ ├── test_config_mixin.py │ └── test_message_dispatcher_mixin.py ├── test_base_responder.py └── test_cache.py ├── resources └── events.json ├── test_config.py ├── test_helpers.py ├── test_manager.py ├── test_message.py ├── test_reload.py ├── test_signals.py └── test_wrapper.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 boreq 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: test_botnet test_examples 2 | 3 | test_botnet: 4 | py.test tests 5 | 6 | test_examples: 7 | py.test examples/tests.py 8 | 9 | pyflakes: 10 | pyflakes botnet 11 | 12 | mypy: 13 | mypy --warn-no-return --ignore-missing-imports botnet 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Botnet 2 | IRC bot written in Python. 3 | 4 | Botnet implements nearly all core functionality in a form of modules which can 5 | be loaded and unloaded at will and communicate with one another using signals. 6 | Thanks to that design a module which encounters serious issues does not affect 7 | the execution of other modules. Furthermore all features of the bot can be 8 | enabled and disabled at will and the modules can be updated without restarting 9 | the entire bot and reconnecting to the IRC network simply by reloading a module. 10 | It is possible to use built-in modules or create easy to load and integrate 11 | user-maintained external modules distributed in a form of Python packages. 12 | 13 | ## Installation 14 | 15 | pip install --process-dependency-links git+https://github.com/boreq/botnet 16 | 17 | ## Usage 18 | 19 | botnet --help 20 | botnet run --help 21 | botnet run /path/to/config.json 22 | 23 | ## Available modules 24 | 25 | To see all available modules navigate to `botnet.modules.builtin` directory. 26 | Each module is provided with a comment containing a description and an example 27 | config snippet. 28 | 29 | ## Configuration 30 | Config snippets from the module description can be added to the `module_config` 31 | key in the config file. This is the general structure of the config file: 32 | 33 | { 34 | "modules": ["module_name1", "module_name2"], 35 | "module_config": { 36 | "namespace": { 37 | "module_name": { 38 | "config_parameter": "value" 39 | } 40 | } 41 | } 42 | } 43 | 44 | All builtin modules use the namespace `botnet`. Most modules are based on the 45 | `BaseResponder` module, so to change the default command prefix alter the 46 | `module_config.botnet.base_responder.command_prefix` configuration key. See the 47 | example config for details. 48 | 49 | ## Example config 50 | 51 | { 52 | "modules": ["irc", "meta"], 53 | "module_config": { 54 | "botnet": { 55 | "irc": { 56 | "server": "irc.example.com", 57 | "port": 6697, 58 | "ssl": true, 59 | "nick": "my_bot", 60 | "channels": [ 61 | { 62 | "name": "#my-channel", 63 | "password": null 64 | } 65 | ], 66 | "autosend": [ 67 | "PRIVMSG your_nick :I connected!" 68 | ] 69 | }, 70 | "base_responder": { 71 | "command_prefix": "." 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /botnet/__init__.py: -------------------------------------------------------------------------------- 1 | from .codes import Code 2 | from .helpers import is_channel_name 3 | from .logging import get_logger 4 | from .message import Message 5 | from .modules import BaseModule, BaseResponder, ConfigMixin, \ 6 | BaseMessageDispatcherMixin, StandardMessageDispatcherMixin, \ 7 | AdminMessageDispatcherMixin 8 | from .signals import message_in, admin_message_in, message_out, on_exception, \ 9 | module_load, module_unload, module_loaded, module_unloaded, config_reload, \ 10 | config_reloaded, config_changed 11 | -------------------------------------------------------------------------------- /botnet/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Command line interface, `cli` group serves as an entry point for the script 3 | called `botnet`. 4 | """ 5 | 6 | 7 | import click 8 | import logging 9 | import signal 10 | from .manager import Manager 11 | 12 | 13 | logger_levels = ['warning', 'info', 'debug'] 14 | 15 | 16 | @click.group() 17 | @click.option('--verbosity', type=click.Choice(logger_levels), default='warning') 18 | @click.pass_context 19 | def cli(ctx, verbosity): 20 | """IRC bot written in Python.""" 21 | log_format = '%(asctime)s %(levelname)s %(name)s: %(message)s' 22 | log_level = getattr(logging, verbosity.upper(), None) 23 | logging.basicConfig(format=log_format, level=log_level) 24 | 25 | 26 | @cli.command() 27 | @click.argument('config', type=click.Path(exists=True)) 28 | def run(config): 29 | """Runs the bot.""" 30 | def signal_handler(signum, frame): 31 | manager.stop() 32 | 33 | def attach_signals(): 34 | for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGHUP, signal.SIGQUIT]: 35 | signal.signal(sig, signal_handler) 36 | 37 | manager = Manager(config_path=config) 38 | attach_signals() 39 | manager.run() 40 | -------------------------------------------------------------------------------- /botnet/codes.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, unique 2 | 3 | 4 | @unique 5 | class Code(Enum): 6 | """Enum with all numeric replies defined in RFC 2812. Also contains 7 | additional replies used by popular networks included at the bottom. 8 | """ 9 | 10 | # Welcome to the Internet Relay Network !@ 11 | RPL_WELCOME = 1 12 | 13 | # Your host is , running version 14 | RPL_YOURHOST = 2 15 | 16 | # This server was created 17 | RPL_CREATED = 3 18 | 19 | # 20 | RPL_MYINFO = 4 21 | 22 | # Try server , port 23 | RPL_BOUNCE = 5 24 | 25 | # :*1 *( " " ) 26 | RPL_USERHOST = 302 27 | 28 | # :*1 *( " )" 29 | RPL_ISON = 303 30 | 31 | # " :" 32 | RPL_AWAY = 301 33 | 34 | # :You are no longer marked as being away 35 | RPL_UNAWAY = 305 36 | 37 | # :You have been marked as being away 38 | RPL_NOWAWAY = 306 39 | 40 | # * : 41 | RPL_WHOISUSER = 311 42 | 43 | # : 44 | RPL_WHOISSERVER = 312 45 | 46 | # :is an IRC operator 47 | RPL_WHOISOPERATOR = 313 48 | 49 | # :seconds idle 50 | RPL_WHOISIDLE = 317 51 | 52 | # :End of WHOIS list 53 | RPL_ENDOFWHOIS = 318 54 | 55 | # :*( ( "@" / "+" ) " " ) 56 | RPL_WHOISCHANNELS = 319 57 | 58 | # * : 59 | RPL_WHOWASUSER = 314 60 | 61 | # :End of WHOWAS 62 | RPL_ENDOFWHOWAS = 369 63 | 64 | # Obsolete. Not used. 65 | RPL_LISTSTART = 321 66 | 67 | # <# visible> : 68 | RPL_LIST = 322 69 | 70 | # :End of LIST 71 | RPL_LISTEND = 323 72 | 73 | # 74 | RPL_UNIQOPIS = 325 75 | 76 | # 77 | RPL_CHANNELMODEIS = 324 78 | 79 | # :No topic is set 80 | RPL_NOTOPIC = 331 81 | 82 | # : 83 | RPL_TOPIC = 332 84 | 85 | # 86 | RPL_INVITING = 341 87 | 88 | # :Summoning user to IRC 89 | RPL_SUMMONING = 342 90 | 91 | # 92 | RPL_INVITELIST = 346 93 | 94 | # :End of channel invite list 95 | RPL_ENDOFINVITELIST = 347 96 | 97 | # 98 | RPL_EXCEPTLIST = 348 99 | 100 | # :End of channel exception list 101 | RPL_ENDOFEXCEPTLIST = 349 102 | 103 | # . : 104 | RPL_VERSION = 351 105 | 106 | #" 107 | #( "H" / "G" > ["*"] [ ( "@" / "+" ) ] 108 | #: " 109 | RPL_WHOREPLY = 352 110 | 111 | # :End of WHO list 112 | RPL_ENDOFWHO = 315 113 | 114 | # "( "=" / "*" / "@" ) 115 | # :[ "@" / "+" ] *( " " [ "@" / "+" ] ) 116 | # - "@" is used for secret channels, "*" for private 117 | # channels, and "=" for others (public channels). 118 | RPL_NAMREPLY = 353 119 | 120 | # :End of NAMES list 121 | RPL_ENDOFNAMES = 366 122 | 123 | # : 124 | RPL_LINKS = 364 125 | 126 | # :End of LINKS list 127 | RPL_ENDOFLINKS = 365 128 | 129 | # 130 | RPL_BANLIST = 367 131 | 132 | # :End of channel ban list 133 | RPL_ENDOFBANLIST = 368 134 | 135 | # : 136 | RPL_INFO = 371 137 | 138 | # :End of INFO list 139 | RPL_ENDOFINFO = 374 140 | 141 | # :- Message of the day - 142 | RPL_MOTDSTART = 375 143 | 144 | # :- 145 | RPL_MOTD = 372 146 | 147 | # :End of MOTD command 148 | RPL_ENDOFMOTD = 376 149 | 150 | # :You are now an IRC operator 151 | RPL_YOUREOPER = 381 152 | 153 | # :Rehashing 154 | RPL_REHASHING = 382 155 | 156 | # You are service 157 | RPL_YOURESERVICE = 383 158 | 159 | # : 160 | RPL_TIME = 391 161 | 162 | # :UserID Terminal Host 163 | RPL_USERSSTART = 392 164 | 165 | # : 166 | RPL_USERS = 393 167 | 168 | # :End of users 169 | RPL_ENDOFUSERS = 394 170 | 171 | # :Nobody logged in 172 | RPL_NOUSERS = 395 173 | 174 | # Link 175 | # V 176 | # 177 | # 178 | RPL_TRACELINK = 200 179 | 180 | # Try. 181 | RPL_TRACECONNECTING = 201 182 | 183 | # H.S. 184 | RPL_TRACEHANDSHAKE = 202 185 | 186 | # ???? [] 187 | RPL_TRACEUNKNOWN = 203 188 | 189 | # Oper 190 | RPL_TRACEOPERATOR = 204 191 | 192 | # User 193 | RPL_TRACEUSER = 205 194 | 195 | # Serv S C 196 | # @ V 197 | RPL_TRACESERVER = 206 198 | 199 | # Service 200 | RPL_TRACESERVICE = 207 201 | 202 | # 0 203 | RPL_TRACENEWTYPE = 208 204 | 205 | # Class 206 | RPL_TRACECLASS = 209 207 | 208 | # Unused. 209 | RPL_TRACERECONNECT = 210 210 | 211 | # File 212 | RPL_TRACELOG = 261 213 | 214 | # :End of TRACE 215 | RPL_TRACEEND = 262 216 | 217 | # 218 | # 219 | #