├── .devassistant ├── .gitignore ├── .mailmap ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.md ├── alembic ├── README ├── env.py ├── script.py.mako └── versions │ ├── 140c0dd3605_added_fields_for_local_remote_sockets_.py │ ├── 20c114cc162_initial_migration.py │ └── 50135956b24_added_ssl_fields_in_network_table.py ├── ircb ├── __init__.py ├── bouncer.py ├── cli │ ├── __init__.py │ ├── loaddata.py │ ├── main.py │ ├── network.py │ ├── run.py │ └── user.py ├── config │ ├── __init__.py │ ├── default_settings.py │ ├── sample_ssl.cert │ └── sample_ssl.key ├── connection.py ├── forms │ ├── __init__.py │ ├── network.py │ └── user.py ├── identd.py ├── irc │ ├── __init__.py │ └── plugins │ │ ├── __init__.py │ │ ├── autojoins.py │ │ ├── ircb.py │ │ └── logger.py ├── lib │ ├── __init__.py │ ├── async.py │ ├── constants │ │ ├── __init__.py │ │ └── signals.py │ └── dispatcher │ │ └── __init__.py ├── models │ ├── __init__.py │ ├── channel.py │ ├── client.py │ ├── lib.py │ ├── logs.py │ ├── network.py │ └── user.py ├── publishers │ ├── __init__.py │ ├── base.py │ ├── channels.py │ ├── logs.py │ └── networks.py ├── storeclient │ ├── __init__.py │ ├── base.py │ ├── channel.py │ ├── client.py │ ├── logs.py │ ├── network.py │ └── user.py ├── stores │ ├── __init__.py │ ├── base.py │ ├── channel.py │ ├── client.py │ ├── logs.py │ ├── network.py │ └── user.py ├── utils │ ├── __init__.py │ └── config.py └── web │ ├── __init__.py │ ├── app.py │ ├── decorators.py │ ├── lib.py │ ├── network.py │ └── user.py ├── requirements.txt ├── setup.py ├── tests ├── README.md └── connection_tests.py └── tox.ini /.devassistant: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - rpm: 3 | - python${py3} 4 | - if $venv: 5 | - rpm: 6 | - python-virtualenv 7 | - else: 8 | - rpm: 9 | - python-setuptools 10 | devassistant_version: 0.11.1 11 | original_kwargs: 12 | name: ircb 13 | venv: '' 14 | project_type: 15 | - python 16 | - lib 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.sw[po] 3 | 4 | # Packages 5 | *.egg 6 | *.egg-info 7 | dist 8 | build 9 | sdist 10 | 11 | *.sqlite3 12 | *.db 13 | 14 | */config/networks.json 15 | */config/users.json 16 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | PolBaladas PolBaladas 2 | Ratnadeep Debnath Ratnadeep Debnath 3 | Sayan Chowdhury Sayan Chowdhury 4 | Shashank Vivek Shashank Vivek 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "3.4" 5 | - "3.5" 6 | install: pip install tox-travis 7 | script: tox -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 2 | 0.3.0 3 | ----- 4 | 5 | Pull Requests 6 | 7 | - (@rtnpro) #51, Implement basic API server for ircb 8 | https://github.com/waartaa/ircb/pull/51 9 | - (@slick666) #53, Changed update method to have immutable arguments. 10 | https://github.com/waartaa/ircb/pull/53 11 | - (@sayanchowdhury) #55, Add default value to Network.status 12 | https://github.com/waartaa/ircb/pull/55 13 | - (@slick666) #57, Load ircb data from file 14 | https://github.com/waartaa/ircb/pull/57 15 | - (@sayanchowdhury) #59, Implement logging, unified import layout 16 | https://github.com/waartaa/ircb/pull/59 17 | - (@rtnpro) #68, Implement logstore plugin 18 | https://github.com/waartaa/ircb/pull/68 19 | - (@rtnpro) #70, Realtime publishers 20 | https://github.com/waartaa/ircb/pull/70 21 | - (@sayanchowdhury) #72, Fix the NetworkStore.create() to use the proper arguments 22 | https://github.com/waartaa/ircb/pull/72 23 | - (@rtnpro) #76, Forward sent channel messages for a bot client to it's siblings. 24 | https://github.com/waartaa/ircb/pull/76 25 | - (@rtnpro) #78, Allow running bouncer server with SSL. #77 26 | https://github.com/waartaa/ircb/pull/78 27 | - (@sayanchowdhury) #56, Implement IDENT server. 28 | https://github.com/waartaa/ircb/pull/56 29 | - (@rtnpro) #79, Allinone run now runs web server as well. 30 | https://github.com/waartaa/ircb/pull/79 31 | - (@farhaanbukhsh) #80, Readdme fix 32 | https://github.com/waartaa/ircb/pull/80 33 | - (@farhaanbukhsh) #83, Fix readme for development 34 | https://github.com/waartaa/ircb/pull/83 35 | - (@rtnpro) #84, Cli verbose option 36 | https://github.com/waartaa/ircb/pull/84 37 | - (@rtnpro) #85, Cli fix allinone 38 | https://github.com/waartaa/ircb/pull/85 39 | - (@sayanchowdhury) #86, Change the filetype of the CHANGELOG file 40 | https://github.com/waartaa/ircb/pull/86 41 | - (@sayanchowdhury) #87, Update the MANIFEST.in file 42 | https://github.com/waartaa/ircb/pull/87 43 | - (@sayanchowdhury) #88, Fix the license classifier in setup.py 44 | https://github.com/waartaa/ircb/pull/88 45 | 46 | Commits 47 | 48 | - 91f4d9fd8 Added to_dict() method to models. 49 | https://github.com/waartaa/ircb/commit/91f4d9fd8 50 | - 0dbd9d51b Zeromq based distributed dispatcher. 51 | https://github.com/waartaa/ircb/commit/0dbd9d51b 52 | - 97122e7f9 Represent model instances as dict in store response. 53 | https://github.com/waartaa/ircb/commit/97122e7f9 54 | - ebf79a247 Deserialize dicts in store response to model instances in storeclient. 55 | https://github.com/waartaa/ircb/commit/ebf79a247 56 | - 5b5bfbaad Return ChoiceField's code in serialized model data. 57 | https://github.com/waartaa/ircb/commit/5b5bfbaad 58 | - 7c2bd0568 Allow locks and enqueuing messages in dispatcher. 59 | https://github.com/waartaa/ircb/commit/7c2bd0568 60 | - 1d54c7187 Sync and register storeclient subscribers to store publisher. 61 | https://github.com/waartaa/ircb/commit/1d54c7187 62 | - c58010396 Updated README. 63 | https://github.com/waartaa/ircb/commit/c58010396 64 | - 363ed45b6 Added missing run.py in cli module. 65 | https://github.com/waartaa/ircb/commit/363ed45b6 66 | - f48bdd976 Connect to IRC when status of NetworkStore is set to '0' (connect). 67 | https://github.com/waartaa/ircb/commit/f48bdd976 68 | - 6dd53ab28 Fix registering first client to bot 69 | https://github.com/waartaa/ircb/commit/6dd53ab28 70 | - 6eb6c9eaa Initialize dispatcher for storeclient for CLI. 71 | https://github.com/waartaa/ircb/commit/6eb6c9eaa 72 | - 0269e778f Disconnect bot when network status set to '2'. 73 | https://github.com/waartaa/ircb/commit/0269e778f 74 | - bd7f9ae68 Added list, connect, disconnect commands for ircb networks. 75 | https://github.com/waartaa/ircb/commit/bd7f9ae68 76 | - 8ca1d5598 Don't run process_queue in Dispatcher as a long running task. 77 | https://github.com/waartaa/ircb/commit/8ca1d5598 78 | - 9fd8cf5c9 Speed dispatcher init process. 79 | https://github.com/waartaa/ircb/commit/9fd8cf5c9 80 | - 2d89224c3 Use loop.run_forever() to run dispatcher server. 81 | https://github.com/waartaa/ircb/commit/2d89224c3 82 | - 0f83f99b1 Don't keep redis connection open in dispatcher. 83 | https://github.com/waartaa/ircb/commit/0f83f99b1 84 | - 3c7695da0 Allow re connecting a disconnected bot. 85 | https://github.com/waartaa/ircb/commit/3c7695da0 86 | - 2503ac2d0 Prevent race condition when registering subscribers to stores. 87 | https://github.com/waartaa/ircb/commit/2503ac2d0 88 | - af24973b4 Scrap previous flask web app. 89 | https://github.com/waartaa/ircb/commit/af24973b4 90 | - 5273a3282 Allow authenticating a user from stores. 91 | https://github.com/waartaa/ircb/commit/5273a3282 92 | - 1b03583f3 Initial work on web API. 93 | https://github.com/waartaa/ircb/commit/1b03583f3 94 | - 160f16218 Use aiohttp_auth to improve Sign{In,Out} API for user. 95 | https://github.com/waartaa/ircb/commit/160f16218 96 | - e48a9f777 Added new requirements. 97 | https://github.com/waartaa/ircb/commit/e48a9f777 98 | - 01286e0dd Implement auth_required decorator and SignoutView. 99 | https://github.com/waartaa/ircb/commit/01286e0dd 100 | - a24b2d5c2 Allow storclient stores to list available fields. 101 | https://github.com/waartaa/ircb/commit/a24b2d5c2 102 | - a0160060c Added base web Views for ircb with some utilities: 103 | https://github.com/waartaa/ircb/commit/a0160060c 104 | - a6435dc76 Added view to list IRC networks for a logged in user. 105 | https://github.com/waartaa/ircb/commit/a6435dc76 106 | - 461ef51ba Remove unused import. 107 | https://github.com/waartaa/ircb/commit/461ef51ba 108 | - 81e5e820a Fix to_dict() in network model. 109 | https://github.com/waartaa/ircb/commit/81e5e820a 110 | - 93c7c4824 Initial implementation of network details, create API. 111 | https://github.com/waartaa/ircb/commit/93c7c4824 112 | - bc3e90c6a Allow generating a serializable dict representation for a model row. 113 | https://github.com/waartaa/ircb/commit/bc3e90c6a 114 | - 8156135eb Explicitly specify API version: v1 in routes. 115 | https://github.com/waartaa/ircb/commit/8156135eb 116 | - 9fb31cbdd Cleaned up network model definition. 117 | https://github.com/waartaa/ircb/commit/9fb31cbdd 118 | - 0c7011504 Added wtforms-alchemy as a requirement. 119 | https://github.com/waartaa/ircb/commit/0c7011504 120 | - 25e5f31af Added NetworkForm. 121 | https://github.com/waartaa/ircb/commit/25e5f31af 122 | - a92a01752 Use NetworkForm to validate data in network create API. 123 | https://github.com/waartaa/ircb/commit/a92a01752 124 | - b83c48865 Add network update API. 125 | https://github.com/waartaa/ircb/commit/b83c48865 126 | - d1ad19f08 Added network connect/disconnect API. 127 | https://github.com/waartaa/ircb/commit/d1ad19f08 128 | - 3a8f01e78 Added user signup API. 129 | https://github.com/waartaa/ircb/commit/3a8f01e78 130 | - 97182313f Added user form. 131 | https://github.com/waartaa/ircb/commit/97182313f 132 | - f52170cc2 fix the default utcnow time for generating access token 133 | https://github.com/waartaa/ircb/commit/f52170cc2 134 | - ce4f2a774 Changed update method to have immutable arguments. bumped SQLAlchemy to 1.0.9 to get the one_or_none function bumped click to 6.3 because I couldn't get it to with 6.2 Added msgpack-python 0.4.7 to suppress error 135 | https://github.com/waartaa/ircb/commit/ce4f2a774 136 | - c900ded69 models: add default value to Network.status 137 | https://github.com/waartaa/ircb/commit/c900ded69 138 | - 96cb513eb Load ircb data from file. Fixes #25 139 | https://github.com/waartaa/ircb/commit/96cb513eb 140 | - b9e194629 Minor typo fix and update requirements.txt 141 | https://github.com/waartaa/ircb/commit/b9e194629 142 | - 6012aaeac Fix the import layout, Implement logging framework 143 | https://github.com/waartaa/ircb/commit/6012aaeac 144 | - e596ff271 Added Tox testing configuration Added simple documentation under the tests folder Added trivial test under the tests folder did simple pep8 cleanup to fix the pep8 tox test Added TravisCI integration 145 | https://github.com/waartaa/ircb/commit/e596ff271 146 | - 0bf606744 Added CLI to launch web server. 147 | https://github.com/waartaa/ircb/commit/0bf606744 148 | - 1283b735c Bumped SQLAlchemy version to 1.0.13. 149 | https://github.com/waartaa/ircb/commit/1283b735c 150 | - 58d0d67e0 Updated README. 151 | https://github.com/waartaa/ircb/commit/58d0d67e0 152 | - 9fc44d7e7 Bump to version 0.2 153 | https://github.com/waartaa/ircb/commit/9fc44d7e7 154 | - c4da551a6 Added models for storing IRC logs. #64 155 | https://github.com/waartaa/ircb/commit/c4da551a6 156 | - f87c3a501 Store user id in irc3 bot instance. #64 157 | https://github.com/waartaa/ircb/commit/f87c3a501 158 | - e05d7658e Added stores for MessageLog and ActivityLog. #64 159 | https://github.com/waartaa/ircb/commit/e05d7658e 160 | - 36ab0ecfc Added plugin for storing IRC logs. #64 161 | https://github.com/waartaa/ircb/commit/36ab0ecfc 162 | - 256dedbab Scaffolding realtime publisher. 163 | https://github.com/waartaa/ircb/commit/256dedbab 164 | - 1ae93af39 Allow fetching raw results from storeclient 165 | https://github.com/waartaa/ircb/commit/1ae93af39 166 | - e94e77421 Support fetching logs from MessageLog store. #71 167 | https://github.com/waartaa/ircb/commit/e94e77421 168 | - 06eb88549 Working MessageLog publisher. Fixes #71 169 | https://github.com/waartaa/ircb/commit/06eb88549 170 | - b9a37b32e Fixed pep8 errors. 171 | https://github.com/waartaa/ircb/commit/b9a37b32e 172 | - 8cad048cb Fixed out of place debuggers in publisher. 173 | https://github.com/waartaa/ircb/commit/8cad048cb 174 | - 13954a49d Optimize skip filters for MessageLogPublisher. #71 175 | https://github.com/waartaa/ircb/commit/13954a49d 176 | - 7fcacfd6d Optimize query to fetch latest N MessageLogs. #71 177 | https://github.com/waartaa/ircb/commit/7fcacfd6d 178 | - 18ca5807e In publisher, clean up item from index when removed from results. 179 | https://github.com/waartaa/ircb/commit/18ca5807e 180 | - a71308eff Allow adding callbacks to create/update events of publisher. 181 | https://github.com/waartaa/ircb/commit/a71308eff 182 | - 090608bf1 Bugfixes in ircb publisher. 183 | https://github.com/waartaa/ircb/commit/090608bf1 184 | - 13e1202e5 Added 'fetch' callback in publisher. 185 | https://github.com/waartaa/ircb/commit/13e1202e5 186 | - a508196c6 Fix serializing message logs when publishing from stores. 187 | https://github.com/waartaa/ircb/commit/a508196c6 188 | - 66bb47044 Created a base publisher class 189 | https://github.com/waartaa/ircb/commit/66bb47044 190 | - 8c3b16ebb Fix the NetworkStore.create() to use the proper arguments 191 | https://github.com/waartaa/ircb/commit/8c3b16ebb 192 | - 6883f7e6f Some fixes in base publisher. 193 | https://github.com/waartaa/ircb/commit/6883f7e6f 194 | - 1a78ab15e Added network publisher. 195 | https://github.com/waartaa/ircb/commit/1a78ab15e 196 | - 572533017 Improve GET api for channel stores. Fixes #73 197 | https://github.com/waartaa/ircb/commit/572533017 198 | - bebf7d7e0 Add realtime publisher for channels. Fixes #74 199 | https://github.com/waartaa/ircb/commit/bebf7d7e0 200 | - 8aa6a32dd Fix serializing & deserializing data. Fixes #75 201 | https://github.com/waartaa/ircb/commit/8aa6a32dd 202 | - e41a5fa4b Added 'id' property to ChannelPublisher. #75 203 | https://github.com/waartaa/ircb/commit/e41a5fa4b 204 | - f1fe34e2f Fixed irc3 integration bug: USER not enough params. 205 | https://github.com/waartaa/ircb/commit/f1fe34e2f 206 | - 643d279da Don't remove builtins from settings module. 207 | https://github.com/waartaa/ircb/commit/643d279da 208 | - ab232620b Forward sent channel messages for a bot client to it's siblings. 209 | https://github.com/waartaa/ircb/commit/ab232620b 210 | - d1f7d8bac Update to irc3 0.9.3 211 | https://github.com/waartaa/ircb/commit/d1f7d8bac 212 | - 084b24d5a Use network.username, if available, for irc3 bot config 213 | https://github.com/waartaa/ircb/commit/084b24d5a 214 | - 97ff477aa Allow running bouncer server with SSL. #77 215 | https://github.com/waartaa/ircb/commit/97ff477aa 216 | - cc6ea1a07 Initial work on the ident server 217 | https://github.com/waartaa/ircb/commit/cc6ea1a07 218 | - 3508fa23d Implement working identd server. 219 | https://github.com/waartaa/ircb/commit/3508fa23d 220 | - 3894e86c6 Refactor run command to open room for identd server. 221 | https://github.com/waartaa/ircb/commit/3894e86c6 222 | - 15f69504e Refactor allinone CLI command to move implementation outside of bouncer. 223 | https://github.com/waartaa/ircb/commit/15f69504e 224 | - 7200a4321 Integrate identd server with CLI 225 | https://github.com/waartaa/ircb/commit/7200a4321 226 | - c5dd25b75 Fix implementation of identd server 227 | https://github.com/waartaa/ircb/commit/c5dd25b75 228 | - 78ca6ac2d Bugfix during saving connection info for a IRC connection. 229 | https://github.com/waartaa/ircb/commit/78ca6ac2d 230 | - 4a11950d4 Allinone run now runs web server as well. 231 | https://github.com/waartaa/ircb/commit/4a11950d4 232 | - a1ca0b3b7 Fix setup file 233 | https://github.com/waartaa/ircb/commit/a1ca0b3b7 234 | - 7a3242126 Fix readme and add intructions for development 235 | https://github.com/waartaa/ircb/commit/7a3242126 236 | - 254596c0b Add virtualwrapper dependency 237 | https://github.com/waartaa/ircb/commit/254596c0b 238 | - 8800da6c9 Fix readme for development 239 | https://github.com/waartaa/ircb/commit/8800da6c9 240 | - 4b45e5b3c Minor fix in readme 241 | https://github.com/waartaa/ircb/commit/4b45e5b3c 242 | - a5a273942 Add steps to configure IRC client 243 | https://github.com/waartaa/ircb/commit/a5a273942 244 | - d29f9943b Add verbose option in ircb CLI. 245 | https://github.com/waartaa/ircb/commit/d29f9943b 246 | - fda09d34e Show status message when running stores server. 247 | https://github.com/waartaa/ircb/commit/fda09d34e 248 | - 929482ee1 Show bouncer endpoint when running in allinone mode. 249 | https://github.com/waartaa/ircb/commit/929482ee1 250 | - 8d6d82f11 Drop unused variable in bouncer. 251 | https://github.com/waartaa/ircb/commit/8d6d82f11 252 | - 55b3079bf Make running identd in allinone mode optional. 253 | https://github.com/waartaa/ircb/commit/55b3079bf 254 | - b61bae91f Print web server endpoint during startup. 255 | https://github.com/waartaa/ircb/commit/b61bae91f 256 | - 3c51e2b5c Change the filetype of the CHANGELOG file 257 | https://github.com/waartaa/ircb/commit/3c51e2b5c 258 | - 1b30a0fbf Update the MANIFEST.in file 259 | https://github.com/waartaa/ircb/commit/1b30a0fbf 260 | - 9a4a4b25b Fix the license classifier in setup.py 261 | https://github.com/waartaa/ircb/commit/9a4a4b25b 262 | # Change Log 263 | 264 | ## [0.2](https://github.com/waartaa/ircb/tree/0.2) (2016-05-29) 265 | 266 | [Full Changelog](https://github.com/waartaa/ircb/compare/0.1.1...0.2) 267 | 268 | **Closed issues:** 269 | 270 | - asyncio sqlalchemy compatability? [\#50](https://github.com/waartaa/ircb/issues/50) 271 | - not able to connect python to mysql [\#46](https://github.com/waartaa/ircb/issues/46) 272 | - /.meteor/meteor' is not executable. [\#45](https://github.com/waartaa/ircb/issues/45) 273 | - python.h directory is not there [\#44](https://github.com/waartaa/ircb/issues/44) 274 | - Add a logging framework [\#43](https://github.com/waartaa/ircb/issues/43) 275 | - Replace in memory dispatcher with one based on zeromq [\#30](https://github.com/waartaa/ircb/issues/30) 276 | 277 | **Merged pull requests:** 278 | 279 | - Implement logging, unified import layout [\#59](https://github.com/waartaa/ircb/pull/59) ([sayanchowdhury](https://github.com/sayanchowdhury)) 280 | - Minor typo fix and update requirements.txt [\#58](https://github.com/waartaa/ircb/pull/58) ([sayanchowdhury](https://github.com/sayanchowdhury)) 281 | - Load ircb data from file [\#57](https://github.com/waartaa/ircb/pull/57) ([slick666](https://github.com/slick666)) 282 | - Add default value to Network.status [\#55](https://github.com/waartaa/ircb/pull/55) ([sayanchowdhury](https://github.com/sayanchowdhury)) 283 | - Proposed Tox and TravisCI for CI/CD [\#54](https://github.com/waartaa/ircb/pull/54) ([slick666](https://github.com/slick666)) 284 | - Changed update method to have immutable arguments. [\#53](https://github.com/waartaa/ircb/pull/53) ([slick666](https://github.com/slick666)) 285 | - Implement basic API server for ircb [\#51](https://github.com/waartaa/ircb/pull/51) ([rtnpro](https://github.com/rtnpro)) 286 | - Server side flux for ircb [\#48](https://github.com/waartaa/ircb/pull/48) ([rtnpro](https://github.com/rtnpro)) 287 | - Zmq dispatcher. Fixes \#30 [\#41](https://github.com/waartaa/ircb/pull/41) ([rtnpro](https://github.com/rtnpro)) 288 | 289 | ## [0.1.1](https://github.com/waartaa/ircb/tree/0.1.1) (2016-01-01) 290 | [Full Changelog](https://github.com/waartaa/ircb/compare/0.1...0.1.1) 291 | 292 | **Fixed bugs:** 293 | 294 | - Fix marking user as not AWAY [\#36](https://github.com/waartaa/ircb/issues/36) 295 | - Fix sending AWAY command [\#32](https://github.com/waartaa/ircb/issues/32) 296 | 297 | **Merged pull requests:** 298 | 299 | - Fix marking nick as not AWAY. Fixes \#36 [\#37](https://github.com/waartaa/ircb/pull/37) ([rtnpro](https://github.com/rtnpro)) 300 | - Revert "Fix marking user as away. \#32" [\#35](https://github.com/waartaa/ircb/pull/35) ([rtnpro](https://github.com/rtnpro)) 301 | - Fix marking user as away. \#32 [\#34](https://github.com/waartaa/ircb/pull/34) ([rtnpro](https://github.com/rtnpro)) 302 | 303 | ## [0.1](https://github.com/waartaa/ircb/tree/0.1) (2015-12-20) 304 | **Implemented enhancements:** 305 | 306 | - Load ircb data from file [\#25](https://github.com/waartaa/ircb/issues/25) 307 | - Autjoin previously joined channels when connected to IRC server [\#27](https://github.com/waartaa/ircb/issues/27) 308 | - Add support for ssl, ssl\_verify fields in "ircb networks create" command [\#22](https://github.com/waartaa/ircb/issues/22) 309 | - Connect to IRC server using SSL [\#18](https://github.com/waartaa/ircb/issues/18) 310 | - Allow SSL connection to IRC networks [\#17](https://github.com/waartaa/ircb/issues/17) 311 | - Dynamically generate IRC joining messages for clients when reusing existing IRC connection [\#11](https://github.com/waartaa/ircb/issues/11) 312 | 313 | **Fixed bugs:** 314 | 315 | - Don't send ChoiceType field in IrcbBot config for network for ssl\_verify [\#23](https://github.com/waartaa/ircb/issues/23) 316 | - Fix handling multiple IRC clients for same bot [\#8](https://github.com/waartaa/ircb/issues/8) 317 | 318 | **Closed issues:** 319 | 320 | - ircb networks create does not work [\#20](https://github.com/waartaa/ircb/issues/20) 321 | - Monthly release for ircb [\#10](https://github.com/waartaa/ircb/issues/10) 322 | - Sent messages using IrcbIrcBot is prefixed with ':' [\#6](https://github.com/waartaa/ircb/issues/6) 323 | - Prevent race condition during two clients trying to connect to the same IRC network [\#15](https://github.com/waartaa/ircb/issues/15) 324 | 325 | **Merged pull requests:** 326 | 327 | - Autjoin previously joined channels when connecting to IRC server. [\#28](https://github.com/waartaa/ircb/pull/28) ([rtnpro](https://github.com/rtnpro)) 328 | - Implemented: Add support for ssl, ssl\_verify fields in 'ircb networks create' command. issue \#22 [\#24](https://github.com/waartaa/ircb/pull/24) ([PolBaladas](https://github.com/PolBaladas)) 329 | - Fixed issue \#20 'ircb network create does not work' [\#21](https://github.com/waartaa/ircb/pull/21) ([PolBaladas](https://github.com/PolBaladas)) 330 | - Allow connecting to IRC server using SSL. Fixes \#18 [\#19](https://github.com/waartaa/ircb/pull/19) ([rtnpro](https://github.com/rtnpro)) 331 | - Fixes race condition during multiple clients connecting to same network [\#16](https://github.com/waartaa/ircb/pull/16) ([rtnpro](https://github.com/rtnpro)) 332 | - Dynamic irc join messages [\#14](https://github.com/waartaa/ircb/pull/14) ([rtnpro](https://github.com/rtnpro)) 333 | - Add local & remote socket info for a IRC network connection [\#13](https://github.com/waartaa/ircb/pull/13) ([rtnpro](https://github.com/rtnpro)) 334 | - Add alembic to manage migration [\#12](https://github.com/waartaa/ircb/pull/12) ([rtnpro](https://github.com/rtnpro)) 335 | - Improve handling of raw messages from IRC clients. Fixes \#6 [\#7](https://github.com/waartaa/ircb/pull/7) ([rtnpro](https://github.com/rtnpro)) 336 | - Use irc3 bot to interact with remote IRC server. [\#5](https://github.com/waartaa/ircb/pull/5) ([rtnpro](https://github.com/rtnpro)) 337 | - Added stores for ircb [\#4](https://github.com/waartaa/ircb/pull/4) ([rtnpro](https://github.com/rtnpro)) 338 | - minor typo fix in README [\#3](https://github.com/waartaa/ircb/pull/3) ([sayanchowdhury](https://github.com/sayanchowdhury)) 339 | - update the README for setting up ircb on dev environments [\#2](https://github.com/waartaa/ircb/pull/2) ([sayanchowdhury](https://github.com/sayanchowdhury)) 340 | - Update installation docs from pip to pip3 [\#1](https://github.com/waartaa/ircb/pull/1) ([sayanchowdhury](https://github.com/sayanchowdhury)) 341 | 342 | 343 | 344 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* 345 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 ircb 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, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include requirements.txt 3 | include CHANGELOG.rst 4 | include alembic.ini 5 | include tox.ini 6 | recursive-include *.py 7 | recursive-include alembic * 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ircb 3 | 4 | A versatile IRC bouncer. 5 | 6 | ## Requirements 7 | 8 | - Python3.5 9 | - Pip3.5 10 | 11 | 12 | ## Setup 13 | 14 | - Install dependencies: 15 | 16 | ``[sudo] pip3.5 install -r requirements.txt`` 17 | 18 | - Copy and extend 19 | 20 | ``ircb/config/default_settings.py``, as needed, to a custom location. say, ``/etc/ircb/settings.py``. 21 | 22 | - [OPTIONAL] ``export IRCB_SETTINGS=`` 23 | 24 | - Install the project as a development dep 25 | 26 | ``python3.5 setup.py develop`` 27 | 28 | ## Setup for development 29 | 30 | - Install system dependencies: 31 | 32 | `` sudo dnf install python3-devel openssl-devel redis`` 33 | 34 | `` sudo pip install virtualenvwrapper`` 35 | 36 | - Make `python3` virtualenv: 37 | 38 | ``mkvirtualenv --python=/usr/bin/python3 python3`` 39 | 40 | - Activate virtualenv: 41 | 42 | ``workon python3`` 43 | 44 | - Install dependencies: 45 | 46 | ``pip3 install -r requirements.txt`` 47 | 48 | - Install the project as development dep: 49 | 50 | ``python3.5 setup.py develop`` 51 | 52 | - Make sure `REDIS` is running: 53 | 54 | ``sudo systemctl start redis.service`` 55 | 56 | - Now, you need to run ``ircb stores``: 57 | 58 | ``ircb run stores`` 59 | 60 | Continue with `` Setting up data`` 61 | 62 | ## Setting up data 63 | - Creating a user: 64 | ``` 65 | ircb users create USERNAME EMAIL [PASSWORD] 66 | ``` 67 | 68 | - Creating a network for a user: 69 | ``` 70 | ircb networks create USER NETWORK_NAME HOST PORT NICK 71 | ``` 72 | You'll get an access token as an output of the above. Use this as 73 | **server password** when configuring your IRC client to connect to ``ircb``. 74 | 75 | ## Running the app 76 | 77 | ### Quickstart 78 | ``` 79 | sudo ircb run allinone 80 | 81 | ``` 82 | 83 | Note: If you are using virtualenv `sudo` will not work this way, you need to 84 | run: 85 | 86 | ``` 87 | sudo ~/.virtualenvs/python3/bin/ircb run allinone 88 | 89 | ``` 90 | ### Advanced 91 | 92 | You can run the various components of ``ircb``: ``stores``, ``bouncers`` as 93 | different processes. 94 | 95 | - Run stores as a different process: ``ircb run stores`` 96 | - Run bouncer: ``ircb run bouncer`` 97 | - Run web server: ``ircb run web`` 98 | - Run identd server: ``sudo ircb run identd`` 99 | 100 | ## Connecting for IRC client 101 | 102 | Now, you should be able to connect to ``ircb`` from your IRC client at: 103 | 104 | - host/port: ``localhost/9000`` 105 | 106 | - server password: ```` 107 | 108 | - IRC client should have the following settings enabled: 109 | 110 | * Use SSL for all server on this network 111 | * Accept invalid SSL certificate 112 | 113 | ### Configure HexChat 114 | 115 | - Go to HexChat -> Network List 116 | 117 | - Change the nick to the nick you have given while configuring network 118 | 119 | - Under ``Network`` Click `Add` and name the server ``ircb`` 120 | 121 | - Click on `Edit` then `Add` and type `localhost/9000` 122 | 123 | - Under ``Server`` tab check the ``SSL`` option mentioned above 124 | 125 | - Enter the ``Server Password`` in `Password` field 126 | 127 | - Close the dialog box and then connect to the network 128 | 129 | Note: In case the problem persist try to ``restart`` ircb server 130 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | 6 | # this is the Alembic Config object, which provides 7 | # access to the values within the .ini file in use. 8 | config = context.config 9 | 10 | # Interpret the config file for Python logging. 11 | # This line sets up loggers basically. 12 | fileConfig(config.config_file_name) 13 | 14 | # add your model's MetaData object here 15 | # for 'autogenerate' support 16 | # from myapp import mymodel 17 | # target_metadata = mymodel.Base.metadata 18 | from ircb.models import Base 19 | target_metadata = Base.metadata 20 | 21 | # other values from the config, defined by the needs of env.py, 22 | # can be acquired: 23 | # my_important_option = config.get_main_option("my_important_option") 24 | # ... etc. 25 | 26 | 27 | def run_migrations_offline(): 28 | """Run migrations in 'offline' mode. 29 | 30 | This configures the context with just a URL 31 | and not an Engine, though an Engine is acceptable 32 | here as well. By skipping the Engine creation 33 | we don't even need a DBAPI to be available. 34 | 35 | Calls to context.execute() here emit the given string to the 36 | script output. 37 | 38 | """ 39 | from ircb.config import settings 40 | url = settings._DB_URI 41 | context.configure( 42 | url=url, target_metadata=target_metadata, literal_binds=True) 43 | 44 | with context.begin_transaction(): 45 | context.run_migrations() 46 | 47 | 48 | def run_migrations_online(): 49 | """Run migrations in 'online' mode. 50 | 51 | In this scenario we need to create an Engine 52 | and associate a connection with the context. 53 | 54 | """ 55 | alembic_config = config.get_section(config.config_ini_section) 56 | from ircb.config import settings 57 | alembic_config['sqlalchemy.url'] = settings.DB_URI 58 | alembic_config['include_schemas'] = True 59 | connectable = engine_from_config( 60 | alembic_config, 61 | prefix='sqlalchemy.', 62 | poolclass=pool.NullPool) 63 | 64 | with connectable.connect() as connection: 65 | context.configure( 66 | connection=connection, 67 | target_metadata=target_metadata 68 | ) 69 | 70 | with context.begin_transaction(): 71 | context.run_migrations() 72 | 73 | if context.is_offline_mode(): 74 | run_migrations_offline() 75 | else: 76 | run_migrations_online() 77 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = ${repr(up_revision)} 11 | down_revision = ${repr(down_revision)} 12 | branch_labels = ${repr(branch_labels)} 13 | depends_on = ${repr(depends_on)} 14 | 15 | from alembic import op 16 | import sqlalchemy as sa 17 | ${imports if imports else ""} 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /alembic/versions/140c0dd3605_added_fields_for_local_remote_sockets_.py: -------------------------------------------------------------------------------- 1 | """Added fields for local & remote sockets to network table 2 | 3 | Revision ID: 140c0dd3605 4 | Revises: 20c114cc162 5 | Create Date: 2015-11-29 14:40:04.862179 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '140c0dd3605' 11 | down_revision = '20c114cc162' 12 | branch_labels = None 13 | depends_on = None 14 | 15 | from alembic import op 16 | import sqlalchemy as sa 17 | 18 | 19 | def upgrade(): 20 | ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('networks', sa.Column('lhost', sa.String(length=100), nullable=True)) 22 | op.add_column('networks', sa.Column('lport', sa.Integer(), nullable=True)) 23 | op.add_column('networks', sa.Column('rhost', sa.String(length=100), nullable=True)) 24 | op.add_column('networks', sa.Column('rport', sa.Integer(), nullable=True)) 25 | ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | ### commands auto generated by Alembic - please adjust! ### 30 | op.drop_column('networks', 'rport') 31 | op.drop_column('networks', 'rhost') 32 | op.drop_column('networks', 'lport') 33 | op.drop_column('networks', 'lhost') 34 | ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /alembic/versions/20c114cc162_initial_migration.py: -------------------------------------------------------------------------------- 1 | """initial migration 2 | 3 | Revision ID: 20c114cc162 4 | Revises: 5 | Create Date: 2015-11-28 23:36:45.751017 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '20c114cc162' 11 | down_revision = None 12 | branch_labels = None 13 | depends_on = None 14 | 15 | from alembic import op 16 | import sqlalchemy as sa 17 | import sqlalchemy_utils 18 | 19 | 20 | def upgrade(): 21 | ### commands auto generated by Alembic - please adjust! ### 22 | op.create_table('roles', 23 | sa.Column('id', sa.Integer(), nullable=False), 24 | sa.Column('name', sa.String(length=50), server_default='', nullable=False), 25 | sa.Column('label', sa.Unicode(length=255), server_default='', nullable=True), 26 | sa.PrimaryKeyConstraint('id'), 27 | sa.UniqueConstraint('name') 28 | ) 29 | op.create_table('users', 30 | sa.Column('id', sa.Integer(), nullable=False), 31 | sa.Column('username', sa.Unicode(length=30), nullable=False), 32 | sa.Column('email', sa.Unicode(length=255), nullable=False), 33 | sa.Column('confirmed_at', sa.DateTime(), nullable=True), 34 | sa.Column('password', sa.VARBINARY(length=1137), server_default='', nullable=False), 35 | sa.Column('reset_password_token', sa.String(length=100), nullable=False), 36 | sa.Column('is_active', sa.Boolean(), server_default='0', nullable=False), 37 | sa.Column('first_name', sa.Unicode(length=50), server_default='', nullable=False), 38 | sa.Column('last_name', sa.Unicode(length=50), server_default='', nullable=False), 39 | sa.Column('created', sa.DateTime(), nullable=True), 40 | sa.Column('last_updated', sa.DateTime(), nullable=True), 41 | sa.PrimaryKeyConstraint('id'), 42 | sa.UniqueConstraint('email'), 43 | sa.UniqueConstraint('username') 44 | ) 45 | op.create_table('networks', 46 | sa.Column('id', sa.Integer(), nullable=False), 47 | sa.Column('name', sa.String(length=255), nullable=False), 48 | sa.Column('nickname', sa.String(length=20), nullable=False), 49 | sa.Column('hostname', sa.String(length=100), nullable=False), 50 | sa.Column('port', sa.Integer(), nullable=False), 51 | sa.Column('realname', sa.String(length=100), nullable=False), 52 | sa.Column('username', sa.String(length=50), nullable=False), 53 | sa.Column('password', sa.String(length=100), nullable=False), 54 | sa.Column('usermode', sa.String(length=1), nullable=False), 55 | sa.Column('access_token', sa.String(length=100), nullable=False), 56 | sa.Column('user_id', sa.Integer(), nullable=False), 57 | sa.Column('current_nickname', sa.String(length=20), nullable=True), 58 | sa.Column('status', sa.String(length=255), nullable=True), 59 | sa.Column('created', sa.DateTime(), nullable=True), 60 | sa.Column('last_updated', sa.DateTime(), nullable=True), 61 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), 62 | sa.PrimaryKeyConstraint('id'), 63 | sa.UniqueConstraint('access_token'), 64 | sa.UniqueConstraint('user_id', 'name') 65 | ) 66 | op.create_table('users_roles', 67 | sa.Column('id', sa.Integer(), nullable=False), 68 | sa.Column('user_id', sa.Integer(), nullable=True), 69 | sa.Column('role_id', sa.Integer(), nullable=True), 70 | sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ondelete='CASCADE'), 71 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), 72 | sa.PrimaryKeyConstraint('id') 73 | ) 74 | op.create_table('channels', 75 | sa.Column('id', sa.Integer(), nullable=False), 76 | sa.Column('name', sa.String(length=50), nullable=False), 77 | sa.Column('password', sa.String(length=100), nullable=False), 78 | sa.Column('network_id', sa.Integer(), nullable=False), 79 | sa.Column('user_id', sa.Integer(), nullable=False), 80 | sa.Column('status', sa.String(length=255), nullable=True), 81 | sa.Column('created', sa.DateTime(), nullable=True), 82 | sa.Column('last_updated', sa.DateTime(), nullable=True), 83 | sa.ForeignKeyConstraint(['network_id'], ['networks.id'], ondelete='CASCADE'), 84 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), 85 | sa.PrimaryKeyConstraint('id'), 86 | sa.UniqueConstraint('network_id', 'name') 87 | ) 88 | op.create_table('clients', 89 | sa.Column('id', sa.Integer(), nullable=False), 90 | sa.Column('socket', sa.String(length=100), nullable=False), 91 | sa.Column('network_id', sa.Integer(), nullable=False), 92 | sa.Column('user_id', sa.Integer(), nullable=False), 93 | sa.Column('created', sa.DateTime(), nullable=True), 94 | sa.Column('last_updated', sa.DateTime(), nullable=True), 95 | sa.ForeignKeyConstraint(['network_id'], ['networks.id'], ondelete='CASCADE'), 96 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), 97 | sa.PrimaryKeyConstraint('id'), 98 | sa.UniqueConstraint('socket', 'network_id') 99 | ) 100 | ### end Alembic commands ### 101 | 102 | 103 | def downgrade(): 104 | ### commands auto generated by Alembic - please adjust! ### 105 | op.drop_table('clients') 106 | op.drop_table('channels') 107 | op.drop_table('users_roles') 108 | op.drop_table('networks') 109 | op.drop_table('users') 110 | op.drop_table('roles') 111 | ### end Alembic commands ### 112 | -------------------------------------------------------------------------------- /alembic/versions/50135956b24_added_ssl_fields_in_network_table.py: -------------------------------------------------------------------------------- 1 | """Added SSL fields in network table. 2 | 3 | Revision ID: 50135956b24 4 | Revises: 140c0dd3605 5 | Create Date: 2015-12-10 20:46:37.397744 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '50135956b24' 11 | down_revision = '140c0dd3605' 12 | branch_labels = None 13 | depends_on = None 14 | 15 | from alembic import op 16 | import sqlalchemy as sa 17 | 18 | 19 | def upgrade(): 20 | ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('networks', sa.Column('ssl', sa.Boolean(), nullable=True)) 22 | op.add_column('networks', sa.Column('ssl_verify', sa.String(length=255), nullable=True)) 23 | ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_column('networks', 'ssl_verify') 29 | op.drop_column('networks', 'ssl') 30 | ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /ircb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waartaa/ircb/18381b167529ee56d1c5d5d22f857bb27692c397/ircb/__init__.py -------------------------------------------------------------------------------- /ircb/bouncer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import logging 4 | import ssl 5 | 6 | from collections import defaultdict 7 | 8 | import ircb.stores 9 | 10 | from ircb.config import settings 11 | from ircb.connection import Connection 12 | from ircb.irc import IrcbBot 13 | from ircb.storeclient import initialize as storeclient_initialize 14 | from ircb.storeclient import ChannelStore, ClientStore, NetworkStore, UserStore 15 | 16 | logger = logging.getLogger('bouncer') 17 | 18 | 19 | class BouncerServerClientProtocol(Connection): 20 | 21 | def __init__(self, get_bot_handle, unregister_client, get_sibling_clients): 22 | self.network = None 23 | self.bot = None 24 | self.get_bot_handle = get_bot_handle 25 | self.unregister_client = unregister_client 26 | self.get_sibling_clients = get_sibling_clients 27 | self.host, self.port = None, None 28 | self.client_id = None 29 | 30 | def connection_made(self, transport): 31 | logger.debug('New client connection received') 32 | self.transport = transport 33 | self.host, self.port = self.transport.get_extra_info( 34 | 'socket').getpeername() 35 | 36 | def data_received(self, data): 37 | asyncio.Task(self.handle_data_received(data)) 38 | 39 | @asyncio.coroutine 40 | def handle_data_received(self, data): 41 | data = self.decode(data) 42 | logger.debug('Received client data: %s', data) 43 | for line in data.rstrip().splitlines(): 44 | tokens = line.split(" ", 1) 45 | verb = tokens[0] 46 | if verb == "QUIT": 47 | pass 48 | elif verb == "PASS": 49 | message = tokens[1] 50 | access_token = message.split(" ")[0] 51 | self.network = yield from self.get_network(access_token) 52 | if self.network is None: 53 | logger.debug( 54 | 'Client authentiacation failed for token: {}'.format( 55 | access_token)) 56 | self.unregister_client(self.network.id, self) 57 | self.transport.write('Authentication failed') 58 | self.transport.close() 59 | return 60 | else: 61 | client = yield from ClientStore.create({ 62 | 'socket': '{}:{}'.format(self.host, self.port), 63 | 'network_id': self.network.id, 64 | 'user_id': self.network.user_id 65 | }) 66 | self.client_id = client.id 67 | elif self.bot: 68 | logger.debug( 69 | 'Forwarding {}\t {}'.format(self.bot, line)) 70 | self.bot.raw(line) 71 | if line.lstrip().startswith('PRIVMSG'): 72 | words = line.split() 73 | # Dispatch sent message to sibling clients for record 74 | if words[1].startswith('#'): 75 | mask = ':{nick}!~{realname}@* '.format( 76 | nick=self.bot.nick, 77 | realname=self.bot.config.get('realname', '') 78 | ) 79 | for client in self.get_sibling_clients(self): 80 | client.send(mask + line) 81 | if self.bot is None: 82 | self.bot = yield from self.get_bot_handle( 83 | self.network, self) 84 | if self.bot is None: 85 | self.transport.write('Bot not connected') 86 | self.transport.close() 87 | asyncio.Task(ClientStore.delete({'id': self.client_id})) 88 | 89 | def connection_lost(self, exc): 90 | self.unregister_client(self.network.id, self) 91 | logger.debug('Client connection lost: {}'.format(self.network)) 92 | if self.client_id: 93 | asyncio.Task(ClientStore.delete({'id': self.client_id})) 94 | 95 | def get_network(self, access_token): 96 | result = yield from NetworkStore.get( 97 | {'query': ('access_token', access_token)}) 98 | return result 99 | 100 | def __str__(self): 101 | return ''.format( 102 | self.host, self.port) 103 | 104 | def __repr__(self): 105 | return self.__str__() 106 | 107 | 108 | class Bouncer(object): 109 | 110 | def __init__(self, loop=None): 111 | self.loop = loop or asyncio.get_event_loop() 112 | self.bots = {} 113 | self.clients = defaultdict(set) 114 | NetworkStore.on('create', self.on_network_create) 115 | NetworkStore.on('update', self.on_network_update) 116 | 117 | def start(self, host, port): 118 | server = self.create(host, port) 119 | try: 120 | self.loop.run_forever() 121 | except KeyboardInterrupt: 122 | pass 123 | server.close() 124 | self.loop.run_until_complete(server.wait_closed()) 125 | 126 | def create(self, host, port): 127 | sc = None 128 | if settings.SSL: 129 | sc = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 130 | sc.load_cert_chain(settings.SSL_CERT_PATH, settings.SSL_KEY_PATH) 131 | coro = self.loop.create_server( 132 | lambda: BouncerServerClientProtocol(self.get_bot_handle, 133 | self.unregister_client, 134 | self.get_sibling_clients), 135 | host, port, ssl=sc) 136 | logger.info('Listening on {}:{}'.format(host, port)) 137 | return self.loop.run_until_complete(coro) 138 | 139 | def on_network_update(self, network, modified=None): 140 | asyncio.Task(self._on_network_create_update(network)) 141 | 142 | def on_network_create(self, network): 143 | asyncio.Task(self._on_network_create_update(network)) 144 | 145 | @asyncio.coroutine 146 | def _on_network_create_update(self, network): 147 | if network.status == '0': 148 | bot = self.bots.get(network.id) 149 | if bot and not bot.protocol.closed: 150 | return 151 | user = yield from UserStore.get({'query': network.user_id}) 152 | logger.debug( 153 | 'Creating new bot: {}-{}'.format( 154 | user.username, network.name) 155 | ) 156 | connected_channels = yield from ChannelStore.get({ 157 | 'filter': dict( 158 | network_id=network.id, 159 | status='1' 160 | ) 161 | }) 162 | config = dict( 163 | id=network.id, 164 | user_id=user.id, 165 | name=network.name, 166 | username=network.username or user.username, 167 | user=user.username, 168 | realname=network.realname or ' ', 169 | password=network.password, 170 | nick=network.nickname, 171 | host=network.hostname, 172 | port=network.port, 173 | ssl=network.ssl, 174 | ssl_verify=network.ssl_verify, 175 | lock=asyncio.Lock(), 176 | autojoins=[channel.name for channel in connected_channels] 177 | ) 178 | # Acquire lock for connecting to IRC server, so that 179 | # other clients connecting to the same bot can wait on this 180 | # lock before trying to send messages to the bot 181 | yield from config['lock'].acquire() 182 | if bot and bot.protocol.closed: 183 | bot.reload_config(**config) 184 | else: 185 | bot = IrcbBot(**config) 186 | bot.run_in_loop() 187 | self.register_bot(network.id, bot) 188 | elif network.status == '2': 189 | bot = self.bots.get(network.id) 190 | if bot: 191 | bot.quit() 192 | bot.protocol.close() 193 | 194 | def on_network_delete(self, network): 195 | pass 196 | 197 | @asyncio.coroutine 198 | def get_bot_handle(self, network, client): 199 | try: 200 | key = network.id 201 | bot = self.bots.get(key) 202 | self.register_client(key, client) 203 | connected_channels = yield from ChannelStore.get({ 204 | 'filter': dict( 205 | network_id=key, 206 | status='1' 207 | ) 208 | }) 209 | if bot is None: 210 | yield from NetworkStore.update( 211 | dict( 212 | filter=('id', network.id), 213 | update={ 214 | 'status': '0' 215 | }) 216 | ) 217 | return 218 | logger.debug('Reusing existing bot: {}'.format(bot)) 219 | # Wait for bot.config.lock to be release when connection is 220 | # made to remote IRC server 221 | if bot.config.lock: 222 | yield from bot.config.lock 223 | bot.config.lock = None 224 | joining_messages_list = [ 225 | ':* 001 {nick} :You are now connected to {network}'.format( 226 | nick=bot.nick, network=network.name), 227 | ':* 251 {nick} : '.format(nick=bot.nick) 228 | ] 229 | for channel in connected_channels: 230 | joining_messages_list.append( 231 | ':{nick}!* JOIN {channel}'.format( 232 | nick=bot.nick, 233 | channel=channel.name) 234 | ) 235 | bot.raw('NAMES %s' % channel.name) 236 | 237 | client.send(*['\r\n'.join(joining_messages_list)]) 238 | 239 | return bot 240 | except Exception as e: 241 | logger.error('get_bot_handle error: {}'.format(e), 242 | exc_info=True) 243 | 244 | def register_bot(self, network_id, bot): 245 | logger.debug('Registering new bot: {}'.format(network_id)) 246 | key = network_id 247 | existing_bot = self.bots.get(key) 248 | if existing_bot: 249 | existing_bot.protocol.transport.close() 250 | del self.bots[key] 251 | bot.clients = self.clients[key] 252 | self.bots[key] = bot 253 | logger.debug('Bots: %s', self.bots) 254 | 255 | def register_client(self, network_id, client): 256 | key = network_id 257 | clients = self.clients[key] 258 | clients.add(client) 259 | logger.debug('Registered new client: %s, %s', key, clients) 260 | 261 | def unregister_client(self, network_id, client): 262 | key = network_id 263 | clients = self.clients[key] 264 | logger.debug('Unregistering client: {}'.format(client)) 265 | try: 266 | clients.remove(client) 267 | except KeyError: 268 | pass 269 | 270 | def get_sibling_clients(self, client): 271 | key = client.network.id 272 | clients = set(self.clients[key]) 273 | siblings = clients.difference(set([client])) 274 | return siblings 275 | 276 | 277 | def runserver(host='0.0.0.0', port=9000): 278 | storeclient_initialize() 279 | bouncer = Bouncer() 280 | bouncer.start(host, port) 281 | 282 | if __name__ == '__main__': 283 | runserver() 284 | -------------------------------------------------------------------------------- /ircb/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waartaa/ircb/18381b167529ee56d1c5d5d22f857bb27692c397/ircb/cli/__init__.py -------------------------------------------------------------------------------- /ircb/cli/loaddata.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import click 3 | import yaml 4 | 5 | from ircb.lib.async import coroutinize 6 | from ircb.storeclient import NetworkStore, UserStore 7 | 8 | 9 | @click.group(name='users') 10 | def loaddata_cli(): 11 | "Loads YAML file with config" 12 | pass 13 | 14 | 15 | def validate_yaml(f): 16 | "Validates YAML file" 17 | return True 18 | 19 | 20 | @click.command(name='loaddata') 21 | @click.argument('f') 22 | @coroutinize 23 | def load_data(f): 24 | with open(f, 'r') as f: 25 | config = yaml.load(f) 26 | 27 | if validate_yaml(config): 28 | 29 | for user in config.keys(): 30 | user_data = config[user] 31 | yield from UserStore.create( 32 | dict( 33 | username=user, 34 | email=user_data['email'], 35 | password=user_data['password'] 36 | ) 37 | ) 38 | networks = user_data['networks'] 39 | for net in networks.keys(): 40 | net_data = networks[net] 41 | network = yield from NetworkStore.create( 42 | dict( 43 | user_username=user, 44 | name=net, 45 | nickname=net_data["nick"], 46 | hostname=net_data["host"], 47 | port=net_data["port"], 48 | realname=net_data["realname"], 49 | username=net_data["username"], 50 | network_password=net_data["password"], 51 | usermode=net_data["usermode"], 52 | ) 53 | ) 54 | -------------------------------------------------------------------------------- /ircb/cli/main.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from ircb.cli.loaddata import load_data 4 | from ircb.cli.network import network_cli 5 | from ircb.cli.run import run_cli 6 | from ircb.cli.user import user_cli 7 | from ircb.utils.config import load_config 8 | 9 | 10 | @click.group() 11 | @click.option('--verbose', '-v', default=False, is_flag=True, 12 | help='Show verbose logs') 13 | def cli(verbose): 14 | """ircb CLI""" 15 | load_config(verbose=verbose) 16 | 17 | cli.add_command(user_cli) 18 | cli.add_command(network_cli) 19 | cli.add_command(run_cli) 20 | cli.add_command(load_data) 21 | 22 | if __name__ == '__main__': 23 | cli() 24 | -------------------------------------------------------------------------------- /ircb/cli/network.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import click 3 | 4 | from tabulate import tabulate 5 | 6 | from ircb.lib.async import coroutinize 7 | from ircb.storeclient import NetworkStore 8 | 9 | 10 | @click.group(name='networks') 11 | def network_cli(): 12 | """Manager networks""" 13 | from ircb.storeclient import initialize 14 | initialize() 15 | 16 | 17 | @click.command(name='create') 18 | @click.argument('user') 19 | @click.argument('network_name') 20 | @click.argument('host') 21 | @click.argument('port') 22 | @click.argument('nick') 23 | @click.option('--realname', default='') 24 | @click.option('--username', default='') 25 | @click.option('--password', default='') 26 | @click.option('--usermode', default='0') 27 | @click.option('--ssl', default=False, is_flag=True) 28 | @click.option('--ssl_verify', default="CERT_NONE", 29 | type=click.Choice( 30 | ['CERT_NONE', 'CERT_OPTIONAL', 'CERT_REQUIRED'])) 31 | @coroutinize 32 | def create(user, network_name, host, port, nick, realname, username, password, 33 | usermode, ssl, ssl_verify): 34 | """Create a network for a user""" 35 | network = yield from NetworkStore.create( 36 | dict( 37 | user_username=user, 38 | name=network_name, 39 | nickname=nick, 40 | hostname=host, 41 | port=port, 42 | realname=realname, 43 | network_username=username, 44 | password=password, 45 | usermode=usermode, 46 | ssl=ssl, 47 | ssl_verify=ssl_verify 48 | ) 49 | ) 50 | print(network.access_token) 51 | 52 | 53 | @click.command(name='list') 54 | @coroutinize 55 | def ls(page=1): 56 | networks = yield from NetworkStore.get({'query': {}}) 57 | headers = ['Id', 'User', 'Name', 'Nick', 'Server'] 58 | table = [ 59 | [network.id, 60 | network.user_id, 61 | network.name, 62 | network.nickname, 63 | '{}/{}'.format(network.hostname, network.port)] 64 | for network in networks] 65 | print(tabulate(table, headers, tablefmt='grid')) 66 | 67 | 68 | @click.command(name='connect') 69 | @click.argument('id') 70 | @coroutinize 71 | def connect(id): 72 | network = yield from NetworkStore.update( 73 | dict( 74 | filter=('id', id), 75 | update={ 76 | 'status': '0' 77 | }) 78 | ) 79 | 80 | 81 | @click.command(name='disconnect') 82 | @click.argument('id') 83 | @coroutinize 84 | def disconnect(id): 85 | network = yield from NetworkStore.update( 86 | dict( 87 | filter=('id', id), 88 | update={ 89 | 'status': '2' 90 | }) 91 | ) 92 | 93 | 94 | network_cli.add_command(create) 95 | network_cli.add_command(ls) 96 | network_cli.add_command(connect) 97 | network_cli.add_command(disconnect) 98 | -------------------------------------------------------------------------------- /ircb/cli/run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import click 4 | 5 | from ircb import bouncer 6 | from ircb.web import app as web 7 | from ircb import identd 8 | 9 | 10 | @click.group(name='run') 11 | def run_cli(): 12 | """Implement run commands""" 13 | pass 14 | 15 | 16 | @click.command(name='allinone') 17 | @click.option('--host', '-h', default='0.0.0.0', 18 | help='Host for bouncer, defaults to 0.0.0.0') 19 | @click.option('--port', '-p', default=9000, 20 | help='Port for bouncer, defaults to 9000') 21 | @click.option('--enable-identd', '-i', default=False, is_flag=True, 22 | help='Run with identd') 23 | @click.option('--identd-host', default='0.0.0.0', 24 | help='Host for identd, defaults to 0.0.0.0') 25 | @click.option('--identd-port', default=113, 26 | help='Port for identd, defaults to 113') 27 | @click.option('--web-host', default='0.0.0.0', 28 | help='Host for web server, defaults to 0.0.0.0') 29 | @click.option('--web-port', default=10000, 30 | help='Port for web server, defaults to 10000') 31 | def run_allinone(host, port, enable_identd, identd_host, identd_port, 32 | web_host, web_port): 33 | """Run ircb in a single process""" 34 | import ircb.stores 35 | import ircb.stores.base 36 | import ircb.storeclient 37 | ircb.stores.initialize() 38 | loop = asyncio.get_event_loop() 39 | bouncer_server = bouncer.Bouncer(loop).create(host, port) 40 | if enable_identd: 41 | identd_server = identd.IdentdServer(loop).create( 42 | identd_host, identd_port) 43 | web_server = web.createserver(loop, web_host, web_port) 44 | try: 45 | loop.run_forever() 46 | except KeyboardInterrupt: 47 | pass 48 | bouncer_server.close() 49 | web_server.close() 50 | loop.run_until_complete(bouncer_server.wait_closed()) 51 | loop.run_until_complete(web_server.wait_closed()) 52 | if enable_identd: 53 | identd_server.close() 54 | loop.run_until_complete(identd_server.wait_closed()) 55 | 56 | 57 | @click.command(name='stores') 58 | def run_stores(): 59 | """Run ircb stores""" 60 | import ircb.stores 61 | import ircb.stores.base 62 | ircb.stores.initialize() 63 | ircb.stores.base.dispatcher.run_forever() 64 | 65 | 66 | @click.command(name='bouncer') 67 | @click.option('--host', '-h', default='0.0.0.0', 68 | help='Host, defaults to 0.0.0.0') 69 | @click.option('--port', '-p', default=9000, 70 | help='Port, defaults to 9000') 71 | def run_bouncer(host, port): 72 | """Run ircb bouncer""" 73 | bouncer.runserver(host, port) 74 | 75 | 76 | @click.option('--host', '-h', default='0.0.0.0', 77 | help='Host, defaults to 0.0.0.0') 78 | @click.option('--port', '-p', default=10000, 79 | help='Port, defaults to 10000') 80 | @click.command(name='web') 81 | def run_web(host, port): 82 | """Run ircb web server""" 83 | web.runserver(host, port) 84 | 85 | 86 | @click.command(name='identd') 87 | @click.option('--host', '-h', default='0.0.0.0', 88 | help='Host, defaults to 0.0.0.0') 89 | @click.option('--port', '-p', default=113, 90 | help='Port, defaults to 113') 91 | def run_identd(host, port): 92 | """Run identd server""" 93 | identd.runserver(host, port) 94 | 95 | run_cli.add_command(run_allinone) 96 | run_cli.add_command(run_bouncer) 97 | run_cli.add_command(run_stores) 98 | run_cli.add_command(run_web) 99 | run_cli.add_command(run_identd) 100 | -------------------------------------------------------------------------------- /ircb/cli/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import click 3 | 4 | from ircb.lib.async import coroutinize 5 | from ircb.storeclient import UserStore 6 | 7 | 8 | @click.group(name='users') 9 | def user_cli(): 10 | """Manager users""" 11 | from ircb.storeclient import initialize 12 | initialize() 13 | 14 | 15 | @click.command(name='create') 16 | @click.argument('username') 17 | @click.argument('email') 18 | @click.argument('password', required=False, default='') 19 | @coroutinize 20 | def user_create(username, email, password): 21 | """Create a user""" 22 | yield from UserStore.create(dict( 23 | username=username, 24 | email=email, 25 | password=password 26 | )) 27 | 28 | 29 | user_cli.add_command(user_create) 30 | -------------------------------------------------------------------------------- /ircb/config/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | import os 5 | 6 | from . import default_settings 7 | 8 | 9 | class Settings(object): 10 | 11 | _instance = None 12 | 13 | def __new__(cls, *args, **kwargs): 14 | if not cls._instance: 15 | cls._instance = super().__new__(cls, *args, **kwargs) 16 | return cls._instance 17 | 18 | def __init__(self): 19 | self._data = {} 20 | for attr in dir(default_settings): 21 | if attr.startswith('__'): 22 | continue 23 | self._data[attr] = getattr(default_settings, attr, None) 24 | settings_path = os.getenv('IRCB_SETTINGS') 25 | if settings_path and os.path.isfile(settings_path): 26 | with open(settings_path) as f: 27 | code = compile(f.read(), settings_path, 'exec') 28 | exec(code, self._data) 29 | # FIXME: Do not remove __builtins__ 30 | # self._data.pop('__builtins__', None) 31 | 32 | def __getitem__(self, key): 33 | return self._data.get(key) 34 | 35 | def __getattr__(self, key): 36 | return self._data.get(key) 37 | 38 | settings = Settings() 39 | 40 | __all__ = ['settings'] 41 | -------------------------------------------------------------------------------- /ircb/config/default_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | SECRET_KEY = 'some key' 4 | 5 | SSL = True 6 | 7 | SSL_CERT_PATH = os.path.join(os.path.dirname(__file__), 8 | 'sample_ssl.cert') 9 | SSL_KEY_PATH = os.path.join(os.path.dirname(__file__), 10 | 'sample_ssl.key') 11 | 12 | DB_URI = 'sqlite:///ircb.db' 13 | 14 | SUBSCRIBER_ENDPOINTS = { 15 | 'stores': 'tcp://127.0.0.1:35000', 16 | } 17 | 18 | LOGLEVEL = 'INFO' 19 | 20 | LOGGING_CONF = dict( 21 | version=1, 22 | level=LOGLEVEL, 23 | formatters=dict( 24 | bare={ 25 | "datefmt": "%Y-%m-%d %H:%M:%S", 26 | "format": "[%(asctime)s][%(name)10s %(levelname)7s] %(message)s" 27 | }, 28 | ), 29 | handlers=dict( 30 | console={ 31 | "class": "logging.StreamHandler", 32 | "formatter": "bare", 33 | "level": LOGLEVEL, 34 | "stream": "ext://sys.stdout", 35 | } 36 | ), 37 | loggers=dict( 38 | ircb={ 39 | "level": "DEBUG", 40 | "propagate": False, 41 | "handlers": ["console"], 42 | }, 43 | network={ 44 | "level": "DEBUG", 45 | "propagate": False, 46 | "handlers": ["console"], 47 | }, 48 | bouncer={ 49 | "level": "DEBUG", 50 | "propagate": False, 51 | "handlers": ["console"], 52 | }, 53 | stores={ 54 | "level": "DEBUG", 55 | "propagate": False, 56 | "handlers": ["console"], 57 | }, 58 | dispatcher={ 59 | "level": "DEBUG", 60 | "propagate": False, 61 | "handlers": ["console"], 62 | }, 63 | irc={ 64 | "level": "DEBUG", 65 | "propagate": False, 66 | "handlers": ["console"], 67 | }, 68 | aiohttp={ 69 | "level": "DEBUG", 70 | "propagate": False, 71 | "handlers": ["console"], 72 | }, 73 | publisher={ 74 | "level": "DEBUG", 75 | "propagate": False, 76 | "handlers": ["console"], 77 | }, 78 | raw={ 79 | "level": "DEBUG", 80 | "propagate": False, 81 | "handlers": ["console"], 82 | }, 83 | identd={ 84 | "level": "DEBUG", 85 | "propagate": False, 86 | "handlers": ["console"], 87 | } 88 | ), 89 | ) 90 | 91 | INTERNAL_HOST = '127.0.0.1' 92 | REDIS_HOST = '127.0.0.1' 93 | REDIS_PORT = 6379 94 | 95 | # A 32 byte string 96 | WEB_SALT = b'c237202ee55411e584f4cc3d8237ff4b' 97 | -------------------------------------------------------------------------------- /ircb/config/sample_ssl.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC/TCCAeWgAwIBAgIJAKTIju0GrsatMA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV 3 | BAMMCnNhbXBsZV9zc2wwHhcNMTYwOTExMTA1ODQxWhcNMjYwOTA5MTA1ODQxWjAV 4 | MRMwEQYDVQQDDApzYW1wbGVfc3NsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 5 | CgKCAQEAysXvAtWNysLqnFjjIjesQPfZfEgBmjrHvzZ4V8iwFIIt8KBGJWDtOg0g 6 | tg9rND5o6mLyasJDApMRoSiaj/hmLeMukf5soeCZu0nsSYeiuGI/gRZMJj1HyW2s 7 | 2iDLnMW+Ylt8PAHDNYLh60SEQFWkpvaEYjrSC0lWqiUIxoHBub+PrKMJ7ZmN1ZZB 8 | bK+uiY5g1pJzDmxBZAw+SjOHTUuzVp1YmrRCxvztaCHWzhA7O8L5LM6yx+c1BGSH 9 | 5hhhedDC9pm5jHL5NOlFYf0tEHkCwG7dQ6+eEq0tKWvhAQYvgL7XQnS35wo0t6fe 10 | mDNRiK/lyvTMjSEjb7lBIhZ41a0D5QIDAQABo1AwTjAdBgNVHQ4EFgQUX65iXISK 11 | /T8edMY28IcEdfOPKFAwHwYDVR0jBBgwFoAUX65iXISK/T8edMY28IcEdfOPKFAw 12 | DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAjMKqzG2F8lSJ5MH0xTvc 13 | mwcOtONgWyIxkyG3Nut3AGgFQeegoV4ygi+L8fIGQcz8ZrwOsrKdJnpgabd2Ignj 14 | VBTlLthsqwPj7i1VehoJ9bo34HwZK5yBw4lW9kZTMhD4v1pthoRnjgNF2drFi4Qt 15 | Fnj2pedF9e8WPnkKcmSd+TNtlvpBJoocEkWB8/PuRb+ZHkO8EReymMd5bdHMBaR4 16 | goVp0b1EaA3Iv86O6utEyb8k71D2Fxi78Pg4TXWsjnvRZdov043byBP1YamoHjjW 17 | ZFO2vWOHozyFQm7WewklkwE8FbbMl4iak+4bL9neqqWd0ALHBQtrmf2LYDfW3SdH 18 | VQ== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /ircb/config/sample_ssl.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAysXvAtWNysLqnFjjIjesQPfZfEgBmjrHvzZ4V8iwFIIt8KBG 3 | JWDtOg0gtg9rND5o6mLyasJDApMRoSiaj/hmLeMukf5soeCZu0nsSYeiuGI/gRZM 4 | Jj1HyW2s2iDLnMW+Ylt8PAHDNYLh60SEQFWkpvaEYjrSC0lWqiUIxoHBub+PrKMJ 5 | 7ZmN1ZZBbK+uiY5g1pJzDmxBZAw+SjOHTUuzVp1YmrRCxvztaCHWzhA7O8L5LM6y 6 | x+c1BGSH5hhhedDC9pm5jHL5NOlFYf0tEHkCwG7dQ6+eEq0tKWvhAQYvgL7XQnS3 7 | 5wo0t6femDNRiK/lyvTMjSEjb7lBIhZ41a0D5QIDAQABAoIBAAWacJ9gbVmkBz2o 8 | yyX/6delwbHIf5rWcvyasbqjRhJbAo/6rdWMlyGaiUPeqzS3YMjRUSeYFKf2jm8o 9 | oxTFZXVxKLFKGZBjl3vwcAIcKGf19xFHH3WTDe/IAxXIADhl0nv/UTCLT+yjoECT 10 | FSB9/V3LxB6+x10eerGa8mIh5cuPLf0KtEMXasQfE9yWSRZZ4ieS2nY7f3vdbhJz 11 | 8pwp/BwWaSWvmxEyk4SFENxjR8rDqtll8tp8QAwuFeGrbXB5M3fwKZHTmirzlWE6 12 | WqC2XrMdiEhX9tuOTXnLrh6bt9kaHhYIRw3yJMXhld9Q+Qnbm7/+Ead4zsk2YIvo 13 | QZvdJ4ECgYEA6yuoDTgFCk0SAs2DIIZzk7lx1hX7rjStAF/FJZ2BCxXLQjraOF+A 14 | AY/qeN5yf+/MnEB33nY/d3DOvyJjwihHdgTdUsrofNUa079mPIY6G5yNH98DaVDW 15 | DvpgbOVGbNx2md47l0jDCgDM3rcqYBKq1+yUoBky624/Il6otnM07KECgYEA3Luw 16 | BjWOCA0iAMEUSYEzU3shLG/kq5a3mYhTX4zu8pj0Z4AdyUQOs7Y1MVgPW7tt3YzP 17 | OyqgLFc0zb8jbpBZq8asf1qbl0ugmNFqtv9un6+C1QSDm5exXul5cT2OgMtNTVgD 18 | 0rK/OEf61wpVXjCFdoe/o/cv6fkdI/QVeo6/bMUCgYAGbFTUt5j7pQs+5FoWg1WY 19 | zVHcpREQuwOWpQb/dgPWR6wbjPv02jbm1AV1c63w7J3MDr63Zsdo/b3H3qqW3P7G 20 | rG9XGY+sCS4IZovmT0w7ANWh7zdqoviVxCTqFIEN7B+ZKEa8ZRJerZLq6lnP8fKU 21 | nzOYA9guMf6rc6ZsBU2GwQKBgQDB+HEj07YfXyMNVJ62RUJMTvyE50MkgkqPMdxK 22 | MDfemgjUVyJVtmfRBwJRfVfpvZg7Q7zr+nZM4Ml3MEs7osAaEnmNZJsr5fqXsBe9 23 | /lNBImOvO8tHVJM6m6LrnzN1/LHOkNSzN/6Pv7kvdVY1ciAmW/5NYTAKxK4V5S1m 24 | yMBxHQKBgBXiWqhhxXgdYGzP0/vCA9zDsli/ox5SiZa+y8AVNIXjrVnLp2dyXozm 25 | BDWs1sfiJKwgfOtYEgQ683G0ErLFhZt2Wr8n3CqIQZ+nXSJEZYyIPDF04OxenxKy 26 | o4abOQIw/KVrEEzO9+0ba8oWmtObDQNnJA948XCl/YmBCcZmO88K 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /ircb/connection.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | 4 | 5 | class Connection(asyncio.Protocol): 6 | 7 | def decode(self, line): 8 | try: 9 | return line.decode() 10 | except UnicodeDecodeError: 11 | return line.decode("latin1") 12 | 13 | def normalize(self, line, ending='\r\n'): 14 | if not line.endswith(ending): 15 | line += ending 16 | return line 17 | 18 | def send(self, *args): 19 | message = self.normalize(" ".join(args)) 20 | print('SEND', message.encode()) 21 | self.transport.write(message.encode()) 22 | -------------------------------------------------------------------------------- /ircb/forms/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .network import NetworkForm 3 | 4 | __all__ = [ 5 | 'NetworkForm' 6 | ] 7 | -------------------------------------------------------------------------------- /ircb/forms/network.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from wtforms_alchemy import ModelForm 3 | from wtforms.fields import SelectField 4 | 5 | from ircb.models import Network 6 | from ircb.models.network import SSL_VERIFY_CHOICES 7 | 8 | 9 | class NetworkForm(ModelForm): 10 | 11 | class Meta: 12 | model = Network 13 | exclude = [ 14 | 'status', 15 | 'rhost', 16 | 'rport', 17 | 'lhost', 18 | 'lport', 19 | 'access_token', 20 | 'current_nickname' 21 | ] 22 | 23 | ssl_verify = SelectField(default='CERT_NONE', choices=SSL_VERIFY_CHOICES) 24 | -------------------------------------------------------------------------------- /ircb/forms/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from wtforms_alchemy import ModelForm 3 | 4 | from ircb.models import User 5 | 6 | 7 | class UserForm(ModelForm): 8 | 9 | class Meta: 10 | model = User 11 | only = [ 12 | 'username', 13 | 'email', 14 | 'password', 15 | 'first_name', 16 | 'last_name' 17 | ] 18 | unique_validator = None 19 | -------------------------------------------------------------------------------- /ircb/identd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import logging 4 | 5 | from ircb.storeclient import NetworkStore, UserStore 6 | from ircb.storeclient import initialize as sc_initialize 7 | from ircb.connection import Connection 8 | from ircb.config import settings 9 | 10 | logger = logging.getLogger('identd') 11 | 12 | 13 | class IdentdServerProtocol(Connection): 14 | def connection_made(self, transport): 15 | logger.debug('New client connection received') 16 | self.transport = transport 17 | 18 | def data_received(self, data): 19 | asyncio.Task(self.handle_received_data(self.decode(data))) 20 | 21 | @asyncio.coroutine 22 | def handle_received_data(self, data): 23 | logger.debug('RECV: {}'.format(data)) 24 | lport, rport = [_.strip() for _ in data.split(',')] 25 | 26 | networks = yield from NetworkStore.get({ 27 | 'query': {'lport': lport, 'rport': rport} 28 | }) 29 | network = networks[0] 30 | user = yield from UserStore.get({ 31 | 'query': network.user_id 32 | }) 33 | rpl_msg = '{lport}, {rport} : USERID : UNIX : {username}'.format( 34 | lport=lport, rport=rport, username=user.username) 35 | logger.debug('RPL: {}'.format(rpl_msg)) 36 | self.send(rpl_mgs) 37 | 38 | 39 | class IdentdServer(object): 40 | 41 | def __init__(self, loop=None): 42 | if loop is None: 43 | loop = asyncio.get_event_loop() 44 | self.loop = loop 45 | 46 | def create(self, host, port): 47 | coro = self.loop.create_server(IdentdServerProtocol, 48 | host, 49 | port) 50 | logger.info('Listening on {}:{}'.format(host, port)) 51 | return self.loop.run_until_complete(coro) 52 | 53 | def start(self, host, port): 54 | server = self.create(host, port) 55 | try: 56 | self.loop.run_forever() 57 | except KeyboardInterrupt: 58 | pass 59 | server.close() 60 | self.loop.run_until_complete(server.wait_closed()) 61 | 62 | 63 | def runserver(host='0.0.0.0', port=113): 64 | logging.config.dictConfig(settings.LOGGING_CONF) 65 | sc_initialize() 66 | identd_server = IdentdServer() 67 | identd_server.start(host, port) 68 | 69 | if __name__ == '__main__': 70 | runserver() 71 | -------------------------------------------------------------------------------- /ircb/irc/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import logging 4 | import re 5 | 6 | import irc3 7 | 8 | from ircb.storeclient import NetworkStore 9 | from ircb.storeclient import ChannelStore 10 | 11 | logger = logging.getLogger('irc') 12 | 13 | 14 | class IrcbIrcConnection(irc3.IrcConnection): 15 | 16 | def connection_made(self, transport): 17 | super().connection_made(transport) 18 | asyncio.Task(self.handle_connection_made(transport)) 19 | 20 | def data_received(self, data): 21 | super().data_received(data) 22 | 23 | def connection_lost(self, exc): 24 | asyncio.Task(self.handle_connection_lost()) 25 | super().connection_lost(exc) 26 | 27 | @asyncio.coroutine 28 | def handle_connection_made(self, transport): 29 | logger.debug('Network connected: %s, %s, %s', 30 | self.factory.config.user, 31 | self.factory.config.name, self.factory.config.nick) 32 | socket = transport.get_extra_info('socket') 33 | lhost, lport = socket.getsockname() 34 | rhost, rport = socket.getpeername() 35 | yield from NetworkStore.update( 36 | dict( 37 | filter=('id', self.factory.config.id), 38 | update={ 39 | 'status': '1', 40 | 'lhost': lhost, 41 | 'lport': lport, 42 | 'rhost': rhost, 43 | 'rport': rport 44 | } 45 | ) 46 | ) 47 | 48 | @asyncio.coroutine 49 | def handle_connection_lost(self): 50 | logger.debug('Network disconnected: %s, %s, %s', 51 | self.factory.config.userinfo, 52 | self.factory.config.name, self.factory.config.nick) 53 | yield from NetworkStore.update( 54 | dict( 55 | filter=('id', self.factory.config.id), 56 | update={ 57 | 'status': '3', 58 | 'lhost': None, 59 | 'lport': None, 60 | 'rhost': None, 61 | 'rport': None 62 | } 63 | ) 64 | ) 65 | 66 | 67 | class IrcbBot(irc3.IrcBot): 68 | 69 | defaults = dict( 70 | irc3.IrcBot.defaults, 71 | nick=None, 72 | realname='', 73 | host='www.waartaa.com', 74 | port=6667, 75 | url='https://github.com/waartaa/ircb', 76 | passwords={}, 77 | ctcp=dict( 78 | version='ircb - https://github.com/waartaa/ircb', 79 | userinfo='{userinfo}', 80 | time='{now:%c}' 81 | ), 82 | includes=['ircb.irc.plugins.ircb', 'ircb.irc.plugins.autojoins', 83 | 'ircb.irc.plugins.logger'], 84 | connection=IrcbIrcConnection, 85 | ) 86 | defaults['irc3.plugins.logger'] = { 87 | 'handler': 'ircb.irc.plugins.logger.StoreHandler'} 88 | cmd_regex = re.compile( 89 | r'(?P[A-Z]+)(?:\s+(?P[^\:]+))?(?:\s*\:(?P.*))?') 90 | 91 | def __init__(self, *args, **kwargs): 92 | self.clients = None 93 | super().__init__(*args, **kwargs) 94 | 95 | def reload_config(self, *ini, **config): 96 | self.config = irc3.utils.Config(dict(self.defaults, *ini, **config)) 97 | 98 | def run_in_loop(self): 99 | """Run bot in an already running event loop""" 100 | self.create_connection() 101 | self.add_signal_handlers() 102 | 103 | def connection_made(self, f): 104 | super().connection_made(f) 105 | # Release lock acquired during creating a new bot instance 106 | self.config.lock.release() 107 | 108 | def join(self, target, password=None): 109 | """join a channel""" 110 | if not password: 111 | password = self.config.passwords.get( 112 | target.strip(self.server_config['CHANTYPES'])) 113 | if password: 114 | target += ' ' + password 115 | self.send_line('JOIN %s' % target) 116 | asyncio.Task(self.join_handler(target, password)) 117 | 118 | @asyncio.coroutine 119 | def join_handler(self, target, password): 120 | yield from ChannelStore.create_or_update( 121 | dict( 122 | channel=target, 123 | network_id=self.config.id, 124 | password=password, 125 | status='0' 126 | ) 127 | ) 128 | 129 | def part(self, target, reason=None): 130 | super().part(target, reason) 131 | asyncio.Task(self.part_handler(target)) 132 | 133 | @asyncio.coroutine 134 | def part_handler(self, target): 135 | yield from ChannelStore.create_or_update( 136 | dict( 137 | channel=target, 138 | network_id=self.config.id, 139 | status='2' 140 | ) 141 | ) 142 | 143 | def raw(self, message): 144 | """Handle raw message""" 145 | logger.debug('Received raw msg: %s' % message) 146 | m = self.cmd_regex.match(message) 147 | cmd = args = msg = None 148 | if m: 149 | cmd, args, msg = m.groups() 150 | args = args.strip() if args else args 151 | 152 | if cmd == 'PRIVMSG': 153 | if msg.startswith('\x01') and msg.endswith('\x01'): 154 | self.ctcp(args, msg.strip('\x01')) 155 | else: 156 | self.privmsg(args, msg) 157 | elif cmd == 'NOTICE': 158 | if msg.startswith('\x01') and msg.endswith('\x01'): 159 | self.ctcp_reply(args, msg.strip('\x01')) 160 | else: 161 | self.notice(args, msg) 162 | elif cmd == 'MODE': 163 | _ = args.split() 164 | self.mode(_[0], *_[1:]) 165 | elif cmd == 'JOIN': 166 | _ = args.split() 167 | self.join(_[0], _[1] if len(_) > 1 else None) 168 | elif cmd == 'PART': 169 | self.part(args, msg) 170 | elif cmd == 'TOPIC': 171 | self.topic(args, msg) 172 | elif cmd == 'AWAY': 173 | self.away(msg) 174 | elif cmd == 'QUIT': 175 | self.quit(msg) 176 | elif cmd == 'NICK': 177 | self.set_nick(args) 178 | else: 179 | self.send_line(message) 180 | 181 | def dispatch(self, data, iotype='in', client=None): 182 | if iotype == 'in': 183 | self.dispatch_to_clients(data) 184 | super().dispatch(data, iotype, client) 185 | 186 | def dispatch_to_clients(self, data): 187 | for client in self.clients: 188 | client.send(data) 189 | -------------------------------------------------------------------------------- /ircb/irc/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waartaa/ircb/18381b167529ee56d1c5d5d22f857bb27692c397/ircb/irc/plugins/__init__.py -------------------------------------------------------------------------------- /ircb/irc/plugins/autojoins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import irc3 3 | from irc3.plugins.autojoins import AutoJoins as Irc3AutoJoins 4 | 5 | 6 | @irc3.plugin 7 | class Autojoins(Irc3AutoJoins): 8 | 9 | def __init__(self, bot): 10 | super().__init__(bot) 11 | self.kicks_count = {} 12 | 13 | @irc3.event(irc3.rfc.KICK) 14 | def on_kick(self, mask, channel, target, **kwargs): 15 | # noop for now 16 | pass 17 | -------------------------------------------------------------------------------- /ircb/irc/plugins/ircb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import irc3 4 | 5 | from ircb.storeclient import ChannelStore 6 | 7 | 8 | @irc3.plugin 9 | class IrcbPlugin(object): 10 | 11 | def __init__(self, bot): 12 | self.bot = bot 13 | 14 | @irc3.event(irc3.rfc.JOIN) 15 | def on_join(self, mask, channel, **kw): 16 | def callback(): 17 | yield from ChannelStore.create_or_update( 18 | dict( 19 | channel=channel, 20 | network_id=self.bot.config.id, 21 | status='1' 22 | ) 23 | ) 24 | asyncio.Task(callback()) 25 | 26 | @irc3.event(irc3.rfc.PART) 27 | def on_part(self, mask, channel, **kw): 28 | def callback(): 29 | yield from ChannelStore.create_or_update( 30 | dict( 31 | channel=channel, 32 | network_id=self.bot.config.id, 33 | status='3' 34 | ) 35 | ) 36 | asyncio.Task(callback()) 37 | -------------------------------------------------------------------------------- /ircb/irc/plugins/logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import datetime 4 | import irc3 5 | from irc3.plugins.logger import Logger as Irc3Logger 6 | from ircb.storeclient import MessageLogStore, ActivityLogStore 7 | 8 | 9 | class StoreHandler(object): 10 | 11 | def __init__(self, bot): 12 | self.bot = bot 13 | 14 | def __call__(self, event): 15 | if event['event'].lower() == 'privmsg': 16 | asyncio.Task(self.handle_message_log(event)) 17 | elif event['event'].lower() in ('join', 'part', 'quit', 'topic'): 18 | asyncio.Task(self.handle_activity_log(event)) 19 | 20 | def handle_message_log(self, event): 21 | yield from MessageLogStore.create(dict( 22 | hostname=event['host'], 23 | roomname=event['channel'], 24 | message=event['data'], 25 | event=event['event'], 26 | timestamp=datetime.datetime.utcnow().timestamp(), 27 | mask=str(event['mask']), 28 | user_id=self.bot.config.get('user_id'), 29 | from_nickname=event['mask'].nick 30 | )) 31 | 32 | def handle_activity_log(self, event): 33 | yield from ActivityLogStore.create(dict( 34 | hostname=event['host'], 35 | roomname=event['channel'], 36 | message=event['data'], 37 | event=event['event'], 38 | timestamp=datetime.datetime.utcnow().timestamp(), 39 | mask=str(event['mask']), 40 | user_id=self.bot.config.get('user_id') 41 | )) 42 | 43 | 44 | @irc3.plugin 45 | class Logger(Irc3Logger): 46 | 47 | def __init__(self, *args, **kwargs): 48 | super().__init__(*args, **kwargs) 49 | 50 | def process(self, **kwargs): 51 | super().process(**kwargs) 52 | 53 | @irc3.event((r'''(@(?P\S+) )?(?P[A-Z]+) (?P#\S+)''' 54 | r'''(\s:(?P.*)|$)'''), iotype='out') 55 | def on_output(self, event, target=None, data=None, **kwargs): 56 | super().on_output(event, target, data, **kwargs) 57 | 58 | @irc3.event((r'''(@(?P\S+) )?:(?P\S+) (?P[A-Z]+)''' 59 | r''' (?P#\S+)(\s:(?P.*)|$)''')) 60 | def on_input(self, mask, event, target=None, data=None, **kwargs): 61 | super().on_input(mask, event, target, data, **kwargs) 62 | -------------------------------------------------------------------------------- /ircb/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waartaa/ircb/18381b167529ee56d1c5d5d22f857bb27692c397/ircb/lib/__init__.py -------------------------------------------------------------------------------- /ircb/lib/async.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | 4 | 5 | def coroutinize(func): 6 | def wrapper(*args, **kwargs): 7 | loop = asyncio.get_event_loop() 8 | coro = asyncio.coroutine(func) 9 | loop.run_until_complete(coro(*args, **kwargs)) 10 | loop.close() 11 | return wrapper 12 | -------------------------------------------------------------------------------- /ircb/lib/constants/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waartaa/ircb/18381b167529ee56d1c5d5d22f857bb27692c397/ircb/lib/constants/__init__.py -------------------------------------------------------------------------------- /ircb/lib/constants/signals.py: -------------------------------------------------------------------------------- 1 | # IRC Network 2 | NETWORK_CONNECT = 'network-connect' 3 | NETWORK_DISCONNECT = 'network-disconnect' 4 | NETWORK_DISCONNECTED = 'network-disconnected' 5 | NETWORK_CONNECTED = 'network-connected' 6 | 7 | # Stores 8 | 9 | # User store 10 | STORE_USER_CREATE = 'store-user-create' 11 | STORE_USER_CREATED = 'store-user-created' 12 | STORE_USER_GET = 'store-user-get' 13 | STORE_USER_GOT = 'store-user-got' 14 | 15 | # Network store 16 | STORE_NETWORK_CREATE = 'store-network-create' 17 | STORE_NETWORK_CREATED = 'store-network-created' 18 | STORE_NETWORK_GET = 'store-network-get' 19 | STORE_NETWORK_GOT = 'store-network-got' 20 | STORE_NETWORK_UPDATE = 'store-network-update' 21 | STORE_NETWORK_UPDATED = 'store-network-updated' 22 | STORE_NETWORK_DELETE = 'store-network-delete' 23 | STORE_NETWORK_DELETED = 'store-network-deleted' 24 | 25 | # Channel store 26 | STORE_CHANNEL_CREATE = 'store-channel-create' 27 | STORE_CHANNEL_CREATED = 'store-channel-created' 28 | STORE_CHANNEL_GET = 'store-channel-get' 29 | STORE_CHANNEL_GOT = 'store-channel-got' 30 | STORE_CHANNEL_UPDATE = 'store-channel-update' 31 | STORE_CHANNEL_UPDATED = 'store-channel-updated' 32 | STORE_CHANNEL_DELETE = 'store-channel-delete' 33 | STORE_CHANNEL_DELETED = 'store-channel-deleted' 34 | STORE_CHANNEL_CREATE_OR_UPDATE = 'store-channel-create-or-update' 35 | STORE_CHANNEL_CREATE_OR_UPDATE_ERROR = 'store-channel-create-or-update-error' 36 | 37 | # Client store 38 | STORE_CLIENT_CREATE = 'store-client-create' 39 | STORE_CLIENT_CREATED = 'store-client-created' 40 | STORE_CLIENT_DELETE = 'store-client-delete' 41 | STORE_CLIENT_DELETED = 'store-client-deleted' 42 | 43 | # MessageLog store 44 | STORE_MESSAGELOG_GET = 'store-messagelog-get' 45 | STORE_MESSAGELOG_GOT = 'store-messagelog-got' 46 | STORE_MESSAGELOG_CREATE = 'store-messagelog-create' 47 | STORE_MESSAGELOG_CREATED = 'store-messagelog-created' 48 | 49 | # ActivityLog store 50 | STORE_ACTIVITYLOG_GET = 'store-activitylog-get' 51 | STORE_ACTIVITYLOG_GOT = 'store-activitylog-got' 52 | STORE_ACTIVITYLOG_CREATE = 'store-activitylog-create' 53 | STORE_ACTIVITYLOG_CREATED = 'store-activitylog-created' 54 | -------------------------------------------------------------------------------- /ircb/lib/dispatcher/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import aiozmq.rpc 4 | import aioredis 5 | import logging 6 | import sys 7 | 8 | from collections import defaultdict 9 | 10 | from ircb.config import settings 11 | 12 | logger = logging.getLogger('dispatcher') 13 | 14 | 15 | class Handler(aiozmq.rpc.AttrHandler): 16 | 17 | def __init__(self, dispatcher): 18 | self._dispatcher = dispatcher 19 | # lock for registering subscriber 20 | self._lock = asyncio.Lock() 21 | 22 | @aiozmq.rpc.method 23 | def send(self, signal, data, taskid=None): 24 | try: 25 | signals = [signal, '__all__'] 26 | for s in signals: 27 | for callback in self._dispatcher._signal_listeners.get(s, []): 28 | callback(signal, data, taskid) 29 | logger.debug('SEND: {} {} {}'.format(signal, data, taskid)) 30 | except Exception as e: 31 | logger.error('SEND ERROR: {} {} {} {}'.format( 32 | e, signal, data, taskid), exc_info=True) 33 | 34 | @asyncio.coroutine 35 | @aiozmq.rpc.method 36 | def register_sub(self, subscriber_addr, key): 37 | yield from self._lock.acquire() 38 | try: 39 | connections = self._dispatcher.publisher.transport.connections() 40 | if subscriber_addr in connections: 41 | self._lock.release() 42 | return 43 | self._dispatcher.publisher.transport.connect(subscriber_addr) 44 | try: 45 | logger.debug('Connecting to redis at {}:{}...'.format( 46 | settings.REDIS_HOST, settings.REDIS_PORT)) 47 | redis = yield from aioredis.create_redis( 48 | (settings.REDIS_HOST, settings.REDIS_PORT), 49 | timeout=5 50 | ) 51 | logger.debug('Connected to redis') 52 | except (IOError, ConnectionRefusedError) as e: 53 | logger.error('Failed to connect to redis at {}:{}'.format( 54 | settings.REDIS_HOST, settings.REDIS_PORT)) 55 | logger.error('Exiting...') 56 | sys.exit(1) 57 | yield from redis.set(key, 1) 58 | redis.close() 59 | finally: 60 | if self._lock.locked(): 61 | self._lock.release() 62 | 63 | 64 | class Dispatcher(object): 65 | 66 | def __init__(self, role, loop=None): 67 | self.loop = loop or asyncio.get_event_loop() 68 | self.role = role 69 | self._signal_listeners = defaultdict(set) 70 | self.handler = Handler(self) 71 | self.subscriber = self.publisher = None 72 | self.lock = asyncio.Lock() 73 | self.queue = asyncio.Queue(loop=self.loop) 74 | asyncio.Task(self.lock.acquire()) 75 | asyncio.Task(self.setup_pubsub()) 76 | 77 | @asyncio.coroutine 78 | def process_queue(self): 79 | while True: 80 | while self.lock.locked(): 81 | yield from asyncio.sleep(0.01) 82 | continue 83 | try: 84 | (signal, data, taskid) = self.queue.get_nowait() 85 | yield from self._send(signal, data, taskid) 86 | except asyncio.QueueEmpty: 87 | break 88 | 89 | @asyncio.coroutine 90 | def setup_pubsub(self): 91 | try: 92 | logger.debug('Connecting to redis at {}:{}...'.format( 93 | settings.REDIS_HOST, settings.REDIS_PORT)) 94 | redis = yield from aioredis.create_redis( 95 | (settings.REDIS_HOST, settings.REDIS_PORT), 96 | timeout=5 97 | ) 98 | logger.debug('Connected to redis') 99 | except (IOError, ConnectionRefusedError) as e: 100 | logger.error('Failed to connect to redis at {}:{}'.format( 101 | settings.REDIS_HOST, settings.REDIS_PORT)) 102 | logger.error('Exiting...') 103 | sys.exit(1) 104 | if self.role == 'stores': 105 | bind_addr = settings.SUBSCRIBER_ENDPOINTS[self.role] 106 | else: 107 | bind_addr = 'tcp://{host}:*'.format(host=settings.INTERNAL_HOST) 108 | self.subscriber = yield from aiozmq.rpc.serve_pubsub( 109 | self.handler, subscribe='', 110 | bind=bind_addr, 111 | log_exceptions=True) 112 | subscriber_addr = list(self.subscriber.transport.bindings())[0] 113 | self.publisher = yield from aiozmq.rpc.connect_pubsub() 114 | if self.role == 'storeclient': 115 | self.publisher.transport.connect( 116 | settings.SUBSCRIBER_ENDPOINTS['stores']) 117 | _key = 'SUBSCRIBER_REGISTERED_{}'.format(subscriber_addr) 118 | ret = 0 119 | yield from redis.set(_key, ret) 120 | while ret != b'1': 121 | yield from self.publisher.publish( 122 | 'register_sub' 123 | ).register_sub( 124 | subscriber_addr, _key 125 | ) 126 | ret = yield from redis.get(_key) 127 | yield from asyncio.sleep(0.01) 128 | self.lock.release() 129 | redis.close() 130 | if self.role == 'stores': 131 | logger.info('Running {role} at {addr}...'.format( 132 | role=self.role, addr=bind_addr)) 133 | 134 | @property 135 | def subscriber_endpoints(self): 136 | return [endpoint for role, endpoint in 137 | settings.SUBSCRIBER_ENDPOINTS.items() 138 | if role != self.role] 139 | 140 | def send(self, signal, data, taskid=None): 141 | asyncio.Task(self.enqueue((signal, data, taskid))) 142 | 143 | @asyncio.coroutine 144 | def enqueue(self, data): 145 | empty = self.queue.empty() 146 | yield from self.queue.put(data) 147 | if empty: 148 | asyncio.Task(self.process_queue()) 149 | 150 | @asyncio.coroutine 151 | def _send(self, signal, data, taskid=None): 152 | logger.debug('PUBLISH from %s: %s' % 153 | (self.role, (signal, data, taskid))) 154 | yield from self.publisher.publish(signal).send(signal, data, taskid) 155 | 156 | def register(self, callback, signal=None): 157 | try: 158 | signal = signal or '__all__' 159 | if callback not in self._signal_listeners.get('__all__', []): 160 | callbacks = self._signal_listeners[signal] 161 | callbacks.add(callback) 162 | logger.debug('REGISTER: {} {}'.format(callback, signal)) 163 | except Exception as e: 164 | logger.error('REGISTER ERROR: {} {} {}'.format( 165 | e, callback, signal), exc_info=True) 166 | 167 | def run_forever(self): 168 | self.loop.run_forever() 169 | -------------------------------------------------------------------------------- /ircb/models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .network import Network 3 | from .user import User 4 | from .channel import Channel 5 | from .client import Client 6 | from .logs import MessageLog, ActivityLog 7 | from .lib import create_tables, get_session, Base 8 | 9 | __all__ = [ 10 | 'Channel', 11 | 'Client', 12 | 'Network', 13 | 'User', 14 | 'MessageLog', 15 | 'ActivityLog', 16 | 'get_session', 17 | 'create_tables', 18 | 'Base' 19 | ] 20 | -------------------------------------------------------------------------------- /ircb/models/channel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | 4 | from sqlalchemy import Column, String, Integer, ForeignKey, DateTime 5 | from sqlalchemy import UniqueConstraint 6 | from sqlalchemy_utils import ChoiceType 7 | 8 | from ircb.config import settings 9 | from ircb.models.lib import Base 10 | 11 | CHANNEL_STATUS_TYPES = ( 12 | ('0', 'Connecting'), 13 | ('1', 'Connected'), 14 | ('2', 'Disconnecting'), 15 | ('3', 'Disconnected') 16 | ) 17 | 18 | 19 | class Channel(Base): 20 | __tablename__ = 'channels' 21 | __table_args__ = ( 22 | UniqueConstraint('network_id', 'name'), 23 | ) 24 | id = Column(Integer, primary_key=True) 25 | name = Column(String(50), nullable=False) 26 | password = Column(String(100), nullable=False, default='') 27 | network_id = Column(Integer(), 28 | ForeignKey('networks.id', ondelete='CASCADE'), 29 | nullable=False) 30 | user_id = Column(Integer(), 31 | ForeignKey('users.id', ondelete='CASCADE'), 32 | nullable=False) 33 | 34 | # Runtime fields 35 | status = Column(ChoiceType(CHANNEL_STATUS_TYPES)) 36 | 37 | # timestamps 38 | created = Column(DateTime, default=datetime.datetime.utcnow) 39 | last_updated = Column(DateTime, default=datetime.datetime.utcnow) 40 | 41 | def to_dict(self): 42 | d = super().to_dict() 43 | d['status'] = self.status if isinstance(self.status, str) \ 44 | else self.status.code 45 | return d 46 | -------------------------------------------------------------------------------- /ircb/models/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | 4 | from sqlalchemy import Column, DateTime, ForeignKey, Integer, String 5 | from sqlalchemy import UniqueConstraint 6 | 7 | from ircb.models.lib import Base 8 | 9 | 10 | class Client(Base): 11 | __tablename__ = 'clients' 12 | __table_args__ = ( 13 | UniqueConstraint('socket', 'network_id'), 14 | ) 15 | 16 | id = Column(Integer, primary_key=True) 17 | socket = Column(String(100), nullable=False) 18 | network_id = Column( 19 | Integer(), ForeignKey('networks.id', ondelete='CASCADE'), 20 | nullable=False) 21 | user_id = Column( 22 | Integer(), ForeignKey('users.id', ondelete='CASCADE'), 23 | nullable=False) 24 | 25 | # created 26 | created = Column(DateTime, default=datetime.datetime.utcnow) 27 | last_updated = Column(DateTime, default=datetime.datetime.utcnow) 28 | -------------------------------------------------------------------------------- /ircb/models/lib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy import create_engine 6 | from sqlalchemy.orm import sessionmaker 7 | from sqlalchemy.orm import scoped_session 8 | 9 | from ircb.config import settings 10 | 11 | 12 | class _Base(object): 13 | 14 | def to_dict(self, serializable=True): 15 | d = {} 16 | for col in self.__table__.columns: 17 | d[col.name] = getattr(self, col.name) 18 | if serializable: 19 | if getattr(self, 'created'): 20 | d['created'] = self.created.timestamp() 21 | if getattr(self, 'last_updated'): 22 | d['last_updated'] = self.last_updated.timestamp() 23 | return d 24 | 25 | @classmethod 26 | def from_dict(cls, data, serialized=True): 27 | if serialized: 28 | if 'created' in data: 29 | data['created'] = datetime.datetime.fromtimestamp( 30 | data['created']) 31 | if 'last_updated' in data: 32 | data['last_updated'] = datetime.datetime.fromtimestamp( 33 | data['last_updated']) 34 | return cls(**data) 35 | 36 | Base = declarative_base(cls=_Base) 37 | 38 | 39 | def create_tables(db_uri=settings.DB_URI): 40 | engine = create_engine(db_uri) 41 | Base.metadata.create_all(engine) 42 | 43 | 44 | def get_session(db_uri=settings.DB_URI): 45 | engine = create_engine(db_uri) 46 | scopedsession = scoped_session(sessionmaker(bind=engine)) 47 | return scopedsession 48 | -------------------------------------------------------------------------------- /ircb/models/logs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | import sqlalchemy as sa 4 | 5 | from ircb.models.lib import Base 6 | 7 | 8 | class BaseLog(object): 9 | id = sa.Column(sa.Integer, primary_key=True) 10 | 11 | hostname = sa.Column(sa.String(100), nullable=False) 12 | roomname = sa.Column(sa.String(255), nullable=False) 13 | 14 | message = sa.Column(sa.String(2048), default='') 15 | event = sa.Column(sa.String(20), nullable=False) 16 | timestamp = sa.Column(sa.TIMESTAMP(timezone=True)) 17 | mask = sa.Column(sa.String(100), default='') 18 | 19 | user_id = sa.Column(sa.Integer) 20 | 21 | # timestamps 22 | created = sa.Column(sa.DateTime, default=datetime.datetime.utcnow) 23 | last_updated = sa.Column(sa.DateTime, 24 | default=datetime.datetime.utcnow) 25 | 26 | def to_dict(self, serializable=True): 27 | d = super().to_dict() 28 | if serializable: 29 | d['timestamp'] = self.timestamp.timestamp() 30 | return d 31 | 32 | @classmethod 33 | def from_dict(cls, data, serialized=True): 34 | if serialized: 35 | data['timestamp'] = datetime.datetime.fromtimestamp( 36 | data['timestamp']) 37 | if 'created' in data: 38 | data['created'] = datetime.datetime.fromtimestamp( 39 | data['created']) 40 | if 'last_updated' in data: 41 | data['last_updated'] = datetime.datetime.fromtimestamp( 42 | data['last_updated']) 43 | return cls(**data) 44 | 45 | 46 | class MessageLog(BaseLog, Base): 47 | """ 48 | Network/Channel/PM messages 49 | """ 50 | __tablename__ = 'message_logs' 51 | 52 | from_nickname = sa.Column(sa.String(20)) 53 | from_user_id = sa.Column(sa.Integer, nullable=True, default=None) 54 | 55 | 56 | class ActivityLog(BaseLog, Base): 57 | """ 58 | Channel activity(join, part, quit) logs 59 | """ 60 | __tablename__ = 'activity_logs' 61 | -------------------------------------------------------------------------------- /ircb/models/network.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | 4 | from hashlib import md5 5 | 6 | import sqlalchemy as sa 7 | 8 | from sqlalchemy_utils import ChoiceType, Choice 9 | 10 | from ircb.models.lib import Base, get_session 11 | from ircb.models.user import User 12 | from ircb.config import settings 13 | 14 | NETWORK_STATUS_TYPES = ( 15 | ('0', 'Connecting'), 16 | ('1', 'Connected'), 17 | ('2', 'Disconnecting'), 18 | ('3', 'Disconnected') 19 | ) 20 | SSL_VERIFY_CHOICES = ( 21 | ('CERT_NONE', 'No certs required'), 22 | ('CERT_OPTIONAL', 'Optional cert'), 23 | ('CERT_REQUIRED', 'Cert required') 24 | ) 25 | session = get_session() 26 | 27 | 28 | def _create_access_token(user_id, network_name): 29 | user = session.query(User).get(user_id) 30 | return md5('{}{}{}{}'.format(settings.SECRET_KEY, 31 | user.username, 32 | network_name, 33 | datetime.datetime.utcnow()).encode() 34 | ).hexdigest() 35 | 36 | 37 | class Network(Base): 38 | __tablename__ = 'networks' 39 | __table_args__ = ( 40 | sa.UniqueConstraint('user_id', 'name'), 41 | ) 42 | id = sa.Column(sa.Integer, primary_key=True) 43 | name = sa.Column(sa.String(255), nullable=False) 44 | nickname = sa.Column(sa.String(20), nullable=False) 45 | hostname = sa.Column(sa.String(100), nullable=False) 46 | port = sa.Column(sa.Integer, nullable=False) 47 | realname = sa.Column(sa.String(100), nullable=False, default='') 48 | username = sa.Column(sa.String(50), nullable=False, default='') 49 | password = sa.Column(sa.String(100), nullable=False, default='') 50 | usermode = sa.Column(sa.String(1), nullable=False, default='0') 51 | ssl = sa.Column(sa.Boolean(), default=False) 52 | ssl_verify = sa.Column(ChoiceType(SSL_VERIFY_CHOICES), 53 | default=Choice(*SSL_VERIFY_CHOICES[0])) 54 | 55 | access_token = sa.Column(sa.String(100), nullable=False, unique=True, 56 | default=lambda context: _create_access_token( 57 | context.current_parameters['user_id'], 58 | context.current_parameters['name'])) 59 | user_id = sa.Column( 60 | sa.Integer(), sa.ForeignKey(User.id, ondelete='CASCADE'), 61 | nullable=False) 62 | 63 | # Runtime fields 64 | current_nickname = sa.Column(sa.String(20), nullable=True) 65 | status = sa.Column(ChoiceType(NETWORK_STATUS_TYPES), 66 | default=Choice(*NETWORK_STATUS_TYPES[3])) 67 | 68 | # Remote socket info 69 | rhost = sa.Column(sa.String(100), nullable=True) 70 | rport = sa.Column(sa.Integer(), nullable=True) 71 | 72 | # Local socket info 73 | lhost = sa.Column(sa.String(100), nullable=True) 74 | lport = sa.Column(sa.Integer(), nullable=True) 75 | 76 | # timestamps 77 | created = sa.Column(sa.DateTime, default=datetime.datetime.utcnow) 78 | last_updated = sa.Column(sa.DateTime, 79 | default=datetime.datetime.utcnow) 80 | 81 | def create_access_token(self): 82 | return _create_access_token(self.user.id, self.name) 83 | 84 | def to_dict(self, serializable=False): 85 | d = super().to_dict() 86 | ssl_verify = self.ssl_verify and self.ssl_verify if isinstance( 87 | self.ssl_verify, str) else self.ssl_verify.code 88 | status = self.status and ( 89 | self.status if isinstance(self.status, str) else self.status.code) 90 | d['ssl_verify'] = ssl_verify 91 | d['status'] = status 92 | return d 93 | -------------------------------------------------------------------------------- /ircb/models/user.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from flask_user import UserMixin 4 | from sqlalchemy import Column, String, Unicode, Boolean, ForeignKey, Integer 5 | from sqlalchemy import DateTime 6 | from sqlalchemy.orm import relationship, backref 7 | from sqlalchemy_utils import PasswordType 8 | 9 | from ircb.models.lib import Base 10 | 11 | 12 | class User(Base, UserMixin): 13 | __tablename__ = 'users' 14 | id = Column(Integer, primary_key=True) 15 | 16 | # User authentication information (required for Flask-User) 17 | username = Column(Unicode(30), nullable=False, unique=True) 18 | email = Column(Unicode(255), nullable=False, unique=True) 19 | confirmed_at = Column(DateTime()) 20 | password = Column( 21 | PasswordType(schemes=['pbkdf2_sha512', 'md5_crypt']), 22 | nullable=False, server_default='') 23 | reset_password_token = Column(String(100), nullable=False, default='') 24 | 25 | # User information 26 | active = Column('is_active', Boolean(), nullable=False, server_default='0') 27 | first_name = Column(Unicode(50), nullable=True, server_default=u'') 28 | last_name = Column(Unicode(50), nullable=True, server_default=u'') 29 | 30 | # Relationships 31 | roles = relationship('Role', secondary='users_roles', 32 | backref=backref('users', lazy='dynamic')) 33 | 34 | # Timestamps 35 | created = Column(DateTime, default=datetime.datetime.utcnow) 36 | last_updated = Column(DateTime, default=datetime.datetime.utcnow) 37 | 38 | def to_dict(self, serializable=False): 39 | d = super().to_dict() 40 | d.pop('password') 41 | d['is_active'] = self.is_active() 42 | return d 43 | 44 | def authenticate(self, password): 45 | if self.password == password: 46 | return True 47 | return False 48 | 49 | 50 | class Role(Base): 51 | __tablename__ = 'roles' 52 | id = Column(Integer(), primary_key=True) 53 | name = Column(String(50), nullable=False, server_default=u'', 54 | unique=True) # for @roles_accepted() 55 | label = Column(Unicode(255), server_default=u'') # for display purposes 56 | 57 | 58 | class UsersRoles(Base): 59 | __tablename__ = 'users_roles' 60 | id = Column(Integer(), primary_key=True) 61 | user_id = Column(Integer(), ForeignKey('users.id', ondelete='CASCADE')) 62 | role_id = Column(Integer(), ForeignKey('roles.id', ondelete='CASCADE')) 63 | -------------------------------------------------------------------------------- /ircb/publishers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ircb.publishers.logs import MessageLogPublisher 3 | from ircb.publishers.networks import NetworkPublisher 4 | from ircb.publishers.channels import ChannelPublisher 5 | 6 | if __name__ == '__main__': 7 | import asyncio 8 | import sys 9 | from ircb.storeclient import initialize 10 | from ircb.utils.config import load_config 11 | load_config() 12 | initialize() 13 | try: 14 | hostname = sys.argv[1] 15 | roomname = sys.argv[2] 16 | user_id = sys.argv[3] 17 | except: 18 | print("Usage: __init__.py '' '' ''") 19 | sys.exit(1) 20 | 21 | message_log_pub = MessageLogPublisher(hostname, roomname, int(user_id)) 22 | message_log_pub.run() 23 | 24 | network_pub = NetworkPublisher(int(user_id)) 25 | network_pub.run() 26 | 27 | channel_pub = ChannelPublisher(int(user_id)) 28 | channel_pub.run() 29 | 30 | loop = asyncio.get_event_loop() 31 | loop.run_forever() 32 | -------------------------------------------------------------------------------- /ircb/publishers/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import asyncio 4 | import logging 5 | from collections import deque 6 | 7 | logger = logging.getLogger('publisher') 8 | 9 | 10 | class BasePublisher(object): 11 | 12 | store = None 13 | name = None 14 | 15 | def __init__(self): 16 | self.fetched = False 17 | self.callbacks = { 18 | 'update': set(), 19 | 'create': set(), 20 | 'fetch': set() 21 | } 22 | self.limit = None 23 | self.results = deque(maxlen=self.limit) if self.limit else \ 24 | deque() 25 | self.results_count = 0 26 | self.fields = [] 27 | self.index = {} 28 | 29 | @property 30 | def id(self): 31 | raise NotImplementedError 32 | 33 | def run(self): 34 | self.store.on('create', self.handle_create, raw=True) 35 | self.store.on('update', self.handle_update, raw=True) 36 | self.store.on('delete', self.handle_delete, raw=True) 37 | asyncio.Task(self._fetch()) 38 | 39 | def on(self, event, callback): 40 | """ 41 | Register callbacks for an event. 42 | """ 43 | callbacks = self.callbacks.get(event) 44 | if callbacks is not None: 45 | callbacks.add(callback) 46 | 47 | @asyncio.coroutine 48 | def _fetch(self): 49 | results = yield from self.fetch() 50 | self.handle_fetch(results) 51 | 52 | def fetch(self): 53 | return [] 54 | 55 | def handle_fetch(self, results): 56 | self.normalize(results) 57 | for callback in self.callbacks.get('fetch') or set(): 58 | callback(results) 59 | self.fetched = True 60 | 61 | def normalize(self, results): 62 | for result in results: 63 | self.index[result['id']] = result 64 | self.results.append(result['id']) 65 | self.results_count += 1 66 | logger.debug('normalized index: %s', self.index) 67 | logger.debug('normalized results: %s', self.results) 68 | 69 | def handle_update(self, data): 70 | """ 71 | Check if an update operation on a row of message_logs table 72 | affects our data, and update it if needed. 73 | """ 74 | if self.skip_update(data): 75 | logger.debug('skipping update data', data) 76 | return 77 | if data['id'] in self.index: 78 | self.index[data['id']] = data 79 | 80 | for callback in self.callbacks.get('update') or set(): 81 | callback(data) 82 | 83 | def handle_create(self, data): 84 | """ 85 | Check if an insert operation in message_logs table affects our 86 | results. If yes, append it to results. 87 | """ 88 | if self.skip_create(data): 89 | logger.debug('skipping create data: {}'.format(data)) 90 | return 91 | 92 | if self.limit and self.results_count == self.limit: 93 | logger.debug('Removing id: %s from index', self.results[0]) 94 | self.index.pop(self.results[0], None) 95 | else: 96 | self.results_count += 1 97 | 98 | self.index[data['id']] = data 99 | self.results.append(data['id']) 100 | logger.debug('updated results: %s, %s', self.results, self.index) 101 | for callback in self.callbacks.get('create') or set(): 102 | callback(data) 103 | 104 | def handle_delete(self, data): 105 | pass 106 | 107 | def skip_create(self, data): 108 | return False 109 | 110 | def skip_update(self, data): 111 | return False 112 | -------------------------------------------------------------------------------- /ircb/publishers/channels.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ircb.storeclient import ChannelStore 4 | 5 | from .base import BasePublisher 6 | 7 | 8 | class ChannelPublisher(BasePublisher): 9 | 10 | name = 'channels' 11 | store = ChannelStore 12 | 13 | def __init__(self, user_id, network_id=None): 14 | self.user_id = user_id 15 | self.network_id = network_id 16 | super().__init__() 17 | 18 | @property 19 | def id(self): 20 | return '{name}-{user_id}-{network_id}'.format( 21 | name=self.name, user_id=self.user_id, 22 | network_id=self.network_id) 23 | 24 | def fetch(self): 25 | filter = { 26 | 'user_id': self.user_id 27 | } 28 | if self.network_id: 29 | filter['network_id'] = self.network_id 30 | results = yield from ChannelStore.get({ 31 | 'filter': filter 32 | }, raw=True) 33 | return results 34 | 35 | def skip_create(self, data): 36 | return self.skip_update(data) 37 | 38 | def skip_update(self, data): 39 | return data['user_id'] != self.user_id or \ 40 | (data['network_id'] != self.network_id 41 | if self.network_id else False) 42 | -------------------------------------------------------------------------------- /ircb/publishers/logs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import logging 4 | 5 | from ircb.storeclient import MessageLogStore 6 | from collections import deque 7 | 8 | from .base import BasePublisher 9 | 10 | logger = logging.getLogger('publisher') 11 | 12 | 13 | class MessageLogPublisher(BasePublisher): 14 | """ 15 | Publish latest logs in a room in realtime. 16 | 17 | It fetches latest chat logs for a room from the store, initially. 18 | Then, it keeps on listening to the store for WRITE events on 19 | MessageLog model and keeps the record of latest chat logs fetched 20 | always updated. 21 | 22 | This can be used to push latest chat logs in a room to a client 23 | in realtime. 24 | """ 25 | name = 'latest_message_logs' 26 | store = MessageLogStore 27 | 28 | def __init__(self, hostname, roomname, user_id, limit=30): 29 | self.limit = limit 30 | self.hostname = hostname 31 | self.roomname = roomname 32 | self.user_id = user_id 33 | super().__init__() 34 | 35 | @property 36 | def id(self): 37 | return '{name}::{roomname}::{user_id}::{limit}'.format( 38 | name=self.name, roomname=self.roomname, 39 | user_id=self.user_id, limit=self.limit) 40 | 41 | def fetch(self): 42 | """ 43 | Fetch initial latest chat logs for a room. 44 | """ 45 | results = yield from MessageLogStore.get({ 46 | 'filter': { 47 | 'hostname': self.hostname, 48 | 'roomname': self.roomname, 49 | 'user_id': self.user_id 50 | }, 51 | 'order_by': ('-timestamp',), 52 | 'limit': self.limit, 53 | # 'fields': self.fields, 54 | 'sort': 'timestamp' 55 | }, raw=True) 56 | logger.debug('fetched: %s', results) 57 | return results 58 | 59 | def skip_create(self, data): 60 | skip = False 61 | if self.skip_update(data): 62 | skip = skip or True 63 | if self.results and data['timestamp'] < self.index[ 64 | self.results[-1]]['timestamp']: 65 | skip = skip or True 66 | if not self.fetched: 67 | skip = skip or True 68 | return skip 69 | 70 | def skip_update(self, data): 71 | """ 72 | We'll skip updating our results if the insert/update event 73 | is not relevant to us. 74 | """ 75 | return data['user_id'] != self.user_id or \ 76 | data['roomname'] != self.roomname or \ 77 | data['hostname'] != self.hostname 78 | -------------------------------------------------------------------------------- /ircb/publishers/networks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ircb.storeclient import NetworkStore 4 | 5 | from .base import BasePublisher 6 | 7 | 8 | class NetworkPublisher(BasePublisher): 9 | 10 | name = 'networks' 11 | store = NetworkStore 12 | 13 | def __init__(self, user_id): 14 | super().__init__() 15 | self.user_id = user_id 16 | 17 | @property 18 | def id(self): 19 | return '{name}::{user_id}'.format( 20 | name=self.name, user_id=self.user_id) 21 | 22 | def fetch(self): 23 | results = yield from NetworkStore.get({ 24 | 'query': { 25 | 'user_id': self.user_id 26 | } 27 | }, raw=True) 28 | return results 29 | 30 | def skip_create(self, data): 31 | return self.skip_update(data) 32 | 33 | def skip_update(self, data): 34 | return data['user_id'] != self.user_id 35 | -------------------------------------------------------------------------------- /ircb/storeclient/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | from .network import NetworkStore 5 | from .client import ClientStore 6 | from .channel import ChannelStore 7 | from .user import UserStore 8 | from .base import init 9 | from .logs import MessageLogStore, ActivityLogStore 10 | 11 | 12 | def initialize(): 13 | init() 14 | NetworkStore.initialize() 15 | MessageLogStore.initialize() 16 | 17 | 18 | __all__ = [ 19 | 'ClientStore', 20 | 'NetworkStore', 21 | 'ChannelStore', 22 | 'UserStore', 23 | 'MessageLogStore', 24 | 'ActivityLogStore', 25 | 'initialize' 26 | ] 27 | -------------------------------------------------------------------------------- /ircb/storeclient/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | 4 | from uuid import uuid1 5 | 6 | from ircb.lib.dispatcher import Dispatcher 7 | 8 | 9 | class BaseStore(object): 10 | CREATE_SIGNAL = None 11 | CREATED_SIGNAL = None 12 | UPDATE_SIGNAL = None 13 | UPDATED_SIGNAL = None 14 | DELETE_SIGNAL = None 15 | DELETED_SIGNAL = None 16 | CREATE_OR_UPDATE_SIGNAL = None 17 | 18 | callbacks = { 19 | 'create': set(), 20 | 'update': set(), 21 | 'delete': set() 22 | } 23 | 24 | raw_callbacks = { 25 | 'create': set(), 26 | 'update': set(), 27 | 'delete': set() 28 | } 29 | 30 | model = None 31 | 32 | @classmethod 33 | def initialize(cls): 34 | signals = [cls.CREATED_SIGNAL, cls.UPDATED_SIGNAL, cls.DELETED_SIGNAL] 35 | for signal in signals: 36 | if signal is None: 37 | continue 38 | dispatcher.register(cls._on_message, signal=signal) 39 | 40 | @classmethod 41 | def get(cls, data, raw=False): 42 | result = yield from cls._get(data) 43 | if isinstance(result, dict): 44 | result = cls.model.from_dict(result) if raw is False else result 45 | elif isinstance(result, tuple): 46 | if raw: 47 | result = result 48 | else: 49 | result = [cls.model.from_dict(item) for item in result] 50 | return result 51 | 52 | @classmethod 53 | def create(cls, data, async=False, timeout=10): 54 | result = yield from cls._create(data, async) 55 | return cls.model(**result) 56 | 57 | @classmethod 58 | def update(cls, data): 59 | result = yield from cls._update(data) 60 | return cls.model(**result) 61 | 62 | @classmethod 63 | def delete(cls, data): 64 | result = yield from cls._delete(data) 65 | return result 66 | 67 | @classmethod 68 | def create_or_update(cls, data): 69 | result = yield from cls._create_or_update(data) 70 | return cls.model.from_dict(result) 71 | 72 | @classmethod 73 | def _get(cls, data): 74 | task_id = cls.get_task_id(data) 75 | fut = asyncio.Future() 76 | 77 | def callback(signal, data, taskid=None): 78 | if taskid == task_id: 79 | fut.set_result(data) 80 | 81 | dispatcher.register(callback, signal=cls.GOT_SIGNAL) 82 | dispatcher.send(cls.GET_SIGNAL, data, taskid=task_id) 83 | 84 | result = yield from fut 85 | return fut.result() 86 | 87 | @classmethod 88 | def _create(cls, data, async=False): 89 | task_id = cls.get_task_id(data) 90 | fut = asyncio.Future() 91 | 92 | def callback(signal, data, taskid=None): 93 | if taskid == task_id: 94 | fut.set_result(data) 95 | 96 | dispatcher.register(callback, signal=cls.CREATED_SIGNAL) 97 | dispatcher.send(cls.CREATE_SIGNAL, data, taskid=task_id) 98 | 99 | result = yield from fut 100 | return fut.result() 101 | 102 | @classmethod 103 | def _update(cls, data): 104 | task_id = cls.get_task_id(data) 105 | future = asyncio.Future() 106 | 107 | def callback(signal, data, taskid=None): 108 | if taskid == task_id: 109 | future.set_result(data) 110 | dispatcher.register(callback, signal=cls.UPDATED_SIGNAL) 111 | dispatcher.send(cls.UPDATE_SIGNAL, data, taskid=task_id) 112 | 113 | result = yield from future 114 | return result 115 | 116 | @classmethod 117 | def _delete(cls, data): 118 | task_id = cls.get_task_id(data) 119 | future = asyncio.Future() 120 | 121 | def callback(signal, data, taskid=None): 122 | if taskid == task_id: 123 | future.set_result(data) 124 | dispatcher.register(callback, signal=cls.DELETED_SIGNAL) 125 | dispatcher.send(cls.DELETE_SIGNAL, data, taskid=task_id) 126 | 127 | result = yield from future 128 | return result 129 | 130 | @classmethod 131 | def _create_or_update(cls, data, async=False): 132 | task_id = cls.get_task_id(data) 133 | fut = asyncio.Future() 134 | 135 | def callback(signal, data, taskid=None): 136 | if taskid == task_id: 137 | fut.set_result(data) 138 | 139 | dispatcher.register(callback, signal=cls.CREATED_SIGNAL) 140 | dispatcher.register(callback, signal=cls.UPDATED_SIGNAL) 141 | dispatcher.send(cls.CREATE_OR_UPDATE_SIGNAL, data, taskid=task_id) 142 | 143 | result = yield from fut 144 | return fut.result() 145 | 146 | @classmethod 147 | def on(cls, action, callback, remove=False, raw=False): 148 | callbacks = cls.raw_callbacks if raw else cls.callbacks 149 | if remove: 150 | callbacks[action].remove(callback) 151 | else: 152 | callbacks[action].add(callback) 153 | 154 | @classmethod 155 | def _on_message(cls, signal, data, taskid=None): 156 | callbacks = None 157 | raw_callbacks = None 158 | if signal == cls.CREATED_SIGNAL: 159 | callbacks = cls.callbacks['create'] 160 | raw_callbacks = cls.raw_callbacks['create'] 161 | elif signal == cls.UPDATED_SIGNAL: 162 | callbacks = cls.callbacks['update'] 163 | raw_callbacks = cls.raw_callbacks['update'] 164 | elif signal == cls.DELETED_SIGNAL: 165 | callbacks = cls.callbacks['delete'] 166 | raw_callbacks = cls.raw_callbacks['delete'] 167 | if callbacks: 168 | for callback in callbacks: 169 | callback(cls.model.from_dict(data)) 170 | if raw_callbacks: 171 | for callback in raw_callbacks: 172 | callback(data) 173 | 174 | @classmethod 175 | def get_task_id(self, data): 176 | return str(uuid1()) 177 | 178 | @classmethod 179 | def fields(cls): 180 | return [col.name for col in cls.model.__table__.columns] 181 | 182 | 183 | def init(): 184 | global dispatcher 185 | dispatcher = Dispatcher(role='storeclient') 186 | 187 | dispatcher = None 188 | -------------------------------------------------------------------------------- /ircb/storeclient/channel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ircb.storeclient.base import BaseStore 3 | from ircb.models import Channel 4 | from ircb.lib.constants.signals import (STORE_CHANNEL_CREATE, 5 | STORE_CHANNEL_CREATED, 6 | STORE_CHANNEL_GET, 7 | STORE_CHANNEL_GOT, 8 | STORE_CHANNEL_UPDATE, 9 | STORE_CHANNEL_UPDATED, 10 | STORE_CHANNEL_DELETE, 11 | STORE_CHANNEL_DELETED, 12 | STORE_CHANNEL_CREATE_OR_UPDATE) 13 | 14 | 15 | class ChannelStore(BaseStore): 16 | CREATE_SIGNAL = STORE_CHANNEL_CREATE 17 | CREATED_SIGNAL = STORE_CHANNEL_CREATED 18 | GET_SIGNAL = STORE_CHANNEL_GET 19 | GOT_SIGNAL = STORE_CHANNEL_GOT 20 | UPDATE_SIGNAL = STORE_CHANNEL_UPDATE 21 | UPDATED_SIGNAL = STORE_CHANNEL_UPDATED 22 | DELETE_SIGNAL = STORE_CHANNEL_DELETE 23 | DELETED_SIGNAL = STORE_CHANNEL_DELETED 24 | CREATE_OR_UPDATE_SIGNAL = STORE_CHANNEL_CREATE_OR_UPDATE 25 | 26 | model = Channel 27 | -------------------------------------------------------------------------------- /ircb/storeclient/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ircb.models import Client 3 | from ircb.storeclient.base import BaseStore 4 | from ircb.lib.constants.signals import (STORE_CLIENT_CREATE, 5 | STORE_CLIENT_CREATED, 6 | STORE_CLIENT_DELETE, 7 | STORE_CLIENT_DELETED) 8 | 9 | 10 | class ClientStore(BaseStore): 11 | CREATE_SIGNAL = STORE_CLIENT_CREATE 12 | CREATED_SIGNAL = STORE_CLIENT_CREATED 13 | DELETE_SIGNAL = STORE_CLIENT_DELETE 14 | DELETED_SIGNAL = STORE_CLIENT_DELETED 15 | 16 | model = Client 17 | -------------------------------------------------------------------------------- /ircb/storeclient/logs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ircb.models import MessageLog, ActivityLog 3 | from ircb.storeclient.base import BaseStore 4 | from ircb.lib.constants.signals import (STORE_MESSAGELOG_CREATE, 5 | STORE_MESSAGELOG_CREATED, 6 | STORE_MESSAGELOG_GET, 7 | STORE_MESSAGELOG_GOT, 8 | STORE_ACTIVITYLOG_CREATE, 9 | STORE_ACTIVITYLOG_CREATED, 10 | STORE_ACTIVITYLOG_GET, 11 | STORE_ACTIVITYLOG_GOT) 12 | 13 | 14 | class MessageLogStore(BaseStore): 15 | CREATE_SIGNAL = STORE_MESSAGELOG_CREATE 16 | CREATED_SIGNAL = STORE_MESSAGELOG_CREATED 17 | GET_SIGNAL = STORE_MESSAGELOG_GET 18 | GOT_SIGNAL = STORE_MESSAGELOG_GOT 19 | 20 | model = MessageLog 21 | 22 | 23 | class ActivityLogStore(BaseStore): 24 | CREATE_SIGNAL = STORE_ACTIVITYLOG_CREATE 25 | CREATED_SIGNAL = STORE_ACTIVITYLOG_CREATED 26 | GET_SIGNAL = STORE_ACTIVITYLOG_GET 27 | GOT_SIGNAL = STORE_ACTIVITYLOG_GOT 28 | 29 | model = ActivityLog 30 | -------------------------------------------------------------------------------- /ircb/storeclient/network.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ircb.models import Network 3 | from ircb.storeclient.base import BaseStore 4 | from ircb.lib.constants.signals import (STORE_NETWORK_CREATE, 5 | STORE_NETWORK_CREATED, 6 | STORE_NETWORK_GET, 7 | STORE_NETWORK_GOT, 8 | STORE_NETWORK_UPDATE, 9 | STORE_NETWORK_UPDATED) 10 | 11 | 12 | class NetworkStore(BaseStore): 13 | CREATE_SIGNAL = STORE_NETWORK_CREATE 14 | CREATED_SIGNAL = STORE_NETWORK_CREATED 15 | GET_SIGNAL = STORE_NETWORK_GET 16 | GOT_SIGNAL = STORE_NETWORK_GOT 17 | UPDATE_SIGNAL = STORE_NETWORK_UPDATE 18 | UPDATED_SIGNAL = STORE_NETWORK_UPDATED 19 | 20 | model = Network 21 | -------------------------------------------------------------------------------- /ircb/storeclient/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ircb.models import User 3 | from ircb.storeclient.base import BaseStore 4 | from ircb.lib.constants.signals import (STORE_USER_CREATE, 5 | STORE_USER_CREATED, 6 | STORE_USER_GET, 7 | STORE_USER_GOT) 8 | 9 | 10 | class UserStore(BaseStore): 11 | CREATE_SIGNAL = STORE_USER_CREATE 12 | CREATED_SIGNAL = STORE_USER_CREATED 13 | GET_SIGNAL = STORE_USER_GET 14 | GOT_SIGNAL = STORE_USER_GOT 15 | 16 | model = User 17 | -------------------------------------------------------------------------------- /ircb/stores/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | from ircb.models import create_tables 5 | 6 | from .network import NetworkStore 7 | from .client import ClientStore 8 | from .channel import ChannelStore 9 | from .user import UserStore 10 | from .logs import MessageLogStore, ActivityLogStore 11 | from .base import init 12 | 13 | 14 | def initialize(): 15 | init() 16 | create_tables() 17 | NetworkStore.initialize() 18 | ClientStore.initialize() 19 | ChannelStore.initialize() 20 | UserStore.initialize() 21 | MessageLogStore.initialize() 22 | ActivityLogStore.initialize() 23 | 24 | __all__ = [ 25 | 'ClientStore', 26 | 'NetworkStore', 27 | 'ChannelStore', 28 | 'UserStore', 29 | 'MessageLogStore', 30 | 'ActivityLogStore' 31 | ] 32 | -------------------------------------------------------------------------------- /ircb/stores/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | from ircb.lib.dispatcher import Dispatcher 5 | from ircb.models.lib import Base 6 | 7 | logger = logging.getLogger('stores') 8 | 9 | 10 | class BaseStore(object): 11 | GET_SIGNAL = None 12 | GOT_SIGNAL = None 13 | CREATE_SIGNAL = None 14 | CREATED_SIGNAL = None 15 | UPDATE_SIGNAL = None 16 | UPDATED_SIGNAL = None 17 | DELETE_SIGNAL = None 18 | DELETED_SIGNAL = None 19 | CREATE_OR_UPDATE = None 20 | CREATE_OR_UPDATE_ERROR = None 21 | 22 | @classmethod 23 | def initialize(cls): 24 | signal_callback_map = dict( 25 | GET_SIGNAL=cls.do_get, 26 | CREATE_SIGNAL=cls.do_create, 27 | UPDATE_SIGNAL=cls.do_update, 28 | DELETE_SIGNAL=cls.do_delete, 29 | CREATE_OR_UPDATE=cls.do_create_or_update 30 | ) 31 | for signal, callback in signal_callback_map.items(): 32 | if getattr(cls, signal, None): 33 | dispatcher.register(callback, getattr(cls, signal)) 34 | 35 | @classmethod 36 | def do_get(cls, signal, data, taskid=None): 37 | logger.debug( 38 | '{} GET: {} {} {}'.format( 39 | cls.__name__, signal, data, taskid) 40 | ) 41 | result = cls.get(**(data or {})) 42 | logger.debug( 43 | '{} GOT: {}'.format( 44 | cls.__name__, result) 45 | ) 46 | dispatcher.send( 47 | signal=cls.GOT_SIGNAL, 48 | data=cls.serialize(result), 49 | taskid=taskid) 50 | 51 | @classmethod 52 | def do_create(cls, signal, data, taskid=None): 53 | logger.debug( 54 | '{} CREATE: {} {} {}'.format( 55 | cls.__name__, signal, data, taskid) 56 | ) 57 | result = cls.create(**(data or {})) 58 | logger.debug( 59 | '{} CREATED: {}'.format( 60 | cls.__name__, result) 61 | ) 62 | dispatcher.send( 63 | signal=cls.CREATED_SIGNAL, 64 | data=cls.serialize(result), 65 | taskid=taskid) 66 | 67 | @classmethod 68 | def do_update(cls, signal, data, taskid=None): 69 | logger.debug( 70 | '{} UPDATE: {} {} {}'.format( 71 | cls.__name__, signal, data, taskid) 72 | ) 73 | result = cls.update(**(data or {})) 74 | logger.debug( 75 | '{} UPDATED: {}'.format( 76 | cls.__name__, result) 77 | ) 78 | dispatcher.send( 79 | signal=cls.UPDATED_SIGNAL, 80 | data=cls.serialize(result), 81 | taskid=taskid) 82 | 83 | @classmethod 84 | def do_delete(cls, signal, data, taskid=None): 85 | logger.debug( 86 | '{} DELETE: {} {} {}'.format( 87 | cls.__name__, signal, data, taskid) 88 | ) 89 | result = cls.delete(**(data or {})) 90 | logger.debug( 91 | '{} DELETED: {}'.format( 92 | cls.__name__, result) 93 | ) 94 | dispatcher.send( 95 | signal=cls.DELETED_SIGNAL, 96 | data=cls.serialize(result), 97 | taskid=taskid) 98 | 99 | @classmethod 100 | def do_create_or_update(cls, signal, data, taskid=None): 101 | logger.debug( 102 | '{} CREATE_OR_UPDATE: {} {} {}'.format( 103 | cls.__name__, signal, data, taskid) 104 | ) 105 | result, action = cls.create_or_update(**(data or {})) 106 | if action == 'create': 107 | action_verb = 'CREATED' 108 | resp_signal = cls.CREATED_SIGNAL 109 | elif action == 'update': 110 | action_verb = 'UPDATED' 111 | resp_signal = cls.UPDATED_SIGNAL 112 | else: 113 | action_verb = 'CREATE_OR_UPDATE_FAILED' 114 | resp_signal = cls.CREATE_OR_UPDATE_ERROR 115 | logger.debug( 116 | '{} {}: {}'.format( 117 | cls.__name__, action_verb, result) 118 | ) 119 | dispatcher.send( 120 | signal=resp_signal, 121 | data=cls.serialize(result), 122 | taskid=taskid) 123 | 124 | @classmethod 125 | def serialize(cls, data): 126 | if isinstance(data, Base): 127 | return cls.serialize_row(data) 128 | elif isinstance(data, list): 129 | return cls.serialize_rows(data) 130 | 131 | @classmethod 132 | def serialize_rows(cls, rows): 133 | return [cls.serialize_row(row) for row in rows] 134 | 135 | @classmethod 136 | def serialize_row(cls, row): 137 | return row.to_dict() 138 | 139 | @classmethod 140 | def get(cls, *args, **kwargs): 141 | raise NotImplementedError 142 | 143 | @classmethod 144 | def create(cls, *args, **kwargs): 145 | raise NotImplementedError 146 | 147 | @classmethod 148 | def update(cls, *args, **kwargs): 149 | raise NotImplementedError 150 | 151 | @classmethod 152 | def delete(cls, *args, **kwargs): 153 | raise NotImplementedError 154 | 155 | @classmethod 156 | def create_or_update(cls, *args, **kwargs): 157 | raise NotImplementedError 158 | 159 | 160 | def init(): 161 | global dispatcher 162 | dispatcher = Dispatcher(role='stores') 163 | 164 | dispatcher = None 165 | -------------------------------------------------------------------------------- /ircb/stores/channel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ircb.models import get_session, Network, Channel 3 | from ircb.lib.constants.signals import (STORE_CHANNEL_CREATE, 4 | STORE_CHANNEL_CREATED, 5 | STORE_CHANNEL_GET, 6 | STORE_CHANNEL_GOT, 7 | STORE_CHANNEL_UPDATE, 8 | STORE_CHANNEL_UPDATED, 9 | STORE_CHANNEL_DELETE, 10 | STORE_CHANNEL_DELETED, 11 | STORE_CHANNEL_CREATE_OR_UPDATE, 12 | STORE_CHANNEL_CREATE_OR_UPDATE_ERROR) 13 | from ircb.stores.base import BaseStore 14 | 15 | session = get_session() 16 | 17 | 18 | class ChannelStore(BaseStore): 19 | CREATE_SIGNAL = STORE_CHANNEL_CREATE 20 | CREATED_SIGNAL = STORE_CHANNEL_CREATED 21 | GET_SIGNAL = STORE_CHANNEL_GET 22 | GOT_SIGNAL = STORE_CHANNEL_GOT 23 | UPDATE_SIGNAL = STORE_CHANNEL_UPDATE 24 | UPDATED_SIGNAL = STORE_CHANNEL_UPDATED 25 | DELETE_SIGNAL = STORE_CHANNEL_DELETE 26 | DELETED_SIGNAL = STORE_CHANNEL_DELETED 27 | CREATE_OR_UPDATE = STORE_CHANNEL_CREATE_OR_UPDATE 28 | CREATE_OR_UPDATE_ERROR = STORE_CHANNEL_CREATE_OR_UPDATE_ERROR 29 | 30 | @classmethod 31 | def get(cls, filter=None, order_by=None, limit=None, sort=None): 32 | qs = session.query(Channel) 33 | if filter: 34 | for key, value in filter.items(): 35 | qs = qs.filter(getattr(Channel, key) == value) 36 | 37 | if order_by: 38 | for item in order_by: 39 | if item.startswith('-'): 40 | qs = qs.order_by(getattr(Channel, item[1:]).desc()) 41 | else: 42 | qs = qs.order_by(getattr(Channel, item)) 43 | 44 | if limit: 45 | qs = qs.limit(limit) 46 | 47 | if sort: 48 | qs = session.query(Channel).\ 49 | filter(Channel.id.in_(qs.subquery())).\ 50 | order_by(Channel.timestamp).all() 51 | 52 | return qs.all() 53 | 54 | @classmethod 55 | def create(cls, channel, network_id, password="", status=0): 56 | network = session.query(Network).get(network_id) 57 | channel = Channel(name=channel, network_id=network_id, 58 | password=password, status=status, 59 | user_id=network.user_id) 60 | session.add(channel) 61 | session.commit() 62 | 63 | @classmethod 64 | def update(cls, filter, update={}): 65 | channel = session.query(Channel).filter( 66 | getattr(Channel, filter[0]) == filter[1]).one() 67 | for key, value in update.items(): 68 | setattr(channel, key, value) 69 | session.add(channel) 70 | session.commit() 71 | return channel 72 | 73 | @classmethod 74 | def delete(cls, id): 75 | channel = session.query(Channel).get(id) 76 | session.delete(channel) 77 | session.commit() 78 | 79 | @classmethod 80 | def create_or_update(cls, channel, network_id, password=None, status=0): 81 | channel_obj = session.query(Channel).filter( 82 | Channel.network_id == network_id, Channel.name == channel).first() 83 | if channel_obj: 84 | if password is not None: 85 | channel_obj.password = password 86 | channel_obj.status = status 87 | action = 'update' 88 | else: 89 | network = session.query(Network).get(network_id) 90 | channel_obj = Channel(name=channel, network_id=network_id, 91 | user_id=network.user_id, 92 | password=password or '', 93 | status=status) 94 | action = 'create' 95 | session.add(channel_obj) 96 | session.commit() 97 | return channel_obj, action 98 | -------------------------------------------------------------------------------- /ircb/stores/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ircb.models import Client, Network, get_session 3 | from ircb.lib.constants.signals import (STORE_CLIENT_CREATE, 4 | STORE_CLIENT_CREATED, 5 | STORE_CLIENT_DELETE, 6 | STORE_CLIENT_DELETED) 7 | from ircb.stores.base import BaseStore 8 | 9 | session = get_session() 10 | 11 | 12 | class ClientStore(BaseStore): 13 | CREATE_SIGNAL = STORE_CLIENT_CREATE 14 | CREATED_SIGNAL = STORE_CLIENT_CREATED 15 | DELETE_SIGNAL = STORE_CLIENT_DELETE 16 | DELETED_SIGNAL = STORE_CLIENT_DELETED 17 | 18 | @classmethod 19 | def create(cls, socket, network_id, user_id): 20 | network = session.query(Network).filter( 21 | Network.id == network_id, Network.user_id == user_id).one() 22 | client = Client( 23 | socket=socket, network_id=network_id, user_id=user_id) 24 | session.add(client) 25 | session.commit() 26 | return client 27 | 28 | @classmethod 29 | def delete(cls, id): 30 | client = session.query(Client).get(id) 31 | session.delete(client) 32 | session.commit() 33 | -------------------------------------------------------------------------------- /ircb/stores/logs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from ircb.lib.constants.signals import (STORE_MESSAGELOG_CREATE, 4 | STORE_MESSAGELOG_CREATED, 5 | STORE_MESSAGELOG_GET, 6 | STORE_MESSAGELOG_GOT, 7 | STORE_ACTIVITYLOG_CREATE, 8 | STORE_ACTIVITYLOG_CREATED, 9 | STORE_ACTIVITYLOG_GET, 10 | STORE_ACTIVITYLOG_GOT) 11 | from ircb.models import get_session, MessageLog, ActivityLog 12 | from ircb.stores.base import BaseStore 13 | 14 | session = get_session() 15 | 16 | 17 | class MessageLogStore(BaseStore): 18 | GET_SIGNAL = STORE_MESSAGELOG_GET 19 | GOT_SIGNAL = STORE_MESSAGELOG_GOT 20 | CREATE_SIGNAL = STORE_MESSAGELOG_CREATE 21 | CREATED_SIGNAL = STORE_MESSAGELOG_CREATED 22 | 23 | @classmethod 24 | def create(cls, hostname, roomname, message, event, timestamp, 25 | mask, user_id, from_nickname, from_user_id=None): 26 | log = MessageLog( 27 | hostname=hostname, roomname=roomname, message=message, 28 | event=event, timestamp=datetime.datetime.fromtimestamp(timestamp), 29 | mask=mask, user_id=user_id, from_nickname=from_nickname, 30 | from_user_id=from_user_id) 31 | session.add(log) 32 | session.commit() 33 | return log 34 | 35 | @classmethod 36 | def get(cls, filter=None, order_by=None, limit=30, sort='timestamp'): 37 | qs = session.query(MessageLog.id) 38 | if filter: 39 | for key, value in filter.items(): 40 | qs = qs.filter(getattr(MessageLog, key) == value) 41 | 42 | if order_by: 43 | for item in order_by: 44 | if item.startswith('-'): 45 | qs = qs.order_by(getattr(MessageLog, item[1:]).desc()) 46 | else: 47 | qs = qs.order_by(getattr(MessageLog, item)) 48 | 49 | qs = qs.limit(limit) 50 | return session.query(MessageLog).\ 51 | filter(MessageLog.id.in_(qs.subquery())).\ 52 | order_by(MessageLog.timestamp).all() 53 | 54 | 55 | class ActivityLogStore(BaseStore): 56 | CREATE_SIGNAL = STORE_ACTIVITYLOG_CREATE 57 | CREATED_SIGNAL = STORE_ACTIVITYLOG_CREATED 58 | GET_SIGNAL = STORE_ACTIVITYLOG_GET 59 | GOT_SIGNAL = STORE_ACTIVITYLOG_GOT 60 | 61 | @classmethod 62 | def get(cls): 63 | pass 64 | 65 | @classmethod 66 | def create(cls, hostname, roomname, message, event, timestamp, 67 | mask, user_id): 68 | log = ActivityLog( 69 | hostname=hostname, roomname=roomname, message=message, 70 | event=event, timestamp=datetime.datetime.fromtimestamp(timestamp), 71 | mask=mask, user_id=user_id) 72 | session.add(log) 73 | session.commit() 74 | return log 75 | -------------------------------------------------------------------------------- /ircb/stores/network.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ircb.lib.constants.signals import (STORE_NETWORK_CREATE, 3 | STORE_NETWORK_CREATED, 4 | STORE_NETWORK_UPDATE, 5 | STORE_NETWORK_UPDATED, 6 | STORE_NETWORK_GET, 7 | STORE_NETWORK_GOT) 8 | from ircb.models import get_session, User, Network 9 | from ircb.stores.base import BaseStore 10 | 11 | session = get_session() 12 | 13 | 14 | class NetworkStore(BaseStore): 15 | CREATE_SIGNAL = STORE_NETWORK_CREATE 16 | CREATED_SIGNAL = STORE_NETWORK_CREATED 17 | GET_SIGNAL = STORE_NETWORK_GET 18 | GOT_SIGNAL = STORE_NETWORK_GOT 19 | UPDATE_SIGNAL = STORE_NETWORK_UPDATE 20 | UPDATED_SIGNAL = STORE_NETWORK_UPDATED 21 | 22 | @classmethod 23 | def get(cls, query): 24 | if isinstance(query, dict): 25 | qs = session.query(Network) 26 | for key, value in query.items(): 27 | qs = qs.filter(getattr(Network, key) == value) 28 | return qs.all() 29 | elif isinstance(query, tuple): 30 | key, value = query 31 | return session.query(Network).filter( 32 | getattr(Network, key) == value).one_or_none() 33 | else: 34 | return session.query(Network).get(query) 35 | 36 | @classmethod 37 | def create(cls, user_username, name, nickname, hostname, port, realname, 38 | network_username, password, usermode, ssl, ssl_verify): 39 | """ 40 | 41 | :param user_username: The username the network is associated with 42 | :param name: The name of the network to make 43 | :param nickname: The nickname to be used on the network 44 | :param hostname: The hostname of the network 45 | :param port: The port number 46 | :param realname: The User's real name for the network 47 | :param network_username: The username for the network 48 | :param password: The password for the network 49 | :param usermode: The user's usermode for the network 50 | :param ssl: 51 | :param ssl_verify: Bool determining is we check the ssl for the network 52 | :return: Network object 53 | """ 54 | user_obj = session.query(User).filter( 55 | User.username == user_username).first() 56 | if user_obj is None: 57 | raise Exception("User is none") 58 | network = Network( 59 | name=name, nickname=nickname, hostname=hostname, 60 | port=port, realname=realname, username=network_username, 61 | password=password, usermode=usermode, ssl=ssl, 62 | ssl_verify=ssl_verify, user_id=user_obj.id 63 | ) 64 | session.add(network) 65 | session.commit() 66 | return network 67 | 68 | @classmethod 69 | def update(cls, filter, update=None): 70 | if update is None: 71 | update = {} 72 | network = session.query(Network).filter( 73 | getattr(Network, filter[0]) == filter[1]).one() 74 | for key, value in update.items(): 75 | setattr(network, key, value) 76 | session.add(network) 77 | session.commit() 78 | return network 79 | -------------------------------------------------------------------------------- /ircb/stores/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ircb.lib.constants.signals import (STORE_USER_CREATE, 3 | STORE_USER_CREATED, 4 | STORE_USER_GET, 5 | STORE_USER_GOT) 6 | from ircb.models import get_session, User 7 | from ircb.stores.base import BaseStore 8 | 9 | session = get_session() 10 | 11 | 12 | class UserStore(BaseStore): 13 | CREATE_SIGNAL = STORE_USER_CREATE 14 | CREATED_SIGNAL = STORE_USER_CREATED 15 | GET_SIGNAL = STORE_USER_GET 16 | GOT_SIGNAL = STORE_USER_GOT 17 | 18 | @classmethod 19 | def get(cls, query): 20 | if isinstance(query, dict): 21 | qs = session.query(User) 22 | for key, value in query.items(): 23 | qs = qs.filter(getattr(User, key) == value) 24 | return qs.all() 25 | elif isinstance(query, tuple): 26 | key, value = query 27 | if key == 'auth': 28 | username, password = value 29 | user = session.query(User).filter( 30 | User.username == username).first() 31 | if user and user.authenticate(password): 32 | return user 33 | return None 34 | else: 35 | return session.query(User).filter( 36 | getattr(User, key) == value).first() 37 | else: 38 | return session.query(User).get(query) 39 | 40 | @classmethod 41 | def create(cls, username, email, password): 42 | user = User(username=username, email=email, password=password) 43 | session.add(user) 44 | session.commit() 45 | return user 46 | -------------------------------------------------------------------------------- /ircb/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waartaa/ircb/18381b167529ee56d1c5d5d22f857bb27692c397/ircb/utils/__init__.py -------------------------------------------------------------------------------- /ircb/utils/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging.config 3 | 4 | from ircb.config import settings 5 | 6 | 7 | def load_config(verbose=False, **kwargs): 8 | logging_conf = settings.LOGGING_CONF 9 | 10 | loglevel = 'INFO' 11 | if verbose: 12 | loglevel = 'DEBUG' 13 | 14 | logging_conf['level'] = loglevel 15 | for key, value in logging_conf['handlers'].items(): 16 | value['level'] = loglevel 17 | 18 | logging.config.dictConfig(settings.LOGGING_CONF) 19 | -------------------------------------------------------------------------------- /ircb/web/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waartaa/ircb/18381b167529ee56d1c5d5d22f857bb27692c397/ircb/web/__init__.py -------------------------------------------------------------------------------- /ircb/web/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import logging 4 | 5 | from aiohttp import web 6 | from aiohttp_auth import auth 7 | from aiohttp_session import get_session, session_middleware 8 | from aiohttp_session.cookie_storage import EncryptedCookieStorage 9 | 10 | from ircb.config import settings 11 | from ircb.web.user import SigninView, SignoutView, SignupView 12 | from ircb.web.network import NetworkListView, NetworkView 13 | from ircb.web.network import NetworkConnectionView 14 | from ircb.utils.config import load_config 15 | 16 | logger = logging.getLogger('aiohttp.access') 17 | 18 | 19 | @asyncio.coroutine 20 | def index(request): 21 | return web.Response(body=b"Hello, ircb!") 22 | 23 | 24 | @asyncio.coroutine 25 | def init(loop, host='0.0.0.0', port=10000): 26 | from ircb.storeclient import initialize 27 | initialize() 28 | load_config() 29 | policy = auth.SessionTktAuthentication( 30 | settings.WEB_SALT, 60, include_ip=True) 31 | middlewares = [ 32 | session_middleware(EncryptedCookieStorage(settings.WEB_SALT)), 33 | auth.auth_middleware(policy) 34 | ] 35 | 36 | app = web.Application(middlewares=middlewares) 37 | app.router.add_route('GET', '/', index) 38 | 39 | app.router.add_route('*', '/api/v1/signup', SignupView, name='signup') 40 | app.router.add_route('*', '/api/v1/signin', SigninView, name='signin') 41 | app.router.add_route('*', '/api/v1/signout', SignoutView, name='signout') 42 | app.router.add_route('*', '/api/v1/networks', NetworkListView, 43 | name='networks') 44 | app.router.add_route('*', '/api/v1/network/{id}', NetworkView, 45 | name='network') 46 | app.router.add_route('PUT', '/api/v1/network/{id}/{action}', 47 | NetworkConnectionView, 48 | name='network_connection') 49 | srv = yield from loop.create_server( 50 | app.make_handler(logger=logger, access_log=logger), host, port) 51 | return srv 52 | 53 | 54 | def createserver(loop, host='0.0.0.0', port=10000): 55 | logger.info('Listening on {host}:{port}'.format(host=host, port=port)) 56 | return loop.run_until_complete(init(loop, host, port)) 57 | 58 | 59 | def runserver(host='0.0.0.0', port=10000): 60 | loop = asyncio.get_event_loop() 61 | server = createserver(loop, host=host, port=port) 62 | try: 63 | loop.run_forever() 64 | except KeyboardInterrupt: 65 | pass 66 | loop.run_until_complete(server.wait_closed()) 67 | 68 | if __name__ == '__main__': 69 | runserver() 70 | -------------------------------------------------------------------------------- /ircb/web/decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from functools import wraps 3 | 4 | from aiohttp import web 5 | from aiohttp_auth.auth import get_auth 6 | 7 | 8 | def auth_required(func): 9 | """ 10 | Fix to make auth_required func with class based views. 11 | 12 | Utility decorator that checks if a user has been authenticated for this 13 | request. 14 | 15 | Allows views to be decorated like: 16 | 17 | @auth_required 18 | def view_func(request): 19 | pass 20 | 21 | providing a simple means to ensure that whoever is calling the function has 22 | the correct authentication details. 23 | 24 | Args: 25 | func: Function object being decorated and raises HTTPForbidden if not 26 | 27 | Returns: 28 | A function object that will raise web.HTTPForbidden() if the passed 29 | request does not have the correct permissions to access the view. 30 | """ 31 | @wraps(func) 32 | async def wrapper(*args): 33 | if isinstance(args[0], web.View): 34 | request = args[0].request 35 | else: 36 | request = args[-1] 37 | if (await get_auth(request)) is None: 38 | raise web.HTTPForbidden() 39 | 40 | return await func(*args) 41 | 42 | return wrapper 43 | -------------------------------------------------------------------------------- /ircb/web/lib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | 4 | from aiohttp import web 5 | 6 | 7 | class View(web.View): 8 | store = None 9 | fields = None 10 | excluded_fields = [] 11 | 12 | __fields = None 13 | 14 | def get_fields(self): 15 | if self.__fields is None: 16 | if self.store: 17 | fields = set(self.store.fields()) 18 | else: 19 | fields = set(self.fields or []) 20 | if self.fields is None: 21 | self.fields = fields 22 | else: 23 | fields = set(self.fields).difference(fields) 24 | fields = fields.difference(set(self.excluded_fields)) 25 | self.__fields = fields 26 | return self.__fields 27 | 28 | def serialize(self, data): 29 | if isinstance(data, list): 30 | result = [] 31 | for item in data: 32 | result.append(self._serialize_row(item)) 33 | else: 34 | result = self._serialize_row(data) 35 | return json.dumps(result) 36 | 37 | def _serialize_row(self, row): 38 | d = row.to_dict(serializable=True) 39 | result = {} 40 | fields = self.get_fields() 41 | for field in fields: 42 | result[field] = d[field] 43 | return result 44 | -------------------------------------------------------------------------------- /ircb/web/network.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import json 4 | 5 | from aiohttp import web, MultiDict 6 | from aiohttp_auth.auth import get_auth 7 | 8 | from ircb.web.decorators import auth_required 9 | from ircb.web.lib import View 10 | from ircb.storeclient import UserStore, NetworkStore 11 | from ircb.forms import NetworkForm 12 | 13 | 14 | class NetworkViewMixin(object): 15 | 16 | @asyncio.coroutine 17 | def _create_or_update(self, data, username, network=None): 18 | if network: 19 | network_data = network.to_dict() 20 | network_data.update(data) 21 | form = NetworkForm(formdata=MultiDict(network_data)) 22 | else: 23 | form = NetworkForm(formdata=MultiDict(data)) 24 | form.validate() 25 | if form.errors: 26 | return web.Response( 27 | body=json.dumps(form.errors).encode(), 28 | status=400, 29 | content_type='application/json') 30 | cleaned_data = form.data 31 | cleaned_data['user'] = username 32 | if network: 33 | network = yield from NetworkStore.update( 34 | dict( 35 | filter=('id', network.id), 36 | update=cleaned_data 37 | ) 38 | ) 39 | else: 40 | network = yield from NetworkStore.create(**cleaned_data) 41 | return web.Response(body=self.serialize(network).encode(), 42 | content_type='application/json') 43 | 44 | 45 | class NetworkListView(View, NetworkViewMixin): 46 | store = NetworkStore 47 | 48 | @auth_required 49 | @asyncio.coroutine 50 | def get(self): 51 | username = yield from get_auth(self.request) 52 | user = yield from UserStore.get( 53 | dict(query=('username', username))) 54 | networks = yield from NetworkStore.get( 55 | dict(query={'user_id': user.id})) or [] 56 | return web.Response(body=self.serialize(networks).encode(), 57 | content_type='application/json') 58 | 59 | @auth_required 60 | @asyncio.coroutine 61 | def post(self): 62 | username = yield from get_auth(self.request) 63 | data = yield from self.request.post() 64 | resp = yield from self._create_or_update(data, username) 65 | return resp 66 | 67 | 68 | class NetworkView(View, NetworkViewMixin): 69 | store = NetworkStore 70 | 71 | @auth_required 72 | @asyncio.coroutine 73 | def get(self): 74 | username = yield from get_auth(self.request) 75 | user = yield from UserStore.get( 76 | dict(query=('username', username))) 77 | network_id = self.request.match_info['id'] 78 | networks = yield from NetworkStore.get( 79 | dict(query={'user_id': user.id, 'id': int(network_id)})) 80 | network = (networks and networks[0]) or None 81 | if network is None: 82 | raise web.HTTPNotFound() 83 | return web.Response(body=self.serialize(network).encode(), 84 | content_type='application/json') 85 | 86 | @auth_required 87 | @asyncio.coroutine 88 | def put(self): 89 | network_id = self.request.match_info['id'] 90 | username = yield from get_auth(self.request) 91 | data = yield from self.request.post() 92 | user = yield from UserStore.get( 93 | dict(query=('username', username))) 94 | networks = yield from NetworkStore.get( 95 | dict(query={'user_id': user.id, 'id': network_id}) 96 | ) 97 | if not networks: 98 | raise web.HTTPNotFound() 99 | resp = yield from self._create_or_update(data, username, networks[0]) 100 | return resp 101 | 102 | 103 | class NetworkConnectionView(View): 104 | store = NetworkStore 105 | 106 | @auth_required 107 | @asyncio.coroutine 108 | def put(self): 109 | network_id = self.request.match_info['id'] 110 | action = self.request.match_info['action'] 111 | if action not in ('connect', 'disconnect'): 112 | raise web.HTTPNotFound() 113 | username = yield from get_auth(self.request) 114 | user = yield from UserStore.get( 115 | dict(query=('username', username))) 116 | networks = yield from NetworkStore.get( 117 | dict(query={'user_id': user.id, 'id': network_id}) 118 | ) 119 | if not networks: 120 | raise web.HTTPNotFound() 121 | network = networks[0] 122 | network = yield from NetworkStore.update( 123 | dict(filter=('id', network.id), 124 | update={'status': '0' if action == 'connect' else '2'}) 125 | ) 126 | return web.Response(body=self.serialize(network).encode(), 127 | content_type='application/json') 128 | -------------------------------------------------------------------------------- /ircb/web/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import json 4 | 5 | from aiohttp import web 6 | from aiohttp_auth import auth 7 | 8 | from ircb.storeclient import UserStore 9 | from ircb.web.decorators import auth_required 10 | from ircb.forms.user import UserForm 11 | 12 | 13 | class SignupView(web.View): 14 | 15 | @asyncio.coroutine 16 | def post(self): 17 | username = yield from auth.get_auth(self.request) 18 | if username: 19 | raise web.HTTPForbidden() 20 | data = yield from self.request.post() 21 | form = UserForm(formdata=data) 22 | form.validate() 23 | yield from self._validate_username(form) 24 | yield from self._validate_email(form) 25 | if form.errors: 26 | return web.Response(body=json.dumps(form.errors).encode(), 27 | status=400, 28 | content_type='application/json') 29 | cleaned_data = form.data 30 | yield from UserStore.create( 31 | dict( 32 | username=cleaned_data['username'], 33 | email=cleaned_data['email'], 34 | password=cleaned_data['password'], 35 | first_name=cleaned_data.get('first_name', ''), 36 | last_name=cleaned_data.get('last_name', '') 37 | ) 38 | ) 39 | return web.Response(body=b'OK') 40 | 41 | def _validate_username(self, form): 42 | username = form.username.data 43 | users = yield from UserStore.get( 44 | dict(query=('username', username))) 45 | if users: 46 | error_msg = 'Username already in use.' 47 | form.username.errors.append(error_msg) 48 | 49 | def _validate_email(self, form): 50 | email = form.email.data 51 | users = yield from UserStore.get( 52 | dict(query=('email', email))) 53 | if users: 54 | error_msg = 'Email already in use.' 55 | form.email.errors.append(error_msg) 56 | 57 | 58 | class SigninView(web.View): 59 | 60 | @asyncio.coroutine 61 | def post(self): 62 | data = yield from self.request.post() 63 | user = yield from UserStore.get( 64 | dict(query=('auth', (data.get('username'), data.get('password')))) 65 | ) 66 | if user: 67 | yield from auth.remember(self.request, user.username) 68 | return web.Response(body=b'OK') 69 | raise web.HTTPForbidden() 70 | 71 | 72 | class SignoutView(web.View): 73 | 74 | @auth_required 75 | @asyncio.coroutine 76 | def post(self): 77 | yield from auth.forget(self.request) 78 | return web.Response(body=b'OK') 79 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | Flask-SQLAlchemy==2.0 3 | Flask-User==0.6.19 4 | SQLAlchemy==1.0.13 5 | SQLAlchemy-Utils==0.30.15 6 | irc3==0.9.3 7 | alembic==0.8.3 8 | WTForms-Alchemy==0.15.0 9 | click==6.7 10 | aioredis==0.3.4 11 | tabulate==0.7.5 12 | aiohttp==0.20.2 13 | aiohttp-auth==0.1.1 14 | aiohttp-session[secure]==0.5.0 15 | aiozmq==0.7.1 16 | msgpack-python==0.4.7 17 | PyYAML==3.11 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | try: 5 | from setuptools import setup, find_packages 6 | except: 7 | from distutils.core import setup, find_packages 8 | 9 | with open('requirements.txt') as f: 10 | requirements = f.read().splitlines() 11 | 12 | setup( 13 | name='ircb', 14 | version='0.3.0', 15 | description='A IRC bouncer', 16 | long_description=''.join(open('README.md').readlines()), 17 | keywords='irc, client, bouncer', 18 | author='Ratnadeep Debnath', 19 | author_email='rtnpro@gmail.com', 20 | license='MIT', 21 | install_requires=requirements, 22 | packages=find_packages(), 23 | classifiers=[ 24 | 'Development Status :: 3 - Alpha', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Operating System :: POSIX :: Linux', 28 | 'Programming Language :: Python :: 3.5', 29 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', 30 | ], 31 | entry_points={ 32 | 'console_scripts': ['ircb=ircb.cli.main:cli'] 33 | } 34 | ) 35 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Unit Testing 2 | This folder is designed to hold all the testing used for ircb. There is a [tox](https://tox.readthedocs.org/en/latest/index.html) file in the root that drives all of the tests. this can be done by called by installing tox (pip install tox) and simply calling the command 'tox' from the project root. 3 | -------------------------------------------------------------------------------- /tests/connection_tests.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from ircb.connection import Connection 4 | 5 | class ConectionTests(TestCase): 6 | """ 7 | Test the connection.Connection class 8 | """ 9 | 10 | def test_decode(self): 11 | con_obj = Connection() 12 | self.assertEqual( 13 | "line", 14 | con_obj.decode("line".encode(encoding='UTF-8', errors='strict')) 15 | ) 16 | self.assertEqual( 17 | "line", 18 | con_obj.decode("line".encode(encoding='latin-1', errors='strict')) 19 | ) -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py34,py35,pep8 3 | 4 | [testenv] 5 | deps = 6 | -r{toxinidir}/requirements.txt 7 | nose 8 | commands = 9 | nosetests 10 | 11 | [testenv:pep8] 12 | deps = pep8 13 | commands = pep8 ircb 14 | 15 | [testenv:flake8] 16 | basepython=python 17 | deps=flake8 18 | commands=flake8 ircb 19 | 20 | [testenv:coverage] 21 | deps= -r{toxinidir}/requirements.txt 22 | nose 23 | coverage 24 | commands = coverage erase 25 | coverage run {envbindir}/nosetests 26 | coverage report --include=*dexml* --omit=*test* 27 | 28 | [tox:travis] 29 | 3.4 = py34, pep8 30 | 3.5 = py35 31 | 32 | --------------------------------------------------------------------------------