├── README.md ├── doc ├── SCRIPTING.MD ├── psyrcd.png └── psyrcd_banner.png ├── pluginbase.py ├── plugins ├── foo.py └── httpd.py ├── psyrcd ├── psyrcd.conf ├── psyrcd.py ├── psyrcd.service ├── requirements.txt ├── scripts ├── ChanServ.py ├── NickServ.py ├── capabilities.py ├── disect.py ├── news.py ├── proctitle.py ├── replace.py └── sortition.py └── setup.py /README.md: -------------------------------------------------------------------------------- 1 | ![Alt text](doc/psyrcd_banner.png?raw=true "Would probably also make for a dope MUD.") 2 | 3 | A full IRCD in 60 seconds or triple your money back: 4 |
 5 | git clone https://github.com/LukeB42/psyrcd && cd psyrcd
 6 | sudo python setup.py install
 7 | psyrcd -f
 8 | 
9 | ### The Psybernetics IRC Server. 10 | 11 | Psyrcd is a pure-python IRCD that supports scriptable commands, user modes and 12 | channel modes, the behavior of which can be redefined while in use. 13 | 14 | A NickServ and ChanServ are included as scripts. 15 | 16 | **Note:** Psyrcd is noticably faster with [uvloop](https://github.com/MagicStack/uvloop) installed. 17 | 18 | ![Alt text](doc/psyrcd.png?raw=true "OK now throw NLTK in the mix") 19 | 20 | Tested with Python 3.5 on Linux 2.6 to 3.14. 21 | Check the commit history for Python 2.x versions. 22 | 23 | 24 | -------------------------------------------------------------------------------- /doc/SCRIPTING.MD: -------------------------------------------------------------------------------- 1 | 2 | #Scripting 3 | 4 | ## Design rationale 5 | 6 | The ideal scripting API might rely on functions that have specific names. This hasn't been adopted yet in Psyrcd because the current design puts less pressure on memory resources. 7 | 8 | It is very likely that this style of scripting will be adopted in future as it will allow run-time metaprogramming at the expense of more __code__ objects. 9 | 10 | ## How to 11 | | Command | Help | 12 | | ------------- |-------------| 13 | | /operserv scripts | Lists all loaded scripts. Indicates file modifications if `--debug` isn't being used. | 14 | | /operserv scripts list | Lists all available scripts. | 15 | | /operserv load scriptname | Loads the specified file as a code object using a specific namespace, where a variable called `init` is set to `True`. | 16 | | /operserv scripts unload scriptname | Unloads the specified file by executing its code object with `init` set to `False`. This indicates that file handles in the cache must be closed and structures on affected objects ought to be removed. | 17 | 18 | Possible namespaces look like the following: 19 | 20 | `namespace = {'client':self,['channel':channel],['mode':mode/'params':params],['setting_mode':bool,'args':args/'display':True],['line':line,'func':func]}` 21 | 22 | Modes can be any number of characters long. Modes are entries in a dictionary, called channel.modes and user.mdoes. Mode arguments are stored in lists by default. 23 | 24 | The structure on a channel or user object looks like `user.modes['scriptmode']`, where 'scriptmode' points to a list or whatever structure your script manually sets. 25 | 26 | The type used to store arguments can be overridden and the way values are appended and removed can be handled from within scripts. 27 | 28 | Mode parameters can be stored in Numpy arrays for example. If you have a mode called numpy, you could do something like: `/mode #channel +numpy:123,456,789,0` 29 | 30 | `/mode #channel -numpy:` would clear the mode completely, rather than removing individual parameters and then the mode itself. 31 | 32 | init and unload ought to cause the script to create or remove structures on channels and clients. 33 | 34 | Modes on load are automatically appended to the necessary supported_modes dictionary and removed on unload. 35 | 36 | Mode scripts can check for the presence of a variable named `display` in their namespace in order to return custom messages in a variable named `output`. 37 | 38 | `@scripts` decorator cycles through modes and match to `server.scripts.umodes.keys()` and `server.scripts.cmodes.keys()`. 39 | 40 | Every time a channel name is the target of a command its modes are checked against IRCServer.Scripts.cmodes. 41 | 42 | Decorator on `handle_*` will send `self,channel,func,params` into your scripts default namespace. 43 | 44 | For example: `/mode #channel +lang:en` 45 | 46 | `channel.modes{'l':['50'],'lang':['en'],'n':1,'t':1}` 47 | 48 | 49 | --- 50 | 51 | #The Future 52 | `/operserv connect server:port key`; generate key at runtime. This is not implemented yet and would result in a major version increment. 53 | 54 | Connect and negotiate as a server, hand connection off to dedicated class. 55 | 56 | Most IRCDs keep their modules local and their behaviors are generally not mutually exclusive. 57 | 58 | Determine the most elegant way of performing breadth-first search with as little stateful info as possible 59 | 60 | decorate `.broadcast()` so it transmits messages across server links. Recipients parse joins/parts/quits 61 | 62 | #Known Errors 63 | Windows doesn't have `fork()`. Run in the foreground or Cygwin. 64 | 65 | #Todo 66 | 67 | Issue a warning and raise SystemExit if psyrcd is already running. 68 | 69 | Implement /userhost and /who. 70 | 71 | Implement all user and channel modes. 72 | 73 | Fix TODO comments. 74 | 75 | -------------------------------------------------------------------------------- /doc/psyrcd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LukeB42/psyrcd/42c659d8429ef8d7b39f58c6d4193f1909e1a9ec/doc/psyrcd.png -------------------------------------------------------------------------------- /doc/psyrcd_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LukeB42/psyrcd/42c659d8429ef8d7b39f58c6d4193f1909e1a9ec/doc/psyrcd_banner.png -------------------------------------------------------------------------------- /pluginbase.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pluginbase 4 | ~~~~~~~~~~ 5 | 6 | Pluginbase is a module for Python that provides a system for building 7 | plugin based applications. 8 | 9 | :copyright: (c) Copyright 2014 by Armin Ronacher. 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | import os 13 | import sys 14 | import uuid 15 | import errno 16 | import pkgutil 17 | import hashlib 18 | import threading 19 | 20 | from types import ModuleType 21 | from weakref import ref as weakref 22 | 23 | 24 | PY2 = sys.version_info[0] == 2 25 | if PY2: 26 | text_type = str 27 | string_types = (str, str) 28 | from io import StringIO as NativeBytesIO 29 | else: 30 | text_type = str 31 | string_types = (str,) 32 | from io import BytesIO as NativeBytesIO 33 | 34 | 35 | __version__ = '0.4' 36 | _local = threading.local() 37 | 38 | _internalspace = ModuleType(__name__ + '._internalspace') 39 | _internalspace.__path__ = [] 40 | sys.modules[_internalspace.__name__] = _internalspace 41 | 42 | 43 | def get_plugin_source(module=None, stacklevel=None): 44 | """Returns the :class:`PluginSource` for the current module or the given 45 | module. The module can be provided by name (in which case an import 46 | will be attempted) or as a module object. 47 | 48 | If no plugin source can be discovered, the return value from this method 49 | is `None`. 50 | 51 | This function can be very useful if additional data has been attached 52 | to the plugin source. For instance this could allow plugins to get 53 | access to a back reference to the application that created them. 54 | 55 | :param module: optionally the module to locate the plugin source of. 56 | :param stacklevel: defines how many levels up the module should search 57 | for before it discovers the plugin frame. The 58 | default is 0. This can be useful for writing wrappers 59 | around this function. 60 | """ 61 | if module is None: 62 | frm = sys._getframe((stacklevel or 0) + 1) 63 | name = frm.f_globals['__name__'] 64 | glob = frm.f_globals 65 | elif isinstance(module, string_types): 66 | frm = sys._getframe(1) 67 | name = module 68 | glob = __import__(module, frm.f_globals, 69 | frm.f_locals, ['__dict__']).__dict__ 70 | else: 71 | name = module.__name__ 72 | glob = module.__dict__ 73 | return _discover_space(name, glob) 74 | 75 | 76 | def _discover_space(name, globals): 77 | try: 78 | return _local.space_stack[-1] 79 | except (AttributeError, IndexError): 80 | pass 81 | 82 | if '__pluginbase_state__' in globals: 83 | return globals['__pluginbase_state__'].source 84 | 85 | mod_name = globals.get('__name__') 86 | if mod_name is not None and \ 87 | mod_name.startswith(_internalspace.__name__ + '.'): 88 | end = mod_name.find('.', len(_internalspace.__name__) + 1) 89 | space = sys.modules.get(mod_name[:end]) 90 | if space is not None: 91 | return space.__pluginbase_state__.source 92 | 93 | 94 | def _shutdown_module(mod): 95 | members = list(mod.__dict__.items()) 96 | for key, value in members: 97 | if key[:1] != '_': 98 | setattr(mod, key, None) 99 | for key, value in members: 100 | setattr(mod, key, None) 101 | 102 | 103 | def _to_bytes(s): 104 | if isinstance(s, text_type): 105 | return s.encode('utf-8') 106 | return s 107 | 108 | 109 | class _IntentionallyEmptyModule(ModuleType): 110 | 111 | def __getattr__(self, name): 112 | try: 113 | return ModuleType.__getattr__(self, name) 114 | except AttributeError: 115 | if name[:2] == '__': 116 | raise 117 | raise RuntimeError( 118 | 'Attempted to import from a plugin base module (%s) without ' 119 | 'having a plugin source activated. To solve this error ' 120 | 'you have to move the import into a "with" block of the ' 121 | 'associated plugin source.' % self.__name__) 122 | 123 | 124 | class _PluginSourceModule(ModuleType): 125 | 126 | def __init__(self, source): 127 | modname = '%s.%s' % (_internalspace.__name__, source.spaceid) 128 | ModuleType.__init__(self, modname) 129 | self.__pluginbase_state__ = PluginBaseState(source) 130 | 131 | @property 132 | def __path__(self): 133 | try: 134 | ps = self.__pluginbase_state__.source 135 | except AttributeError: 136 | return [] 137 | return ps.searchpath + ps.base.searchpath 138 | 139 | 140 | def _setup_base_package(module_name): 141 | try: 142 | mod = __import__(module_name, None, None, ['__name__']) 143 | except ImportError: 144 | mod = None 145 | if '.' in module_name: 146 | parent_mod = __import__(module_name.rsplit('.', 1)[0], 147 | None, None, ['__name__']) 148 | else: 149 | parent_mod = None 150 | 151 | if mod is None: 152 | mod = _IntentionallyEmptyModule(module_name) 153 | if parent_mod is not None: 154 | setattr(parent_mod, module_name.rsplit('.', 1)[-1], mod) 155 | sys.modules[module_name] = mod 156 | 157 | 158 | class PluginBase(object): 159 | """The plugin base acts as a control object around a dummy Python 160 | package that acts as a container for plugins. Usually each 161 | application creates exactly one base object for all plugins. 162 | 163 | :param package: the name of the package that acts as the plugin base. 164 | Usually this module does not exist. Unless you know 165 | what you are doing you should not create this module 166 | on the file system. 167 | :param searchpath: optionally a shared search path for modules that 168 | will be used by all plugin sources registered. 169 | """ 170 | 171 | def __init__(self, package, searchpath=None): 172 | #: the name of the dummy package. 173 | self.package = package 174 | if searchpath is None: 175 | searchpath = [] 176 | #: the default search path shared by all plugins as list. 177 | self.searchpath = searchpath 178 | _setup_base_package(package) 179 | 180 | def make_plugin_source(self, *args, **kwargs): 181 | """Creats a plugin source for this plugin base and returns it. 182 | All parameters are forwarded to :class:`PluginSource`. 183 | """ 184 | return PluginSource(self, *args, **kwargs) 185 | 186 | 187 | class PluginSource(object): 188 | """The plugin source is what ultimately decides where plugins are 189 | loaded from. Plugin bases can have multiple plugin sources which act 190 | as isolation layer. While this is not a security system it generally 191 | is not possible for plugins from different sources to accidentally 192 | cross talk. 193 | 194 | Once a plugin source has been created it can be used in a ``with`` 195 | statement to change the behavior of the ``import`` statement in the 196 | block to define which source to load the plugins from:: 197 | 198 | plugin_source = plugin_base.make_plugin_source( 199 | searchpath=['./path/to/plugins', './path/to/more/plugins']) 200 | 201 | with plugin_source: 202 | from myapplication.plugins import my_plugin 203 | 204 | :param base: the base this plugin source belongs to. 205 | :param identifier: optionally a stable identifier. If it's not defined 206 | a random identifier is picked. It's useful to set this 207 | to a stable value to have consistent tracebacks 208 | between restarts and to support pickle. 209 | :param searchpath: a list of paths where plugins are looked for. 210 | :param persist: optionally this can be set to `True` and the plugins 211 | will not be cleaned up when the plugin source gets 212 | garbage collected. 213 | """ 214 | # Set these here to false by default so that a completely failing 215 | # constructor does not fuck up the destructor. 216 | persist = False 217 | mod = None 218 | 219 | def __init__(self, base, identifier=None, searchpath=None, 220 | persist=False): 221 | #: indicates if this plugin source persists or not. 222 | self.persist = persist 223 | if identifier is None: 224 | identifier = str(uuid.uuid4()) 225 | #: the identifier for this source. 226 | self.identifier = identifier 227 | #: A reference to the plugin base that created this source. 228 | self.base = base 229 | #: a list of paths where plugins are searched in. 230 | self.searchpath = searchpath 231 | #: The internal module name of the plugin source as it appears 232 | #: in the :mod:`pluginsource._internalspace`. 233 | self.spaceid = '_sp' + hashlib.md5( 234 | _to_bytes(self.base.package) + b'|' + 235 | _to_bytes(identifier), 236 | ).hexdigest() 237 | #: a reference to the module on the internal 238 | #: :mod:`pluginsource._internalspace`. 239 | self.mod = _PluginSourceModule(self) 240 | 241 | if hasattr(_internalspace, self.spaceid): 242 | raise RuntimeError('This plugin source already exists.') 243 | sys.modules[self.mod.__name__] = self.mod 244 | setattr(_internalspace, self.spaceid, self.mod) 245 | 246 | def __del__(self): 247 | if not self.persist: 248 | self.cleanup() 249 | 250 | def list_plugins(self): 251 | """Returns a sorted list of all plugins that are available in this 252 | plugin source. This can be useful to automatically discover plugins 253 | that are available and is usually used together with 254 | :meth:`load_plugin`. 255 | """ 256 | rv = [] 257 | for _, modname, ispkg in pkgutil.iter_modules(self.mod.__path__): 258 | rv.append(modname) 259 | return sorted(rv) 260 | 261 | def load_plugin(self, name): 262 | """This automatically loads a plugin by the given name from the 263 | current source and returns the module. This is a convenient 264 | alternative to the import statement and saves you from invoking 265 | ``__import__`` or a similar function yourself. 266 | 267 | :param name: the name of the plugin to load. 268 | """ 269 | if '.' in name: 270 | raise ImportError('Plugin names cannot contain dots.') 271 | with self: 272 | return __import__(self.base.package + '.' + name, 273 | globals(), {}, ['__name__']) 274 | 275 | def open_resource(self, plugin, filename): 276 | """This function locates a resource inside the plugin and returns 277 | a byte stream to the contents of it. If the resource cannot be 278 | loaded an :exc:`IOError` will be raised. Only plugins that are 279 | real Python packages can contain resources. Plain old Python 280 | modules do not allow this for obvious reasons. 281 | 282 | .. versionadded:: 0.3 283 | 284 | :param plugin: the name of the plugin to open the resource of. 285 | :param filename: the name of the file within the plugin to open. 286 | """ 287 | mod = self.load_plugin(plugin) 288 | fn = getattr(mod, '__file__', None) 289 | if fn is not None: 290 | if fn.endswith(('.pyc', '.pyo')): 291 | fn = fn[:-1] 292 | if os.path.isfile(fn): 293 | return open(os.path.join(os.path.dirname(fn), filename), 'rb') 294 | buf = pkgutil.get_data(self.mod.__name__ + '.' + plugin, filename) 295 | if buf is None: 296 | raise IOError(errno.ENOENT, 'Could not find resource') 297 | return NativeBytesIO(buf) 298 | 299 | def cleanup(self): 300 | """Cleans up all loaded plugins manually. This is necessary to 301 | call only if :attr:`persist` is enabled. Otherwise this happens 302 | automatically when the source gets garbage collected. 303 | """ 304 | self.__cleanup() 305 | 306 | def __cleanup(self, _sys=sys, _shutdown_module=_shutdown_module): 307 | # The default parameters are necessary because this can be fired 308 | # from the destructor and so late when the interpreter shuts down 309 | # that these functions and modules might be gone. 310 | if self.mod is None: 311 | return 312 | modname = self.mod.__name__ 313 | self.mod.__pluginbase_state__ = None 314 | self.mod = None 315 | try: 316 | delattr(_internalspace, self.spaceid) 317 | except AttributeError: 318 | pass 319 | prefix = modname + '.' 320 | # avoid the bug described in issue #6 321 | if modname in _sys.modules: 322 | del _sys.modules[modname] 323 | for key, value in list(_sys.modules.items()): 324 | if not key.startswith(prefix): 325 | continue 326 | mod = _sys.modules.pop(key, None) 327 | if mod is None: 328 | continue 329 | _shutdown_module(mod) 330 | 331 | def __assert_not_cleaned_up(self): 332 | if self.mod is None: 333 | raise RuntimeError('The plugin source was already cleaned up.') 334 | 335 | def __enter__(self): 336 | self.__assert_not_cleaned_up() 337 | _local.__dict__.setdefault('space_stack', []).append(self) 338 | return self 339 | 340 | def __exit__(self, exc_type, exc_value, tb): 341 | try: 342 | _local.space_stack.pop() 343 | except (AttributeError, IndexError): 344 | pass 345 | 346 | def _rewrite_module_path(self, modname): 347 | self.__assert_not_cleaned_up() 348 | if modname == self.base.package: 349 | return self.mod.__name__ 350 | elif modname.startswith(self.base.package + '.'): 351 | pieces = modname.split('.') 352 | return self.mod.__name__ + '.' + '.'.join( 353 | pieces[self.base.package.count('.') + 1:]) 354 | 355 | 356 | class PluginBaseState(object): 357 | __slots__ = ('_source',) 358 | 359 | def __init__(self, source): 360 | if source.persist: 361 | self._source = lambda: source 362 | else: 363 | self._source = weakref(source) 364 | 365 | @property 366 | def source(self): 367 | rv = self._source() 368 | if rv is None: 369 | raise AttributeError('Plugin source went away') 370 | return rv 371 | 372 | 373 | class _ImportHook(ModuleType): 374 | 375 | def __init__(self, name, system_import): 376 | ModuleType.__init__(self, name) 377 | self._system_import = system_import 378 | self.enabled = True 379 | 380 | def enable(self): 381 | """Enables the import hook which drives the plugin base system. 382 | This is the default. 383 | """ 384 | self.enabled = True 385 | 386 | def disable(self): 387 | """Disables the import hook and restores the default import system 388 | behavior. This effectively breaks pluginbase but can be useful 389 | for testing purposes. 390 | """ 391 | self.enabled = False 392 | 393 | def plugin_import(self, name, globals=None, locals=None, 394 | fromlist=None, level=None): 395 | if level is None: 396 | # set the level to the default value specific to this python version 397 | level = -1 if PY2 else 0 398 | import_name = name 399 | if self.enabled: 400 | ref_globals = globals 401 | if ref_globals is None: 402 | ref_globals = sys._getframe(1).f_globals 403 | space = _discover_space(name, ref_globals) 404 | if space is not None: 405 | actual_name = space._rewrite_module_path(name) 406 | if actual_name is not None: 407 | import_name = actual_name 408 | 409 | return self._system_import(import_name, globals, locals, 410 | fromlist, level) 411 | 412 | 413 | try: 414 | import builtins as builtins 415 | except ImportError: 416 | import builtins 417 | import_hook = _ImportHook(__name__ + '.import_hook', builtins.__import__) 418 | builtins.__import__ = import_hook.plugin_import 419 | sys.modules[import_hook.__name__] = import_hook 420 | del builtins 421 | -------------------------------------------------------------------------------- /plugins/foo.py: -------------------------------------------------------------------------------- 1 | __package__ = [{"name": "foo", "type": "command", "description": "Test plugin."}] 2 | 3 | def foo(ctx): 4 | print(dir()) 5 | print(ctx) 6 | return "42" 7 | return str(ctx) 8 | 9 | def __init__(ctx): 10 | """ 11 | Mainly for modifying the server instance. 12 | 13 | 14 | 15 | Possible to define __package__ earlier than this point, enclose some 16 | variables in a callable and then modify __package__. 17 | """ 18 | __package__[0]["callable"] = foo 19 | 20 | def __del__(ctx): 21 | print(ctx) 22 | 23 | 24 | # Note that by this point, after __init__ has been invoked, __package__ is equal to the following: 25 | # __package__ = [{"name": "foo", "type": "command", "description": "Test plugin.", "callable": foo}] 26 | -------------------------------------------------------------------------------- /plugins/httpd.py: -------------------------------------------------------------------------------- 1 | import uvloop 2 | import asyncio 3 | from flask import Flask, Response 4 | 5 | from tornado.wsgi import WSGIContainer 6 | from tornado.web import Application, FallbackHandler 7 | from tornado.platform.asyncio import AsyncIOMainLoop 8 | 9 | ircd = None 10 | 11 | app = Flask("httpd") 12 | @app.route("/") 13 | def index(): 14 | global ircd 15 | response = Response(mimetype="text/html") 16 | response.data = "I have %i clients." % len(ircd.clients) 17 | 18 | return response 19 | 20 | def httpd(ctx): 21 | return "Running." 22 | 23 | def __init__(ctx): 24 | global ircd 25 | ircd = ctx.server 26 | container = WSGIContainer(app) 27 | application = Application([ 28 | (".*", FallbackHandler, {"fallback": container}) 29 | ]) 30 | 31 | AsyncIOMainLoop().install() 32 | application.listen(5000) 33 | 34 | def __del__(ctx): 35 | ... 36 | 37 | __package__ = { 38 | "name": "httpd", 39 | "type": "command", 40 | "description": "An example HTTPD.", 41 | "callable": "httpd" 42 | } 43 | -------------------------------------------------------------------------------- /psyrcd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # /etc/init.d/psyrcd 3 | # 4 | # Sys-V init script for psyrcd, the Psybernetics IRC server. 5 | # Luke Brooks (luke@psybernetics.org.uk) 6 | # 7 | # You generally want to fill out the username below 8 | # and probably place this psyrcd directory in /srv. 9 | # Make sure your log directory is owned by your $USER. 10 | 11 | BASEDIR=/srv/psyrcd 12 | 13 | USER= 14 | SCRIPTS=$BASEDIR/scripts/ 15 | PIDFILE=$BASEDIR/pid 16 | LOGFILE=/var/log/psyrcd/ircd.log 17 | 18 | # Carry out specific functions when asked to by the system 19 | case "$1" in 20 | start) 21 | echo "Starting psyrcd" 22 | echo changeme|psyrcd --run-as=$USER --logfile=$LOGFILE --pidfile=$PIDFILE --scripts-dir=$SCRIPTS --preload 23 | ;; 24 | stop) 25 | echo "Stopping psyrcd" 26 | psyrcd --stop --pidfile=$PIDFILE 27 | ;; 28 | *) 29 | echo "Usage: /etc/init.d/psyrcd {start|stop}" 30 | exit 1 31 | ;; 32 | esac 33 | 34 | exit 0 35 | 36 | -------------------------------------------------------------------------------- /psyrcd.conf: -------------------------------------------------------------------------------- 1 | server { 2 | name = "psyrcd-dev" 3 | domain = "irc.psybernetics.org" 4 | description = "I fought the lol, and. The lol won." 5 | welcome = "Welcome to {}" // Formatted with server["name"]. 6 | link_key = "${PSYRCD_LINK_KEY}" // Populated from the environment. 7 | ping_frequency = 60 8 | 9 | max { 10 | clients = 8192 11 | idle_time = 120 12 | nicklen = 12 13 | channels = 200 14 | topiclen = 512 15 | } 16 | } 17 | 18 | oper { 19 | /* Set the password to true to generate a random password, false to disable 20 | * the oper system, a string of your choice or pipe at runtime: 21 | * $ openssl rand -base64 32 | psyrcd --preload -f 22 | */ 23 | username = true 24 | password = true 25 | } 26 | 27 | services { 28 | nickserv { 29 | enabled = false 30 | database_uri = "sqlite:///var/opt/psyrcd/nickserv.db" 31 | } 32 | chanserv { 33 | enabled = false 34 | database_uri = "sqlite:////var/opt/psyrcd/chanserv.db" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /psyrcd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Psyrcd The Psybernetics IRC Server 3 | 4 | [Service] 5 | ExecStart=/usr/bin/psyrcd 6 | 7 | [Install] 8 | WantedBy=multi-user.target 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyhcl 2 | uvloop 3 | -------------------------------------------------------------------------------- /scripts/ChanServ.py: -------------------------------------------------------------------------------- 1 | # ChanServ.py for Psyrcd. 2 | # Many many thanks to the contributors of Anope. 3 | # Implements /chanserv and channel mode R. 4 | # MIT License 5 | 6 | # Schema: channel | password | description | owner | operators | bans | topic | topic_by | topic_time | time_reg | time_use | 7 | # successor | url | email | entrymsg | mlock | keeptopic | peace | restricted | secureops | signkick | topiclock | modes | protected 8 | # Colour key: 9 | # \x02 bold 10 | # \x03 coloured text 11 | # \x1D italic text 12 | # \x0F colour reset 13 | # \x16 reverse colour 14 | # \x1F underlined text 15 | 16 | import re 17 | import time 18 | import hashlib 19 | import datetime 20 | 21 | log = cache['config']['logging'] 22 | TABLE = "chanserv" 23 | DB_FILE = "./services.db" 24 | MAX_OPS = False 25 | CS_IDENT = "ChanServ!services@" + cache['config']['SRV_DOMAIN'] 26 | NS_TABLE = "nickserv" 27 | MAX_RECORDS = 5000 28 | MAX_CHANNELS = 25 29 | MAX_DAYS_UNUSED = 62 30 | 31 | 32 | class Channel(object): 33 | """ 34 | A dictionary-like object for channel records. 35 | """ 36 | def __init__(self, channel): 37 | self.channel = channel 38 | self.db = cache['db'] 39 | self.c = self.db.cursor() 40 | self.c.execute("SELECT * FROM %s WHERE channel=?" % \ 41 | TABLE, (self.channel,)) 42 | self.r = self.c.fetchone() 43 | if not self.r: 44 | self.channel = '' 45 | 46 | def __getitem__(self, key): 47 | self.c.execute("SELECT * FROM %s WHERE channel=?" % \ 48 | TABLE, (self.channel,)) 49 | self.r = self.c.fetchone() 50 | if self.r and key in self.r.keys(): 51 | if key in ['operators', 'modes', 'bans', 'protected']: 52 | if ':' in self.r[key]: 53 | return(dict([x.split(':') for x in self.r[key].split(',')])) 54 | else: return dict() 55 | else: return(self.r[key]) 56 | else: raise CSError("Invalid key") 57 | 58 | def __setitem__(self, key, value): 59 | if self.r: 60 | if ':' in key: 61 | k,v = key.split(':') 62 | o = self[k] 63 | if type(o) == dict: 64 | if MAX_OPS and (k == 'operators'): 65 | if v not in o and len(o) >= MAX_OPS: return() 66 | o[v]=value 67 | v=str(o)\ 68 | .replace('{','')\ 69 | .replace('}','')\ 70 | .replace("u'",'')\ 71 | .replace(' ','')\ 72 | .replace("'",'') 73 | self.c.execute("UPDATE %s SET %s=? WHERE channel=?" % \ 74 | (TABLE, k), (v, self.channel)) 75 | self.db.commit() 76 | elif key in self.r.keys(): 77 | self.c.execute("UPDATE %s SET %s=? WHERE channel=?" % \ 78 | (TABLE,key), (value, self.channel)) 79 | self.db.commit() 80 | else: raise CSError("Invalid key") 81 | else: raise CSError("Invalid channel") 82 | 83 | def __delitem__(self, key): 84 | if ':' in key: 85 | k,v = key.split(':') 86 | o = self[k] 87 | if type(o) == dict: 88 | if v in o: 89 | del o[v] 90 | v=str(o)\ 91 | .replace('{','')\ 92 | .replace('}','')\ 93 | .replace("u'",'')\ 94 | .replace(' ','')\ 95 | .replace("'",'') 96 | self.c.execute("UPDATE %s SET %s=? WHERE channel=?" % \ 97 | (TABLE,k), (v,self.channel)) 98 | self.db.commit() 99 | elif key == 'channel': self.c.execute("DELETE FROM %s WHERE channel=?" % TABLE, (self.channel,)) 100 | else: self[key] = '' 101 | 102 | def keys(self): 103 | if self.r: return(self.r.keys()) 104 | else: return([]) 105 | 106 | def __repr__(self): 107 | if self.r: return("" % \ 108 | (self.channel,hex(id(self)))) 109 | else: return("" % hex(id(self))) 110 | 111 | def re_to_irc(r, displaying=True): 112 | if not displaying: 113 | r = re.sub('\.','\\\.',r) 114 | r = re.sub('\*','.*',r) 115 | else: 116 | r = re.sub('\\\.','.',r) 117 | r = re.sub('\.\*','*',r) 118 | return(r) 119 | 120 | def op_cmp(user,target): 121 | if user != 'q' and target == 'q': return false 122 | elif (user != 'a' and user != 'q') and (target == 'a' or target == 'q'): 123 | return False 124 | elif (user != 'o' and user != 'a' and user != 'q') \ 125 | and (target == 'o' or target == 'a' or target == 'q'): 126 | return False 127 | else: 128 | return True 129 | 130 | def is_op(nick, channel): 131 | if 'h' in channel.modes and nick in channel.modes['h']: 132 | return(True) 133 | elif 'o' in channel.modes and nick in channel.modes['o']: 134 | return(True) 135 | elif 'a' in channel.modes and nick in channel.modes['a']: 136 | return(True) 137 | elif 'q' in channel.modes and nick in channel.modes['q']: 138 | return(True) 139 | else: 140 | return(False) 141 | 142 | def secureops(channel): 143 | for user in channel.clients: 144 | if not 'R' in user.modes or (user.nick not in ops and user.nick != c['owner']): 145 | if 'q' in channel.modes and user.nick in channel.modes['q']: csmode(channel,'-q',user.nick) 146 | if 'a' in channel.modes and user.nick in channel.modes['a']: csmode(channel,'-a',user.nick) 147 | if 'o' in channel.modes and user.nick in channel.modes['o']: csmode(channel,'-o',user.nick) 148 | if 'h' in channel.modes and user.nick in channel.modes['h']: csmode(channel,'-h',user.nick) 149 | if 'v' in channel.modes and user.nick in channel.modes['v']: csmode(channel,'-v',user.nick) 150 | csmsg("Enforced SecureOps.") 151 | 152 | def restrict(channel): 153 | for user in channel.clients.copy(): 154 | if not 'R' in user.modes or (user.nick not in ops and user.nick != c['owner']): 155 | for op_list in channel.ops: 156 | if user.nick in op_list: op_list.remove(user.nick) 157 | client.broadcast(channel.name, ':%s KICK %s %s :RESTRICTED' % \ 158 | (CS_IDENT, channel.name, user.nick)) 159 | user.channels.pop(channel.name) 160 | channel.clients.remove(user) 161 | csmsg("Enforced RESTRICTED.") 162 | 163 | def init_channel(client, channel): 164 | """ 165 | Handle a channel being initialised, or a client joining a registered one. 166 | """ 167 | c = Channel(channel.name) 168 | if c.r: 169 | ops = c['operators'] 170 | protected = c['protected'] 171 | 172 | # Succession/Expiration 173 | db = cache['db'] 174 | cur = db.cursor() 175 | cur.execute("SELECT * FROM %s WHERE nick=?" % NS_TABLE, (c['owner'],)) 176 | r = cur.fetchone() 177 | if not r: 178 | if c['successor']: 179 | cur.execute("SELECT * FROM %s WHERE nick=?" % \ 180 | NS_TABLE, (c['successor'],)) 181 | s = cur.fetchone() 182 | if s: 183 | c['owner'] = c['successor'] 184 | c['successor'] = '' 185 | del s 186 | else: 187 | del c['channel'] 188 | return(None) 189 | else: 190 | del c['channel'] 191 | return(None) 192 | elif not 'R' in channel.modes: csmode(channel,'+R') 193 | 194 | # Bans 195 | if not client.oper and (client.nick != c['owner'] or \ 196 | ('R' not in client.modes and client.nick == c['owner'])) and \ 197 | (client.nick not in protected or ('R' not in client.modes and client.nick in protected)): 198 | bans = c['bans'] 199 | for b in bans.keys(): 200 | if re.match(b, client.client_ident(True)): 201 | return(':%s NOTICE %s :Cannot join %s. (Banned)' % (CS_IDENT,client.nick,channel.name)) 202 | 203 | # Restricted 204 | if not client.oper and c['restricted']: 205 | if not 'R' in client.modes or (client.nick != c['owner'] \ 206 | and client.nick not in ops and client.nick not in protected): 207 | return(':%s NOTICE %s :Cannot join %s. (Restricted)' % (CS_IDENT,client.nick,channel.name)) 208 | 209 | # Topic/KeepTopic 210 | if c['topic'] and c['keeptopic'] and not len(channel.clients) \ 211 | and channel.topic != c['topic']: 212 | channel.topic = c['topic'] 213 | channel.topic_by = c['topic_by'] 214 | channel.topic_time = c['topic_time'] 215 | 216 | # Entrymsg 217 | if c['entrymsg']: csmsg("[%s] %s" % (channel.name, c['entrymsg'])) 218 | 219 | # MLock 220 | if c['mlock']: 221 | for mode, settings in c['modes'].items(): 222 | if ',' in settings: settings = settings.split(',') 223 | csmode(channel,mode,settings) 224 | 225 | # Operators 226 | if 'o' in channel.supported_modes and client.nick in channel.modes['o']: 227 | channel.modes['o'].remove(client.nick) 228 | if 'R' in client.modes and (client.nick == c['owner'] or client.nick == c['successor']): 229 | c['time_use'] = time.time() 230 | csmode(channel,'+q',client.nick) 231 | if 'R' in client.modes and client.nick in ops and(client.nick != c['owner'] \ 232 | and client.nick != c['successor']): 233 | c['time_use'] = time.time() 234 | csmode(channel,'+'+ops[client.nick],client.nick) 235 | del db,cur,r 236 | elif 'R' in channel.modes: csmode(channel, '-R') 237 | del c 238 | return(None) 239 | 240 | def escape(query): return query.replace("'","") 241 | 242 | def csmsg(msg): 243 | client.broadcast(client.nick, ":%s NOTICE %s :%s" % \ 244 | (CS_IDENT,client.nick,msg)) 245 | 246 | def csmode(channel, mode, args=None): 247 | if type(mode) == str or type(mode) == unicode: mode=[mode] 248 | for x in mode: 249 | f = x[0] 250 | m = x[1:] 251 | if type(channel) == str: 252 | channel = client.server.channels.get(channel) 253 | if channel and m in channel.supported_modes: 254 | if f == '+' and not m in channel.modes: channel.modes[m]=[] 255 | if f == '+' and not args in channel.modes[m]: 256 | if type(args) == list: channel.modes[m].extend(args) 257 | elif args: channel.modes[m].append(args) 258 | elif f == '-': 259 | if args and args in channel.modes[m]: channel.modes[m].remove(args) 260 | else: del channel.modes[m] 261 | if not args: client.broadcast(channel.name, ':%s MODE %s %s' % (CS_IDENT,channel.name,f+m)) 262 | else: client.broadcast(channel.name, ':%s MODE %s %s %s' % (CS_IDENT,channel.name,f+m,args)) 263 | 264 | def fmt_timestamp(ts): return datetime.datetime.fromtimestamp(int(ts)).strftime('%b %d %H:%M:%S %Y') 265 | 266 | def csmsg_list(t): 267 | for r in t: 268 | if client.oper: ip = " Owner: %s," % r['owner'] 269 | else: ip = '' 270 | chan = client.server.channels.get(r['channel']) 271 | if chan: 272 | if 'R' in chan.modes: csmsg("\x02\x033%s\x0F:%s Description: %s, Registered: %s" % \ 273 | (r['channel'], ip, r['description'], fmt_timestamp(r['time_reg']))) 274 | else: csmsg("\x02\x032%s\x0F:%s Description: %s, Registered: %s" % \ 275 | (r['channel'], ip, r['description'], fmt_timestamp(r['time_reg']))) 276 | else: csmsg("\x02%s\x0F:%s Description: %s, Registered: %s" % \ 277 | (r['channel'], ip, r['description'], fmt_timestamp(r['time_reg']))) 278 | csmsg("End of \x02LIST\x0F command.") 279 | 280 | def is_expired(seconds): 281 | t = time.time() 282 | seconds = t - seconds 283 | minutes, seconds = divmod(seconds, 60) 284 | hours, minutes = divmod(minutes, 60) 285 | days, hours = divmod(hours, 24) 286 | weeks, days = divmod(days, 7) 287 | if MAX_DAYS_UNUSED >= days+(weeks*7): 288 | return False 289 | else: 290 | return True 291 | 292 | class CSError(Exception): 293 | def __init__(self, value): self.value = value # causes error messages to be 294 | def __str__(self): return(repr(self.value)) # dispersed to umode:W users 295 | 296 | if 'init' in dir(): 297 | provides=['command:chanserv,cs:Channel registration service.', 'cmode:R:Registered channel.'] 298 | if init: 299 | # You generally want to have your imports here and then put them on the 300 | # cache so they're not recomputed for every sentence said in an associated channel 301 | # or command executed by a similar user, just because you're using the --debug flag. 302 | 303 | # Reader beware: sqlite3 is only being used in keeping with the ethos "only the stdlib". 304 | # Feel free to implement /your/ modules with SQLAlchemy, Dataset, PyMongo, PyTables.. SciKit.. NLTK.. 305 | if not 'db' in cache: 306 | import sqlite3 307 | db = sqlite3.connect(DB_FILE, check_same_thread=False) 308 | db.row_factory = sqlite3.Row 309 | cache['db'] = db 310 | db.execute("CREATE TABLE IF NOT EXISTS %s (channel, password, description, \ 311 | owner, operators, bans, topic, topic_by, topic_time, time_reg REAL, time_use REAL, \ 312 | successor, url, email, entrymsg, mlock, keeptopic, peace, restricted, secureops, signkick, \ 313 | topiclock, modes, protected)" % TABLE) 314 | db.commit() 315 | else: 316 | if 'db' in cache: 317 | cache['db'].close() 318 | del cache['db'] 319 | 320 | if 'new' in dir() and 'channel' in dir(): 321 | cancel = init_channel(client,channel) 322 | if not cancel: del cancel 323 | 324 | # The following happens when the server detects 325 | # that a channel carrying our mode is doing something. 326 | # Here we can determine what the client is doing, and then 327 | # modify the client, the server, and/or command parameters. 328 | if 'func' in dir(): 329 | c = Channel(channel.name) 330 | if c.r: 331 | 332 | if func.__name__ == 'handle_join': 333 | cancel = init_channel(client,channel) 334 | if not cancel: del cancel 335 | 336 | elif func.__name__ == 'handle_topic': 337 | if client.oper or is_op(client.nick, channel): 338 | if c['topiclock']: 339 | cancel = ':%s NOTICE %s :Topic locked for %s.' % \ 340 | (CS_IDENT,client.nick,channel.name) 341 | elif ':' in params and is_op(client.nick, channel): 342 | c['topic'] = params.split(':')[1] 343 | c['topic_by'] = client.nick 344 | c['topic_time'] = str(time.time())[:10] 345 | 346 | elif func.__name__ == 'handle_kick': 347 | if params.split()[1] == c['owner']: 348 | params = params.split() 349 | user = client.server.clients.get(params[1]) 350 | if user and 'R' in user.modes and user in channel.clients: 351 | params[1:] = '_' 352 | csmsg("Cannot kick channel Founder.") 353 | params = ' '.join(params) 354 | elif c['peace']: 355 | if not client.oper and (client.nick != c['owner'] or 'R' not in client.modes): 356 | cancel = ':%s NOTICE %s :Cannot use KICK in \x02%s\x0F. (Peace)' % \ 357 | (CS_IDENT,client.nick,channel.name) 358 | elif params.split()[1] in c['protected']: 359 | user = client.server.clients.get(params.split()[1]) 360 | if user and 'R' in user.modes: 361 | cancel = ':%s NOTICE %s :Cannot kicked protected user \x02%s\x0F from \x02%s\x0F.' % \ 362 | (CS_IDENT,client.nick,user.nick,channel.name) 363 | 364 | elif func.__name__ == 'handle_mode': 365 | # Mode Lock 366 | if c['mlock'] and not client.oper and (client.nick != c['owner'] \ 367 | or (client.nick == c['owner'] and 'R' not in client.modes)): 368 | cancel = ':%s NOTICE %s :Modes are locked for \x02%s\x0F.' % \ 369 | (CS_IDENT,client.nick,channel.name) 370 | 371 | # SecureOps / Peace 372 | elif not client.oper and (c['secureops'] or c['peace'] or c['protected']) \ 373 | and is_op(client.nick, channel): 374 | target='' 375 | mode = params.split(' ',1)[1] 376 | if ' ' in mode: mode,target = mode.split(' ',1) 377 | if mode[1:] in ['v','h','o','a','q']: 378 | target = client.server.clients.get(target) 379 | ops = c['operators'] 380 | if c['secureops']: 381 | if target and target.nick not in ops: 382 | cancel = ':%s NOTICE %s :\x02%s\x0F is not in any of the access lists for \x02%s\x0F. (SecureOps)' % \ 383 | (CS_IDENT,client.nick,target.nick,channel.name) 384 | elif target and not 'R' in target.modes: 385 | cancel = ':%s NOTICE %s :\x02%s\x0F is not identified with services. (SecureOps)' % \ 386 | (CS_IDENT,client.nick,target.nick) 387 | elif (c['peace'] and mode[0] == '-') and ('R' not in client.modes or client.nick != c['owner']): 388 | cancel = ':%s NOTICE %s :Cannot revoke privileges on \x02%s\x0F. (Peace)' % \ 389 | (CS_IDENT,client.nick,channel.name) 390 | elif mode[0] == '-' and target and target.nick in c['protected'] and 'R' in target.modes: 391 | cancel = ':%s NOTICE %s :Cannot revoke privileges on \x02%s\x0F from protected user \x02%s\x0F.' % \ 392 | (CS_IDENT,client.nick,channel.name,target.nick) 393 | 394 | elif 'R' in channel.modes: csmode(channel.name,'-R') 395 | del c 396 | 397 | # This namespace indicates a client is retrieving 398 | # the list of modes in a channel where one of our 399 | # cmodes is in use. 400 | if 'display' in dir() and 'channel' in dir(): 401 | output = '(Registered.)' 402 | 403 | if 'command' in dir(): 404 | client.last_activity = str(time.time())[:10] 405 | params = escape(params) 406 | cmd=params 407 | args='' 408 | if ' ' in params: 409 | cmd,args = params.split(' ',1) 410 | cmd,args=(cmd.lower(),args.lower()) 411 | if cmd == 'help' or not cmd: 412 | if not args: 413 | csmsg("\x02/CHANSERV\x0F allows you to register and control various aspects of") 414 | csmsg("channels. ChanServ can often prevent malicious users from \"taking") 415 | csmsg("over\" channels by limiting who is allowed channel operator") 416 | csmsg("privileges. Available commands are listed below; to use them, type") 417 | csmsg("\x02/CHANSERV \x1Fcommand\x0F. For more information on a specific command,") 418 | csmsg("type \x02/CHANSERV HELP \x1Fcommand\x0F.") 419 | csmsg("") 420 | csmsg(" REGISTER Register a channel") 421 | csmsg(" SET Set channel options and information") 422 | csmsg(" SOP Modify the list of SOP users") 423 | csmsg(" AOP Modify the list of AOP users") 424 | csmsg(" HOP Maintains the HOP (HalfOP) list for a channel") 425 | csmsg(" VOP Maintains the VOP (VOiced People) list for a channel") 426 | csmsg(" DROP Cancel the registration of a channel") 427 | csmsg(" BAN Bans a selected host on a channel") 428 | csmsg(" UNBAN Remove ban on a selected host from a channel") 429 | csmsg(" CLEAR Tells ChanServ to clear certain settings on a channel") 430 | csmsg(" OWNER Gives you owner status on channel") 431 | csmsg(" DEOWNER Removes your owner status on a channel") 432 | csmsg(" PROTECT Protects a selected nick on a channel") 433 | csmsg(" DEPROTECT Deprotects a selected nick on a channel") 434 | csmsg(" OP Gives Op status to a selected nick on a channel") 435 | csmsg(" DEOP Deops a selected nick on a channel") 436 | csmsg(" HALFOP Halfops a selected nick on a channel") 437 | csmsg(" DEHALFOP Dehalfops a selected nick on a channel") 438 | csmsg(" VOICE Voices a selected nick on a channel") 439 | csmsg(" DEVOICE Devoices a selected nick on a channel") 440 | csmsg(" INVITE Tells ChanServ to invite you into a channel") 441 | csmsg(" KICK Kicks a selected nick from a channel") 442 | csmsg(" LIST Lists all registered channels matching a given pattern") 443 | csmsg(" LOGOUT This command will logout the selected nickname") 444 | csmsg(" TOPIC Manipulate the topic of the specified channel") 445 | csmsg(" INFO Lists information about the named registered channel") 446 | csmsg(" APPENDTOPIC Add text to a channels topic") 447 | csmsg(" ENFORCE Enforce various channel modes and set options") 448 | csmsg("") 449 | csmsg("Note that any channel which is not used for %i days" % MAX_DAYS_UNUSED) 450 | csmsg("(i.e. which no user on the channel's access list enters") 451 | csmsg("for that period of time) will be automatically dropped.") 452 | 453 | elif args == 'register': 454 | csmsg("Syntax: \x02REGISTER \x1Fchannel\x0F \x02\x1Fpassword\x0F \x02\x1Fdescription\x0F") 455 | csmsg("") 456 | csmsg("Registers a channel in the ChanServ database. In order") 457 | csmsg("to use this command, you must first be a channel operator") 458 | csmsg("on the channel you're trying to register. The password") 459 | csmsg("is used with the \x02IDENTIFY\x0F command to allow others to") 460 | csmsg("make changes to the channel settings at a later time.") 461 | csmsg("The last parameter, which \x02must\x0F be included, is a") 462 | csmsg("general description of the channel's purpose.") 463 | csmsg("") 464 | csmsg("When you register a channel, you are recorded as the") 465 | csmsg("\"founder\" of the channel. The channel founder is allowed") 466 | csmsg("to change all of the channel settings for the channel;") 467 | csmsg("ChanServ will also automatically give the founder") 468 | csmsg("channel-operator privileges when s/he enters the channel.") 469 | csmsg("See the \x02ACCESS\x0F command (\x02/ChanServ HELP ACCESS\x0F) for") 470 | csmsg("information on giving a subset of these privileges to") 471 | csmsg("other channel users.") 472 | csmsg("") 473 | csmsg("NOTICE: In order to register a channel, you must have") 474 | csmsg("first registered your nickname. If you haven't,") 475 | csmsg("use \x02/NickServ HELP\x0F for information on how to do so.") 476 | csmsg("") 477 | csmsg("Note that any channel which is not used for %i days" % MAX_DAYS_UNUSED) 478 | csmsg("(i.e. which no user on the channel's access list enters") 479 | csmsg("for that period of time) will be automatically dropped.") 480 | 481 | elif args == 'set': 482 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02\x1Foption\x0F \x02\x1Fparameters\x0F") 483 | csmsg("") 484 | csmsg("Allows the channel founder to set various channel options") 485 | csmsg("and other information.") 486 | csmsg("") 487 | csmsg("Available options:") 488 | csmsg("") 489 | csmsg(" FOUNDER Set the founder of a channel") 490 | csmsg(" SUCCESSOR Set the successor for a channel") 491 | csmsg(" PASSWORD Set the founder password") 492 | csmsg(" DESC Set the channel description") 493 | csmsg(" URL Associate a URL with the channel") 494 | csmsg(" EMAIL Associate an E-mail address with the channel") 495 | csmsg(" ENTRYMSG Set a message to be sent to users when they") 496 | csmsg(" enter the channel") 497 | csmsg(" MLOCK Lock channel modes on or off") 498 | csmsg(" KEEPTOPIC Retain topic when channel is not in use") 499 | csmsg(" PEACE Regulate the use of critical commands") 500 | csmsg(" RESTRICTED Restrict access to the channel") 501 | csmsg(" SECUREOPS Stricter control of chanop status") 502 | csmsg(" SIGNKICK Sign kicks that are done with KICK command") 503 | csmsg(" TOPICLOCK Topic can only be changed with TOPIC") 504 | csmsg("") 505 | csmsg("Type \x02/CHANSERV HELP SET \x1Foption\x0F for more information on a") 506 | csmsg("particular option.") 507 | 508 | if args == 'set founder': 509 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02FOUNDER \x1Fnick\x0F") 510 | csmsg("") 511 | csmsg("Changes the founder of a channel. The new nickname must") 512 | csmsg("be a registered one.") 513 | 514 | elif args == 'set successor': 515 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02SUCCESSOR \x1Fnick\x0F") 516 | csmsg("") 517 | csmsg("Changes the successor of a channel. If the founders'") 518 | csmsg("nickname nickname expires or is dropped while the channel is still") 519 | csmsg("registered, the successor will become the new founder of the") 520 | csmsg("channel. However, if the successor already has too many") 521 | csmsg("channels registered (%i), the channel will be dropped" % MAX_CHANNELS) 522 | csmsg("instead, just as if no successor had been set. The new") 523 | csmsg("nickname must be a registered one.") 524 | 525 | elif args == 'set password': 526 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02PASSWORD \x1Fpassword\x0F") 527 | csmsg("") 528 | csmsg("Sets the password used to drop the channel.") 529 | 530 | elif args == 'set desc': 531 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02DESC \x1Fdescription\x0F") 532 | csmsg("") 533 | csmsg("Sets the description of the channel, which shows up with") 534 | csmsg("The \x02LIST\x0F and \x02INFO\x0F commands.") 535 | 536 | elif args == 'set url': 537 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02URL \x1Furl\x0F") 538 | csmsg("") 539 | csmsg("Associates the given URL with the channel. This URL will") 540 | csmsg("be displayed whenever someone requests information on the") 541 | csmsg("channel with the \x02INFO\x0F command.") 542 | 543 | elif args == 'set email': 544 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02EMAIL \x1Femail\x0F") 545 | csmsg("") 546 | csmsg("Associates the given E-Mail address with the channel.") 547 | csmsg("This address will be displayed whenever an IRC Operator") 548 | csmsg("requests information on the channel with the \x02INFO\x0F") 549 | csmsg("command. This can help IRC Operators issue new passwords.") 550 | 551 | elif args == 'set entrymsg': 552 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02ENTRYMSG \x1Fmessage\x0F") 553 | csmsg("") 554 | csmsg("Sets the message which will be sent via /notice to users") 555 | csmsg("when they enter the channel. If \x02message\x0F is \"\x02off\x0F\" then no") 556 | csmsg("message will be shown.") 557 | 558 | elif args == 'set mlock': 559 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02MLOCK {ON|OFF}\x0F") 560 | csmsg("") 561 | csmsg("Sets the mode-lock parameter for the channel. ChanServ") 562 | csmsg("allows you to lock active channel modes to a channel,") 563 | csmsg("even across channel instances. Modes involving sophisticated") 564 | csmsg("parameters (non-list, string, integer or floating point") 565 | csmsg("values) cannot be locked.") 566 | 567 | elif args == 'set keeptopic': 568 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02KEEPTOPIC {ON|OFF}\x0F") 569 | csmsg("") 570 | csmsg("Enables or disables \x02topic retention\x0F for a channel.") 571 | csmsg("When \x02topic retention\x0F is set, the topic for the channel") 572 | csmsg("will be remembered by ChanServ even after the last user") 573 | csmsg("leaves the channel, and will be restored the next time") 574 | csmsg("the channel is created.") 575 | 576 | elif args == 'set peace': 577 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02PEACE {ON|OFF}\x0F") 578 | csmsg("") 579 | csmsg("When \x02peace\x0F is set, a user won't be able to kick, ban") 580 | csmsg("or remove channel status from another user.") 581 | 582 | elif args == 'set restricted': 583 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02RESTRICTED {ON|OFF}\x0F") 584 | csmsg("") 585 | csmsg("Enables or disables the \x02restricted access\x0F option for a") 586 | csmsg("channel. When \x02restricted access\x0F is set, users not on") 587 | csmsg("the access list will instead be denied entry to the channel.") 588 | 589 | elif args == 'set secureops': 590 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02SECUREOPS {ON|OFF}\x0F") 591 | csmsg("") 592 | csmsg("When \x02secure ops\x0F is set, users who are not on the userlist") 593 | csmsg("will not be allowed chanop status.") 594 | 595 | elif args == 'set signkick': 596 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02SIGNKICK {ON|OFF}\x0F") 597 | csmsg("") 598 | csmsg("Enables or disables signed kicks for a channel.") 599 | csmsg("When \x02SIGNKICK\x0F is set, kicks issued with the") 600 | csmsg("ChanServ \x02KICK\x0F command will have the nick that used the") 601 | csmsg("command in their reason.") 602 | 603 | elif args == 'set topiclock': 604 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02TOPICLOCK {ON|OFF}\x0F") 605 | csmsg("") 606 | csmsg("Enables or disables the \x02topic lock\x0F for a channel.") 607 | csmsg("When \x02topic lock\x0F is set, ChanServ will not allow the") 608 | csmsg("channel topic to be changed except by the \x02TOPIC\x0F") 609 | csmsg("command.") 610 | 611 | elif args == 'drop': 612 | csmsg("Syntax \x02DROP \x1Fchannel\x0F \x02\x1Fpassword\x0F") 613 | csmsg("") 614 | csmsg("Unregisters the named channel ") 615 | if client.oper: 616 | csmsg("IRC Operators may supply anything as a password.") 617 | 618 | elif args == 'enforce': 619 | csmsg("Syntax: \x02ENFORCE \x1Fchannel\x0F \x02\x1Fwhat\x0F") 620 | csmsg("") 621 | csmsg("Enforce various channel modes and options. The \x1Fchannel\x0F") 622 | csmsg("option indicates what channel to enforce the modes and options") 623 | csmsg("on. The \x1Fwhat\x0F option indicates what modes and options to") 624 | csmsg("enforce, and can be any of SET, SECUREOPS, RESTRICTED or MODES.") 625 | csmsg("") 626 | csmsg("If \x1Fwhat\x0F is SET, it will enforce SECUREOPS and RESTRICTED") 627 | csmsg("on the users currently in the channel, if they are set. Give") 628 | csmsg("SECUEROPS to enforce the SECUREOPS option, even if it is not") 629 | csmsg("enabled. Use RESTRICTED to enforce the RESTRICTED option, also") 630 | csmsg("if it is not enabled.") 631 | csmsg("") 632 | csmsg("If \x1Fwhat\x0F is MODES, it will enforce any stored modes") 633 | csmsg("associated with the channel.") 634 | csmsg("") 635 | csmsg("Limited to channel Founders and IRC Operators.") 636 | 637 | elif args == 'ban': 638 | csmsg("Syntax: \x02BAN \x1Fchannel\x0F \x02\x1Fmask\x0F") 639 | csmsg("") 640 | csmsg("Bans a selected mask on a channel. Limited to AOPs") 641 | csmsg("and above, channel owners and IRC Operators.") 642 | 643 | elif args == 'unban': 644 | csmsg("Syntax: \x02UNBAN \x1Fchannel\x0F \x02\x1Fmask\x0F") 645 | csmsg("") 646 | csmsg("Unbans a selected mask from a channel. Limited to AOPs") 647 | csmsg("and above, channel owners and IRC Operators.") 648 | 649 | elif args == 'sop': 650 | csmsg("Syntax: \x02SOP \x1Fchannel\x0F \x02ADD \x1Fnick\x0F") 651 | csmsg(" \x02SOP \x1Fchannel\x0F \x02DEL \x1Fnick\x0F") 652 | csmsg(" \x02SOP \x1Fchannel\x0F \x02LIST\x0F") 653 | csmsg(" \x02SOP \x1Fchannel\x0F \x02CLEAR\x0F") 654 | csmsg("") 655 | csmsg("Maintains the \x02SOP\x0F (SUperOp) \x02list\x0F for a channel.") 656 | csmsg("") 657 | csmsg("The \x02SOP ADD\x0F command adds the given nickname to the") 658 | csmsg("SOP list.") 659 | csmsg("") 660 | csmsg("The \x02SOP DEL\x0F command removes the given nick from the") 661 | csmsg("SOP list. If a list of entry numbers is given, those") 662 | csmsg("entries are deleted. (See the example for LIST below.)") 663 | csmsg("") 664 | csmsg("The \x02SOP LIST\x0F command displays the SOP list.") 665 | csmsg("") 666 | csmsg("The \x02SOP CLEAR\x0F command clears all entries of the") 667 | csmsg("SOP list.") 668 | csmsg("") 669 | csmsg("The \x02SOP ADD\x0F, \x02SOP DEL\x0F, \x02SOP LIST\x0F and \x02SOP CLEAR\x0F commands are") 670 | csmsg("limited to the channel founder.") 671 | 672 | elif args == 'aop': 673 | csmsg("Syntax: \x02AOP \x1Fchannel\x0F \x02ADD \x1Fnick\x0F") 674 | csmsg(" \x02AOP \x1Fchannel\x0F \x02DEL \x1Fnick\x0F") 675 | csmsg(" \x02AOP \x1Fchannel\x0F \x02LIST\x0F") 676 | csmsg(" \x02AOP \x1Fchannel\x0F \x02CLEAR\x0F") 677 | csmsg("") 678 | csmsg("Maintains the \x02AOP\x0F (AutoOp) \x02list\x0F for a channel. The AOP") 679 | csmsg("list gives users the right to be auto-opped on you channel,") 680 | csmsg("to unban or invite themselves if needed, to have their") 681 | csmsg("greet message showed on join, and so on.") 682 | csmsg("") 683 | csmsg("The \x02AOP ADD\x0F command adds the given nicknamet o the") 684 | csmsg("AOP list.") 685 | csmsg("") 686 | csmsg("The \x02AOP DEL\x0F commmand removes the given nick from the") 687 | csmsg("AOP list. If list of entry numbers is given, those") 688 | csmsg("entries are deleted. (See the example for LIST below.)") 689 | csmsg("") 690 | csmsg("The \x02AOP LIST\x0F command displays the AOP list.") 691 | csmsg("") 692 | csmsg("The \x02AOP CLEAR\x0F command clears all entries of the") 693 | csmsg("AOP list.") 694 | csmsg("") 695 | csmsg("The \x02AOP ADD\x0F and \x02AOP DEL\x0F commands are limited to") 696 | csmsg("SOP or above, while the \x02AOP CLEAR\x0F command can only") 697 | csmsg("be used bu the channel founder. However, any use on the") 698 | csmsg("AOP list may use the \x02AOP LIST\x0F command.") 699 | 700 | elif args == 'hop': 701 | csmsg("Syntax: \x02HOP \x1Fchannel\x0F \x02ADD \x1Fnick\x0F") 702 | csmsg(" \x02HOP \x1Fchannel\x0F \x02DEL \x1Fnick\x0F") 703 | csmsg(" \x02HOP \x1Fchannel\x0F \x02LIST\x0F") 704 | csmsg(" \x02HOP \x1Fchannel\x0F \x02CLEAR\x0F") 705 | csmsg("") 706 | csmsg("Maintains the \x02HOP\x0F (HalfOp) \x02list\x0F for a channel. The HOP") 707 | csmsg("list gives users the right to be auto-halfopped on your") 708 | csmsg("channel.") 709 | csmsg("") 710 | csmsg("The \x02HOP ADD\x0F command adds the given nickname to the") 711 | csmsg("HOP list.") 712 | csmsg("") 713 | csmsg("The \x02HOP DEL\x0F command removes the given nick from the") 714 | csmsg("HOP list.") 715 | csmsg("") 716 | csmsg("The \x02HOP LIST\x0F command displays te HOP list.") 717 | csmsg("") 718 | csmsg("The \x02HOP CLEAR\x0F command clears all entries of the") 719 | csmsg("HOP list.") 720 | 721 | elif args == 'vop': 722 | csmsg("Syntax: \x02VOP \x1Fchannel\x0F \x02ADD \x1Fnick\x0F") 723 | csmsg(" \x02VOP \x1Fchannel\x0F \x02DEL \x1Fnick\x0F") 724 | csmsg(" \x02VOP \x1Fchannel\x0F \x02LIST\x0F") 725 | csmsg(" \x02VOP \x1Fchannel\x0F \x02CLEAR\x0F") 726 | csmsg("") 727 | csmsg("Maintains the \x02VOP\x0F (VOiced People) \x02list\x0F for a channel.") 728 | csmsg("The VOP list allows users to be auto-voices and to voice") 729 | csmsg("themselves if they aren't.") 730 | csmsg("") 731 | csmsg("The \x02VOP ADD\x0F command adds the given nickname to the") 732 | csmsg("VOP list.") 733 | csmsg("") 734 | csmsg("The \x02VOP DEL\x0F command removes the given nick from the") 735 | csmsg("VOP list.") 736 | csmsg("") 737 | csmsg("The \x02VOP LIST\x0F command displays the VOP list.") 738 | csmsg("") 739 | csmsg("The \x02VOP CLEAR\x0F command clears all entries of the") 740 | csmsg("VOP list.") 741 | 742 | elif args == 'owner': 743 | csmsg("Syntax: \x02OWNER \x1Fchannel\x0F \x02\x1Fnick\x0F") 744 | csmsg("") 745 | csmsg("Gives owner status to a selected nick on \x02channel\x0F.") 746 | csmsg("Limited to those with founder access on the channel.") 747 | 748 | elif args == 'deowner': 749 | csmsg("Syntax: \x02DEOWNER \x1Fchannel\x0F \x02\x1Fnick\x0F") 750 | csmsg("") 751 | csmsg("Removes owner status from a selected nick on \x02channel\x0F.") 752 | csmsg("Limited to those with founder access on the channel.") 753 | 754 | elif args == 'op': 755 | csmsg("Syntax: \x02OP \x1Fchannel\x0F \x02\x1Fnick\x0F") 756 | csmsg("") 757 | csmsg("Ops a selected nick on a channel.") 758 | csmsg("Limited to AOPs and above.") 759 | 760 | elif args == 'deop': 761 | csmsg("Syntax: \x02DEOP \x1Fchannel\x0F \x02\x1Fnick\x0F") 762 | csmsg("") 763 | csmsg("Deops a selected nick on a channel.") 764 | csmsg("Limited to AOPs and above.") 765 | 766 | 767 | elif args == 'halfop': 768 | csmsg("Syntax: \x02HALFOP \x1Fchannel\x0F \x02\x1Fnick\x0F") 769 | csmsg("") 770 | csmsg("Halfops a selected nick on a channel.") 771 | csmsg("Limited to AOPs and above.") 772 | 773 | elif args == 'dehalfop': 774 | csmsg("Syntax: \x02DEHALFOP \x1Fchannel\x0F \x02\x1Fnick\x0F") 775 | csmsg("") 776 | csmsg("Dehalfops a selected nick on a channel.") 777 | csmsg("Limited to AOPs and above.") 778 | 779 | elif args == 'voice': 780 | csmsg("Syntax: \x02VOICE \x1Fchannel\x0F \x02\x1Fnick\x0F") 781 | csmsg("") 782 | csmsg("Voices a selected nick on a channel.") 783 | csmsg("Limited to AOPs and above.") 784 | 785 | elif args == 'devoice': 786 | csmsg("Syntax: \x02DEVOICE \x1Fchannel\x0F \x02\x1Fnick\x0F") 787 | csmsg("") 788 | csmsg("Devoices a selected nick on a channel.") 789 | csmsg("Limited to AOPs and above.") 790 | 791 | elif args == 'kick': 792 | csmsg("Syntax \x02KICK \x1Fchannel\x0F \x02\x1Fnick\x0F \x02\x1Freason\x0F") 793 | csmsg("") 794 | csmsg("Kicks a selected nick on a channel, provided you have") 795 | csmsg("the rights to.") 796 | 797 | elif args == 'clear': 798 | csmsg("Syntax: \x02CLEAR \x1Fchannel\x0F \x02\x1Fwhat\x0F") 799 | csmsg("") 800 | csmsg("Tells ChanServ to clear certain settings on a channel. \x1fwhat\x0F") 801 | csmsg("can be any of the following:") 802 | csmsg("") 803 | csmsg(" MODES Resets all modes on the channel, leaving only +Rnt") 804 | csmsg(" intact.") 805 | csmsg(" BANS Clears all bans on the channel.") 806 | csmsg(" EXCEPTS Clears all excepts on the channel.") 807 | csmsg(" OPS Removes channel-operator (mode +o) from all channel") 808 | csmsg(" Operators.") 809 | csmsg(" HOPS Removes channel half-operator status (mode +h) from") 810 | csmsg(" all channel HalfOps.") 811 | csmsg(" VOICES Removes \"voice\" status (mode +v) from anyone with") 812 | csmsg(" that mode set.") 813 | csmsg(" USERS Removes (kicks) all users from the channel who are") 814 | csmsg(" neither (User-Mode) +Q or authenticated as the") 815 | csmsg(" channel Founder.") 816 | csmsg("") 817 | csmsg("Limited to IRC Operators and those with Founder access on the") 818 | csmsg("channel.") 819 | 820 | elif args == 'protect': 821 | csmsg("Syntax: \x02PROTECT \x1Fchannel\x0F \x02\x1Fnick\x0F") 822 | csmsg("") 823 | csmsg("Protects a registered nick on a channel. This prevents the") 824 | csmsg("selected mick from having their privileges revoked, from") 825 | csmsg("being kicked and from matching ChanServ bans when joining.") 826 | csmsg("") 827 | csmsg("By default, limited to the founder, SOPs and IRC Operators.") 828 | 829 | elif args == 'deprotect': 830 | csmsg("Syntax: \x02DEPROTECT \x1Fchannel\x0F \x02\x1Fnick\x0F") 831 | csmsg("") 832 | csmsg("Deprotects a selected nick on a channel.") 833 | csmsg("Use \x02/CHANSERV HELP PROTECT\x0F to see a description of") 834 | csmsg("what the \x02PROTECT\x0F command protects.") 835 | csmsg("") 836 | csmsg("By default, limited to the founder, SOPs and IRC Operators.") 837 | 838 | else: 839 | if args: csmsg("No help available for \x02%s\x0F." % args) 840 | 841 | elif cmd == 'register': 842 | if not args or len(args.split()) < 3: 843 | csmsg("Syntax: \x02/CHANSERV REGISTER \x1Fchannel\x0F \x02\x1Fpassword\x0F \x02\x1Fdescription\x0F") 844 | elif not 'R' in client.modes: 845 | csmsg("A registered nickname is required for channel registration.") 846 | else: 847 | channel_name, password, description = args.split(' ',2) 848 | password = hashlib.sha1(args.encode('utf-8')).hexdigest() 849 | if not re.match('^#([a-zA-Z0-9_])+$', channel_name): 850 | csmsg("\x02%s\x0F is not a valid channel name.") 851 | else: 852 | r = None 853 | db = cache['db'] 854 | c = db.cursor() 855 | c.execute("SELECT * FROM %s WHERE channel=?" % TABLE, (channel_name,)) 856 | r = c.fetchone() 857 | if r: csmsg("\x02%s\x0F is already registered." % channel_name) 858 | else: 859 | c.execute("SELECT * FROM %s WHERE owner=?" % TABLE, (client.nick,)) 860 | r = c.fetchall() 861 | if len(r) >= MAX_CHANNELS: 862 | csmsg("You already have %i channels registered to this nick:" % MAX_CHANNELS) 863 | for i in r: csmsg("\x02%s\x0F, %s" % (i['channel'],fmt_timestamp(i['time_reg']))) 864 | del i 865 | else: 866 | channel = client.channels.get(channel_name) 867 | if channel: 868 | topic = channel.topic 869 | topic_by = channel.topic_by 870 | topic_time = channel.topic_time 871 | else: 872 | topic = topic_by = topic_time = '' 873 | t = time.time() 874 | db.execute("INSERT INTO %s VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" % \ 875 | TABLE, (channel_name,password,description,client.nick,'','',topic,topic_by,topic_time,t,t, 876 | '','','','','','','','','','','','','')) 877 | db.commit() 878 | csmsg("Registered \x02%s\x0F to \x02%s\x0F." % (channel_name,client.nick)) 879 | client.broadcast('umode:W',':%s NOTICE * :%s has registered the channel \x02%s\x0F.' % \ 880 | (CS_IDENT, client.nick, channel_name)) 881 | if channel: 882 | client.broadcast(channel_name, ':%s MODE %s +R' % (CS_IDENT,channel_name)) 883 | del db,c,r 884 | 885 | elif cmd == 'set': 886 | if not args or len(args.split(' ',2)) < 3: 887 | csmsg("Syntax: \x02SET \x1Fchannel\x0F \x02\x1Foption\x0F \x02\x1Fparameters\x0F") 888 | csmsg("\x02/CHANSERV HELP SET\x0F for more information.") 889 | else: 890 | channel, option = (args.split()[0], args.split()[1]) 891 | params = escape(params.split(' ',3)[3]) 892 | c = Channel(channel) 893 | if not 'R' in client.modes: 894 | csmsg("Access denied.") 895 | elif not c.r: 896 | csmsg("\x02%s\x0F is not a registered channel." % channel) 897 | else: 898 | if not 'A' in client.modes and client.nick != c['owner'] and client.nick != c['successor']: 899 | csmsg("Access denied.") 900 | else: 901 | if option == 'founder': 902 | if not 'A' in client.modes and client.nick != c['owner']: 903 | csmsg("Access denied.") 904 | else: 905 | db = cache['db'] 906 | cur = db.cursor() 907 | cur.execute("SELECT * FROM %s WHERE nick=?" % NS_TABLE, (params,)) 908 | r = cur.fetchone() 909 | if not r: csmsg("\x02%s\x0F isn't a registered nick." % params) 910 | else: 911 | c['owner'] = escape(params) 912 | csmsg("Founder for %s changed to \x02%s\x0F." % (channel,params)) 913 | del db,cur,r 914 | 915 | elif option == 'successor': 916 | if not 'A' in client.modes and client.nick != c['owner']: 917 | csmsg("Access denied.") 918 | else: 919 | db = cache['db'] 920 | cur = db.cursor() 921 | cur.execute("SELECT * FROM %s WHERE nick=?" % NS_TABLE, (params,)) 922 | r = cur.fetchone() 923 | if not r: csmsg("\x02%s\x0F isn't a registered nick." % params) 924 | else: 925 | c['successor'] = escape(params) 926 | csmsg("Successor for %s changed to \x02%s\x0F." % (channel,params)) 927 | del db,cur,r 928 | 929 | elif option == 'password': 930 | if not 'A' in client.modes and client.nick != c['owner']: 931 | csmsg("Access denied.") 932 | else: 933 | c['password'] = hashlib.sha1(params.encode('utf-8')).hexdigest() 934 | csmsg("Password for %s changed to \x02%s\x0F." % (channel,params)) 935 | 936 | elif option == 'desc': 937 | c['description'] = escape(params) 938 | csmsg("Description for %s changed to \x02%s\x0F." % (channel,params)) 939 | 940 | elif option == 'url': 941 | c['url'] = escape(params) 942 | csmsg("URL for %s changed to \x02%s\x0F" % (channel,params)) 943 | 944 | elif option == 'email': 945 | if not 'A' in client.modes and client.nick != c['owner']: 946 | csmsg("Access denied.") 947 | else: 948 | c['email'] = escape(params) 949 | csmsg("Email address for %s changed to \x02%s\x0F." % (channel,params)) 950 | 951 | elif option == 'entrymsg': 952 | if params.lower() == 'off': 953 | c['entrymsg'] = '' 954 | csmsg("Entry message disabled for %s." % channel) 955 | else: 956 | c['entrymsg'] = escape(params) 957 | csmsg("Entry message for %s changed to \x02%s\x0F." % (channel,params)) 958 | 959 | elif option.lower() in ['mlock','keeptopic','peace', 'restricted', 'secureops', 'topiclock']: 960 | if params.lower() == 'off' and not c[option] or params.lower() == c[option]: 961 | csmsg("%s is already \x02%s\x0F for %s." % (option.title(), params.upper(), channel)) 962 | else: 963 | if params.lower() == 'off': 964 | if option.lower() == 'mlock': del c['modes'] 965 | c[option] = '' 966 | else: 967 | if option.lower() == 'mlock': 968 | chan = client.server.channels.get(channel) 969 | if not chan: 970 | csmsg("\x02%s\x0F isn't active at the moment. No modes appended." % chan) 971 | else: 972 | for mode, settings in chan.modes.items(): 973 | if mode in ['v','h','o','a','q','b','e','R']: continue 974 | if type(settings) == list: 975 | c['modes:+%s' % mode] = ','.join(settings) 976 | # Comment the following line if you would like to persist 977 | # invites across channel deaths. 978 | elif str(mode) == '+i': c['modes:+%s' % mode] = '' 979 | elif type(settings) in [str, unicode, int, float]: 980 | c['modes:+%s' % mode ] = str(settings) 981 | csmsg("The following modes are locked for \x02%s\x0F: %s." % \ 982 | (channel,', '.join(c['modes'].keys()))) 983 | c[option] = 'on' 984 | csmsg("%s for %s set to \x02%s\x0F." % (option.title(),channel,params.upper())) 985 | else: 986 | csmsg("Unkown option \x02%s\x0F." % option.upper()) 987 | csmsg("\x02/CHANSERV HELP SET\x0F for more information.") 988 | del c 989 | 990 | elif cmd == 'enforce': 991 | if not args or len(args.split()) != 2: csmsg("Syntax: \x02ENFORCE \x1Fchannel\x0F \x02\x1Fwhat\x0F") 992 | else: 993 | chan,what = args.split() 994 | what = what.lower() 995 | c = Channel(chan) 996 | channel = client.server.channels.get(chan) 997 | if (not 'R' in client.modes or client.nick != c['owner']) and not client.oper: 998 | csmsg("Access denied.") 999 | elif not c.r: csmsg("\x02%s\x0F is not registered." % chan) 1000 | elif not channel: csmsg("\x02%s\x0F is not in use." % chan) 1001 | else: 1002 | ops = c['operators'] 1003 | if what == 'set': 1004 | if c['secureops']: secureops(channel) 1005 | else: csmsg("Didn't enforce SecureOps.") 1006 | if c['restricted']: restrict(channel) 1007 | else: csmsg("Didn't enforce RESTRICTED.") 1008 | elif what == 'secureops': secureops(channel) 1009 | elif what == 'restricted': restrict(channel) 1010 | elif what == 'modes': 1011 | modes = c['modes'] 1012 | for_removal = [] 1013 | for mode in channel.modes: 1014 | if '+'+mode not in modes and mode not in ['R','n','t','b','e','v','h','o','a','q']: 1015 | for_removal.append('-'+mode) 1016 | for mode in for_removal: csmode(channel,mode) 1017 | for mode, settings in c['modes'].items(): 1018 | if not mode[1:] in channel.modes: 1019 | if ',' in settings: settings = settings.split(',') 1020 | csmode(channel,mode,settings) 1021 | if modes: csmsg("Enforced \x02%s\x0F on \x02%s\x0F." % (', '.join(modes.keys()),channel.name)) 1022 | else: csmsg("Enforced modes.") 1023 | else: csmsg("Unkown option \x02%s\x0F." % what) 1024 | 1025 | elif cmd == 'sop': 1026 | if not args or args.split()[1].lower() not in ['add','del','list','clear']: 1027 | csmsg("Syntax: \x02SOP \x1Fchannel\x0F \x02{ADD|DEL|LIST|CLEAR} [\x1Fnick\x0F\x02]\x0F") 1028 | csmsg("\x02/CHANSERV HELP SOP\x0F for more information.") 1029 | else: 1030 | chan, params = args.split(' ',1) 1031 | params = params.split() 1032 | c = Channel(chan) 1033 | if not c.r: csmsg("%s isn't registered." % chan) 1034 | elif ('R' not in client.modes or client.nick != c['owner']) and not client.oper: csmsg("Access denied.") 1035 | else: 1036 | if params[0].lower() in ['add','del']: 1037 | nick = params[1] 1038 | db = cache['db'] 1039 | cur = db.cursor() 1040 | cur.execute("SELECT * FROM %s WHERE nick=?" % NS_TABLE, (nick,)) 1041 | r = cur.fetchone() 1042 | if not r: csmsg("Channel SOP lists may only contain registered nicknames.") 1043 | elif params[0].lower() == 'add': 1044 | c['operators:%s' % nick] = 'a' 1045 | csmsg("\x02%s\x0F added to %s SOP list." % (nick,chan)) 1046 | elif params[0].lower() == 'del': 1047 | ops = c['operators'] 1048 | if nick not in ops or (nick in ops and ops[nick] != 'a'): 1049 | csmsg("\x02%s\x0F is not in the SOP list for %s." % (nick,chan)) 1050 | else: 1051 | del c['operators:%s' % nick] 1052 | csmsg("Removed \x02%s\x0F from %s SOP list." % (nick,chan)) 1053 | del db,cur,r 1054 | elif params[0].lower() == 'list': 1055 | ops = c['operators'] 1056 | lst = [i for i in ops.items() if i[1] == 'a'] 1057 | for x in lst: csmsg("\x02%s\x0F" % x[0]) 1058 | csmsg("End of %s SOP list." % chan) 1059 | elif params[0].lower() == 'clear': 1060 | ops = c['operators'] 1061 | lst = [i for i in ops.items() if i[1] == 'a'] 1062 | for x in lst: del c['operators:%s' % x[0]] 1063 | csmsg("Cleared %s SOP list." % chan) 1064 | 1065 | elif cmd == 'aop': 1066 | if not args or args.split()[1].lower() not in ['add','del','list','clear']: 1067 | csmsg("Syntax: \x02AOP \x1Fchannel\x0F \x02{ADD|DEL|LIST|CLEAR} [\x1Fnick\x0F\x02]\x0F") 1068 | csmsg("\x02/CHANSERV HELP AOP\x0F for more information.") 1069 | else: 1070 | chan, params = args.split(' ',1) 1071 | params = params.split() 1072 | c = Channel(chan) 1073 | if c.r: ops = c['operators'] 1074 | if not c.r: csmsg("%s isn't registered." % chan) 1075 | elif (('R' not in client.modes or client.nick != c['owner']) and not client.oper) \ 1076 | and (client.nick not in ops or (client.nick in ops and (ops[client.nick] != 'a' and ops[client.nick] != 'q'))): 1077 | csmsg("Access denied.") 1078 | else: 1079 | if params[0].lower() in ['add','del']: 1080 | nick = params[1] 1081 | db = cache['db'] 1082 | cur = db.cursor() 1083 | cur.execute("SELECT * FROM %s WHERE nick=?" % NS_TABLE, (nick,)) 1084 | r = cur.fetchone() 1085 | if not r: csmsg("Channel AOP lists may only contain registered nicknames.") 1086 | elif params[0].lower() == 'add': 1087 | c['operators:%s' % nick] = 'o' 1088 | csmsg("\x02%s\x0F added to %s AOP list." % (nick,chan)) 1089 | elif params[0].lower() == 'del': 1090 | ops = c['operators'] 1091 | if nick not in ops or (nick in ops and ops[nick] != 'o'): 1092 | csmsg("\x02%s\x0F is not in the AOP list for %s." % (nick,chan)) 1093 | else: 1094 | del c['operators:%s' % nick] 1095 | csmsg("Removed \x02%s\x0F from %s AOP list." % (nick,chan)) 1096 | del db,cur,r 1097 | elif params[0].lower() == 'list': 1098 | lst = [i for i in ops.items() if i[1] == 'o'] 1099 | for x in lst: csmsg("\x02%s\x0F" % x[0]) 1100 | csmsg("End of %s AOP list." % chan) 1101 | elif params[0].lower() == 'clear': 1102 | ops = c['operators'] 1103 | lst = [i for i in ops.items() if i[1] == 'o'] 1104 | for x in lst: del c['operators:%s' % x[0]] 1105 | csmsg("Cleared %s AOP list." % chan) 1106 | 1107 | elif cmd == 'hop': 1108 | if not args or args.split()[1].lower() not in ['add','del','list','clear']: 1109 | csmsg("Syntax: \x02HOP \x1Fchannel\x0F \x02{ADD|DEL|LIST|CLEAR} [\x1Fnick\x0F\x02]\x0F") 1110 | csmsg("\x02/CHANSERV HELP HOP\x0F for more information.") 1111 | else: 1112 | chan, params = args.split(' ',1) 1113 | params = params.split() 1114 | c = Channel(chan) 1115 | if c.r: ops = c['operators'] 1116 | if not c.r: csmsg("%s isn't registered." % chan) 1117 | elif (('R' not in client.modes or client.nick != c['owner']) and not client.oper) \ 1118 | and (client.nick not in ops or (client.nick in ops \ 1119 | and (ops[client.nick] != 'o' and ops[client.nick] != 'a' and ops[client.nick] != 'q'))): 1120 | csmsg("Access denied.") 1121 | else: 1122 | if params[0].lower() in ['add','del']: 1123 | nick = params[1] 1124 | db = cache['db'] 1125 | cur = db.cursor() 1126 | cur.execute("SELECT * FROM %s WHERE nick=?" % NS_TABLE, (nick,)) 1127 | r = cur.fetchone() 1128 | if not r: csmsg("Channel HOP lists may only contain registered nicknames.") 1129 | elif params[0].lower() == 'add': 1130 | c['operators:%s' % nick] = 'h' 1131 | csmsg("\x02%s\x0F added to %s HOP list." % (nick,chan)) 1132 | elif params[0].lower() == 'del': 1133 | ops = c['operators'] 1134 | if nick not in ops or (nick in ops and ops[nick] != 'h'): 1135 | csmsg("\x02%s\x0F is not in the HOP list for %s." % (nick,chan)) 1136 | else: 1137 | del c['operators:%s' % nick] 1138 | csmsg("Removed \x02%s\x0F from %s HOP list." % (nick,chan)) 1139 | del db,cur,r 1140 | elif params[0].lower() == 'list': 1141 | lst = [i for i in ops.items() if i[1] == 'h'] 1142 | for x in lst: csmsg("\x02%s\x0F" % x[0]) 1143 | csmsg("End of %s HOP list." % chan) 1144 | elif params[0].lower() == 'clear': 1145 | ops = c['operators'] 1146 | lst = [i for i in ops.items() if i[1] == 'h'] 1147 | for x in lst: del c['operators:%s' % x[0]] 1148 | csmsg("Cleared %s HOP list." % chan) 1149 | 1150 | elif cmd == 'vop': 1151 | if not args or args.split()[1].lower() not in ['add','del','list','clear']: 1152 | csmsg("Syntax: \x02VOP \x1Fchannel\ x0F\x02{ADD|DEL|LIST|CLEAR} [\x1Fnick\x0F\x02]\x0F") 1153 | csmsg("\x02/CHANSERV HELP VOP\x0F for more information.") 1154 | else: 1155 | chan, params = args.split(' ',1) 1156 | params = params.split() 1157 | c = Channel(chan) 1158 | if c.r: ops = c['operators'] 1159 | if not c.r: csmsg("%s isn't registered." % chan) 1160 | elif (('R' not in client.modes or client.nick != c['owner']) and not client.oper) \ 1161 | and (client.nick not in ops or (client.nick in ops \ 1162 | and (ops[client.nick] != 'o' and ops[client.nick] != 'a' and ops[client.nick] != 'q'))): 1163 | csmsg("Access denied") 1164 | else: 1165 | if params[0].lower() in ['add','del']: 1166 | nick = params[1] 1167 | db = cache['db'] 1168 | cur = db.cursor() 1169 | cur.execute("SELECT * FROM %s WHERE nick=?" % NS_TABLE, (nick,)) 1170 | r = cur.fetchone() 1171 | if not r: csmsg("Channel VOP lists may only contain registered nicknames.") 1172 | elif params[0].lower() == 'add': 1173 | c['operators:%s' % nick] = 'v' 1174 | csmsg("\x02%s\x0F added to %s VOP list." % (nick,chan)) 1175 | elif params[0].lower() == 'del': 1176 | ops = c['operators'] 1177 | if nick not in ops or (nick in ops and ops[nick] != 'v'): 1178 | csmsg("\x02%s\x0F is not in the VOP list for %s." % (nick,chan)) 1179 | else: 1180 | del c['operators:%s' % nick] 1181 | csmsg("Removed \x02%s\x0F from %s VOP list." % (nick,chan)) 1182 | del db,cur,r 1183 | elif params[0].lower() == 'list': 1184 | lst = [i for i in ops.items() if i[1] == 'v'] 1185 | for x in lst: csmsg("\x02%s\x0F" % x[0]) 1186 | csmsg("End of %s VOP list." % chan) 1187 | elif params[0].lower() == 'clear': 1188 | ops = c['operators'] 1189 | lst = [i for i in ops.items() if i[1] == 'v'] 1190 | for x in lst: del c['operators:%s' % x[0]] 1191 | csmsg("Cleared %s VOP list." % chan) 1192 | 1193 | elif cmd == 'ban': 1194 | if not args or len(args.split(' ',1)) != 2: 1195 | csmsg("Syntax: \x02/CHANSERV BAN \x1F#channel\x0F \x02\x1Fmask\x0F") 1196 | else: 1197 | chan, mask = args.split(' ',1) 1198 | chan = escape(chan) 1199 | mask = escape(mask) 1200 | if '!' not in mask and '@' not in mask: mask += '!*@*' 1201 | c = Channel(chan) 1202 | if not c.r: csmsg("\x02%s\x0F isn't registered." % chan) 1203 | else: 1204 | o = c['operators'] 1205 | if not client.oper and 'R' not in client.modes or client.nick != c['owner'] and client.nick not in o \ 1206 | or (client.nick in o and (o[client.nick] == 'v' or o[client.nick] == 'h')): 1207 | csmsg("Access denied.") 1208 | else: 1209 | b = c['bans'] 1210 | m = re_to_irc(mask,False) 1211 | if m in b: csmsg("\x02%s\x0F is already banned from %s." % (mask,chan)) 1212 | else: 1213 | c['bans:%s' % m] = client.nick 1214 | csmsg("Banned \x02%s\x0F from %s." % (mask,chan)) 1215 | 1216 | elif cmd == 'unban': 1217 | if not args or len(args.split(' ',1)) != 2: 1218 | csmsg("Syntax: \x02/CHANSERV UNBAN \x1F#channel\x0F \x02\x1Fmask\x0F") 1219 | else: 1220 | chan, mask = args.split(' ',1) 1221 | chan = escape(chan) 1222 | mask = escape(mask) 1223 | if '!' not in mask and '@' not in mask: mask += '!*@*' 1224 | c = Channel(chan) 1225 | if not c.r: csmsg("\x02%s\x0F isn't registered." % chan) 1226 | else: 1227 | o = c['operators'] 1228 | if not client.oper and 'R' not in client.modes or client.nick != c['owner'] and client.nick not in o \ 1229 | or (client.nick in o and o[client.nick] == 'v' or client.nick in o and o[client.nick] == 'h'): 1230 | csmsg("Access denied.") 1231 | else: 1232 | b = c['bans'] 1233 | m = re_to_irc(mask,False) 1234 | if not m in b: csmsg("\x02%s\x0F isn't banned from %s." % (mask,chan)) 1235 | else: 1236 | del c['bans:%s' % m] 1237 | csmsg("Unbanned \x02%s\x0F from %s." % (mask,chan)) 1238 | 1239 | elif cmd == 'clear': 1240 | if not args or len(args.split(' ',1)) != 2: 1241 | csmsg("Syntax: \x02CLEAR \x1Fchannel\x0F \x02\x1Fwhat\x0F") 1242 | else: 1243 | chan, what = args.split(' ',2) 1244 | what = what.lower() 1245 | c = Channel(chan) 1246 | channel = client.server.channels.get(chan) 1247 | if not c.r: 1248 | csmsg("%s isn't registered." % chan) 1249 | elif not channel: 1250 | csmsg("%s is not currently in use." % chan) 1251 | elif (client.nick != c['owner'] or 'R' not in client.modes) and not client.oper: 1252 | csmsg("Access denied.") 1253 | else: 1254 | if what == 'modes': 1255 | modes = channel.modes.copy() 1256 | [csmode(channel,'-'+mode) for mode in modes if mode not in ['n','t','R','e','b','v','h','o','a','q']] 1257 | csmsg("Modes reset for \x02%s\x0F." % chan) 1258 | elif what == 'bans': 1259 | # Uncomment the following line if you would like this command 1260 | # to also clear ChanServ bans. 1261 | # del c['bans'] 1262 | if 'b' in channel.modes: 1263 | channel.modes['b']=[] 1264 | csmsg("Bans cleared for \x02%s\x0F." % chan) 1265 | elif what == 'excepts': 1266 | if 'e' in channel.modes: 1267 | channel.modes['e']=[] 1268 | csmsg("Excepts cleared for \x02%s\x0F." % chan) 1269 | elif what == 'ops': 1270 | if 'o' in channel.modes and len(channel.modes['o']) > 0: 1271 | for nick in channel.modes['o']: csmode(channel,'-o',nick) 1272 | csmsg("Cleared Operators list on \x02%s\x0F" % chan) 1273 | elif what == 'hops': 1274 | if 'h' in channel.modes and len(channel.modes['h']) > 0: 1275 | for nick in channel.modes['h']: csmode(channel,'-h',nick) 1276 | csmsg("Cleared Half-Operators list on \x02%s\x0F" % chan) 1277 | elif what == 'voices': 1278 | if 'v' in channel.modes and len(channel.modes['v']) > 0: 1279 | for nick in channel.modes['v']: csmode(channel,'-v',nick) 1280 | csmsg("Cleared Voiced People on \x02%s\x0F" % chan) 1281 | elif what == 'users': 1282 | protected = c['protected'] 1283 | for user in channel.clients.copy(): 1284 | if 'Q' in user.modes or ('R' in user.modes and user.nick == c['owner']) \ 1285 | or ('R' in user.modes and user.nick in protected): continue 1286 | client.broadcast(channel.name, ':%s KICK %s %s :CLEAR USERS used by %s.' % \ 1287 | (CS_IDENT, channel.name, user.nick, client.nick)) 1288 | for op_list in channel.ops: 1289 | if user.nick in op_list: op_list.remove(user.nick) 1290 | user.channels.pop(channel.name) 1291 | channel.clients.remove(user) 1292 | if not len(channel.clients): 1293 | client.server.channels.pop(channel.name) 1294 | csmsg("Cleared users from \x02%s\x0F." % chan) 1295 | else: csmsg("Unknown setting \x02%s\x0F." % what) 1296 | 1297 | elif cmd == 'owner': 1298 | if not args or len(args.split(' ',1)) != 2: 1299 | csmsg("Syntax \x02OWNER \x1Fchannel\x0F \x02\x1Fnick\x0F") 1300 | else: 1301 | chan, nick = args.split(' ',1) 1302 | c = Channel(chan) 1303 | channel = client.server.channels.get(chan) 1304 | if c.r: 1305 | ops = c['operators'] 1306 | if not c.r: 1307 | csmsg("%s isn't registered." % chan) 1308 | elif not channel: 1309 | csmsg("%s is not currently in use." % chan) 1310 | elif client.nick != c['owner'] or 'R' not in client.modes: 1311 | csmsg("Access denied.") 1312 | elif nick in channel.modes['q']: 1313 | csmsg("%s is already an owner in %s." % (nick,chan)) 1314 | else: 1315 | user = [u for u in channel.clients if u.nick == nick] 1316 | if user: 1317 | csmode(channel,'+q',nick) 1318 | csmsg("Owner status given to %s in %s." % (nick,chan)) 1319 | else: csmsg("%s is not on %s." % (nick,chan)) 1320 | 1321 | elif cmd == 'deowner': 1322 | if not args or len(args.split(' ',1)) != 2: 1323 | csmsg("Syntax \x02DEOWNER \x1Fchannel\x0F \x02\x1Fnick\x0F") 1324 | else: 1325 | chan, nick = args.split(' ',1) 1326 | c = Channel(chan) 1327 | channel = client.server.channels.get(chan) 1328 | if c.r: 1329 | ops = c['operators'] 1330 | if not c.r: 1331 | csmsg("%s isn't registered." % chan) 1332 | elif not channel: 1333 | csmsg("%s is not currently in use." % chan) 1334 | elif client.nick != c['owner'] or 'R' not in client.modes: 1335 | csmsg("Access denied.") 1336 | elif nick not in channel.modes['q']: 1337 | csmsg("%s is not an owner in %s." % (nick,chan)) 1338 | else: 1339 | user = [u for u in channel.clients if u.nick == nick] 1340 | if user: 1341 | csmode(channel,'-q',nick) 1342 | csmsg("Owner status removed from %s in %s." % (nick,chan)) 1343 | else: csmsg("%s is not on %s." % (nick,chan)) 1344 | 1345 | elif cmd == 'protect': 1346 | if not args or len(args.split(' ',1)) != 2: 1347 | csmsg("Syntax \x02PROTECT \x1Fchannel\x0F \x02\x1Fnick\x0F") 1348 | else: 1349 | chan, nick = args.split(' ',1) 1350 | c = Channel(chan) 1351 | channel = client.server.channels.get(chan) 1352 | if c.r: 1353 | ops = c['operators'] 1354 | protected = c['protected'] 1355 | if not c.r: 1356 | csmsg("%s isn't registered." % chan) 1357 | elif not 'R' in client.modes and not client.oper: 1358 | csmsg("Access denied.") 1359 | elif (client.nick not in ops and client.nick != c['owner']) \ 1360 | or (client.nick in ops and ops[client.nick] != 'a') and not client.oper: 1361 | csmsg("Access denied.") 1362 | elif nick in protected: csmsg("%s is already protected in \x02%s\x0F." % (nick,chan)) 1363 | else: 1364 | db = cache['db'] 1365 | cur = db.cursor() 1366 | cur.execute("SELECT * FROM %s WHERE nick=?" % NS_TABLE, (nick,)) 1367 | r = cur.fetchone() 1368 | if not r: csmsg("\x02%s\x0F isn't registered." % nick) 1369 | else: 1370 | c['protected:%s' % nick] = client.nick 1371 | csmsg("Protected %s in \x02%s\x0F." % (nick,chan)) 1372 | del db,cur,r 1373 | 1374 | elif cmd == 'deprotect': 1375 | if not args or len(args.split(' ',1)) != 2: 1376 | csmsg("Syntax \x02DEPROTECT \x1Fchannel\x0F \x02\x1Fnick\x0F") 1377 | else: 1378 | chan, nick = args.split(' ',1) 1379 | c = Channel(chan) 1380 | channel = client.server.channels.get(chan) 1381 | if c.r: 1382 | ops = c['operators'] 1383 | protected = c['protected'] 1384 | if not c.r: 1385 | csmsg("%s isn't registered." % chan) 1386 | elif not 'R' in client.modes and not client.oper: 1387 | csmsg("Access denied.") 1388 | elif (client.nick not in ops and client.nick != c['owner']) \ 1389 | or (client.nick in ops and ops[client.nick] != 'a') and not client.oper: 1390 | csmsg("Access denied.") 1391 | elif nick not in protected: 1392 | csmsg("%s isn't in the list of protected users for \x02%s\x0F." % (nick,chan)) 1393 | else: 1394 | del c['protected:%s' % nick] 1395 | csmsg("Removed %s from the list of protected users for \x02%s\x0F." % (nick,chan)) 1396 | 1397 | elif cmd == 'op': 1398 | if not args or len(args.split(' ',1)) != 2: 1399 | csmsg("Syntax \x02OP \x1Fchannel\x0F \x02\x1Fnick\x0F") 1400 | else: 1401 | chan, nick = args.split(' ',1) 1402 | c = Channel(chan) 1403 | channel = client.server.channels.get(chan) 1404 | if c.r: 1405 | ops = c['operators'] 1406 | if not c.r: 1407 | csmsg("%s isn't registered." % chan) 1408 | elif not channel: 1409 | csmsg("%s is not currently in use." % chan) 1410 | elif client.nick not in ops and c['owner'] != client.nick: 1411 | csmsg("Access denied.") 1412 | elif c['owner'] != client.nick and client.nick in ops \ 1413 | and (ops[client.nick] == 'v' or ops[client.nick] == 'h'): 1414 | csmsg("Access denied.") 1415 | elif nick in channel.modes['o']: 1416 | csmsg("%s is already an operator in %s." % (nick,chan)) 1417 | else: 1418 | user = [u for u in channel.clients if u.nick == nick] 1419 | if user: 1420 | csmode(channel,'+o',nick) 1421 | csmsg("Operator status given to %s in %s." % (nick,chan)) 1422 | else: csmsg("%s is not on %s." % (nick,chan)) 1423 | 1424 | elif cmd == 'deop': 1425 | if not args or len(args.split(' ',1)) != 2: 1426 | csmsg("Syntax \x02DEOP \x1Fchannel\x0F \x02\x1Fnick\x0F") 1427 | else: 1428 | chan, nick = args.split(' ',1) 1429 | c = Channel(chan) 1430 | channel = client.server.channels.get(chan) 1431 | if c.r: 1432 | ops = c['operators'] 1433 | if not c.r: 1434 | csmsg("%s isn't registered." % chan) 1435 | elif not channel: 1436 | csmsg("%s is not currently in use." % chan) 1437 | elif not 'R' in client.modes: csmsg("Access denied. (Must be identified with services.)") 1438 | elif client.nick not in ops and c['owner'] != client.nick: 1439 | csmsg("Access denied.") 1440 | elif client.nick in ops and (ops[client.nick] == 'v' or ops[client.nick] == 'h') \ 1441 | and c['owner'] != client.nick: 1442 | csmsg("Access denied.") 1443 | elif nick not in channel.modes['o']: 1444 | csmsg("%s isn't an operator in %s." % (nick,chan)) 1445 | else: 1446 | user = [u for u in channel.clients if u.nick == nick] 1447 | if user: 1448 | csmode(channel,'-o',nick) 1449 | csmsg("Removed operator status from %s in %s." % (nick,chan)) 1450 | else: csmsg("%s is not on %s." % (nick,chan)) 1451 | 1452 | elif cmd == 'halfop': 1453 | if not args or len(args.split(' ',1)) != 2: 1454 | csmsg("Syntax \x02HALFOP \x1Fchannel\x0F \x02\x1Fnick\x0F") 1455 | else: 1456 | chan, nick = args.split(' ',1) 1457 | c = Channel(chan) 1458 | channel = client.server.channels.get(chan) 1459 | if c.r: 1460 | ops = c['operators'] 1461 | if not c.r: 1462 | csmsg("%s isn't registered." % chan) 1463 | elif not channel: 1464 | csmsg("%s is not currently in use." % chan) 1465 | elif client.nick not in ops and c['owner'] != client.nick: 1466 | csmsg("Access denied.") 1467 | elif c['owner'] != client.nick and client.nick in ops \ 1468 | and (ops[client.nick] == 'v' or ops[client.nick] == 'h'): 1469 | csmsg("Access denied.") 1470 | elif nick in channel.modes['h']: 1471 | csmsg("%s is already a half-operator in %s." % (nick,chan)) 1472 | else: 1473 | user = [u for u in channel.clients if u.nick == nick] 1474 | if user: 1475 | csmode(channel,'+h',nick) 1476 | csmsg("Half Operator status given to %s in %s." % (nick,chan)) 1477 | else: csmsg("%s is not on %s." % (nick,chan)) 1478 | 1479 | elif cmd == 'dehalfop': 1480 | if not args or len(args.split(' ',1)) != 2: 1481 | csmsg("Syntax \x02DEHALFOP \x1Fchannel\x0F \x02\x1Fnick\x0F") 1482 | else: 1483 | chan, nick = args.split(' ',1) 1484 | c = Channel(chan) 1485 | channel = client.server.channels.get(chan) 1486 | if c.r: 1487 | ops = c['operators'] 1488 | if not c.r: 1489 | csmsg("%s isn't registered." % chan) 1490 | elif not channel: 1491 | csmsg("%s is not currently in use." % chan) 1492 | elif client.nick not in ops and c['owner'] != client.nick: 1493 | csmsg("Access denied.") 1494 | elif c['owner'] != client.nick and client.nick in ops \ 1495 | and (ops[client.nick] == 'v' or ops[client.nick] == 'h'): 1496 | csmsg("Access denied.") 1497 | elif nick not in channel.modes['h']: 1498 | csmsg("%s isn't a half-operator in %s." % (nick,chan)) 1499 | else: 1500 | user = [u for u in channel.clients if u.nick == nick] 1501 | if user: 1502 | csmode(channel,'-h',nick) 1503 | csmsg("Removed half operator status from %s in %s." % (nick,chan)) 1504 | else: csmsg("%s is not on %s." % (nick,chan)) 1505 | 1506 | elif cmd == 'voice': 1507 | if not args or len(args.split(' ',1)) != 2: 1508 | csmsg("Syntax \x02VOICE \x1Fchannel\x0F \x02\x1Fnick\x0F") 1509 | else: 1510 | chan, nick = args.split(' ',1) 1511 | c = Channel(chan) 1512 | channel = client.server.channels.get(chan) 1513 | if c.r: 1514 | ops = c['operators'] 1515 | if not c.r: 1516 | csmsg("%s isn't registered." % chan) 1517 | elif not channel: 1518 | csmsg("%s is not currently in use." % chan) 1519 | elif client.nick not in ops and c['owner'] != client.nick: 1520 | csmsg("Access denied.") 1521 | elif client.nick in ops and ops[client.nick] == 'v' and c['owner'] != client.nick: 1522 | csmsg("Access denied.") 1523 | elif nick in channel.modes['v']: 1524 | csmsg("%s is already voiced in %s." % (nick,chan)) 1525 | else: 1526 | user = [u for u in channel.clients if u.nick == nick] 1527 | if user: 1528 | csmode(channel,'+o',nick) 1529 | csmsg("Voice given to %s in %s." % (nick,chan)) 1530 | else: csmsg("%s is not on %s." % (nick,chan)) 1531 | 1532 | elif cmd == 'devoice': 1533 | if not args or len(args.split(' ',1)) != 2: 1534 | csmsg("Syntax \x02DEVOICE \x1Fchannel\x0F \x02\x1Fnick\x0F") 1535 | else: 1536 | chan, nick = args.split(' ',1) 1537 | c = Channel(chan) 1538 | channel = client.server.channels.get(chan) 1539 | if c.r: 1540 | ops = c['operators'] 1541 | if not c.r: 1542 | csmsg("%s isn't registered." % chan) 1543 | elif not channel: 1544 | csmsg("%s is not currently in use." % chan) 1545 | elif client.nick not in ops and c['owner'] != client.nick: 1546 | csmsg("Access denied.") 1547 | elif client.nick in ops and ops[client.nick] == 'v' and c['owner'] != client.nick: 1548 | csmsg("Access denied.") 1549 | elif nick not in channel.modes['v']: 1550 | csmsg("%s isn't voiced in %s." % (nick,chan)) 1551 | else: 1552 | user = [u for u in channel.clients if u.nick == nick] 1553 | if user: 1554 | csmode(channel,'-v',nick) 1555 | csmsg("Voice removed from %s in %s." % (nick,chan)) 1556 | else: csmsg("%s is not on %s." % (nick,chan)) 1557 | 1558 | elif cmd == 'invite': 1559 | if not args: csmsg("Syntax: \x02/CHANSERV INVITE \x1Fchannel\x0F") 1560 | elif not 'R' in client.modes: csmsg("Access denied.") 1561 | else: 1562 | c = Channel(args) 1563 | channel = client.server.channels.get(args) 1564 | if not c.r or not channel: csmsg("Channel \x02%s\x0F doesn't exist") 1565 | elif 'i' not in channel.modes: csmsg("\x02%s\x0F is not +i.") 1566 | else: 1567 | o = c['operators'] 1568 | if client.nick != c['owner'] and client.nick != c['successor'] and \ 1569 | (not client.nick in o or (client.nick in o and o[client.nick] == 'v')): 1570 | csmsg("Access denied.") 1571 | elif 'i' in channel.modes and type(channel.modes['i']) == list: 1572 | channel.modes['i'].append(client.nick) 1573 | 1574 | # Tell the channel 1575 | response = ':%s NOTICE @%s :%s invited %s into the channel.' % \ 1576 | (CS_IDENT, channel.name, CS_IDENT.split('!')[0], client.nick) 1577 | client.broadcast(channel.name,response) 1578 | 1579 | # Tell the invitee 1580 | response = ':%s INVITE %s :%s' % \ 1581 | (CS_IDENT, client.nick, channel.name) 1582 | client.broadcast(client.nick,response) 1583 | 1584 | elif cmd == 'kick': 1585 | if not args or not ' ' in args or len(args.split(' ',2)) != 3: 1586 | csmsg("Usage: \x02/CHANSERV KICK \x1Fchannel\x0F \x02\x1Fnick\x0F \x02\x1Freason\x0F") 1587 | else: 1588 | channel_name, nick, reason = args.split(' ',2) 1589 | c = Channel(channel_name) 1590 | if not c.r: csmsg("\x02%s\x0F isn't registered." % channel_name) 1591 | else: 1592 | channel = client.server.channels.get(channel_name) 1593 | if not channel: csmsg("%s no such channel." % channel_name) 1594 | else: 1595 | chanops = c['operators'] 1596 | if 'R' not in client.modes: 1597 | csmsg("Access denied. (Must be identified with services.)") 1598 | elif client.nick != c['owner'] and client.nick not in chanops: 1599 | csmsg("Access denied.") 1600 | elif client.nick in chanops and chanops[client.nick] == 'v': 1601 | csmsg("Access denied.") 1602 | elif c['peace']: csmsg("Access denied. (Peace.)") 1603 | else: 1604 | user = None 1605 | for i in channel.clients: 1606 | if i.nick == nick: 1607 | user = i 1608 | break 1609 | if not user: csmsg("\x02%s\x0F is not currently on channel %s" % (nick,channel_name)) 1610 | else: 1611 | if 'Q' in user.modes: csmsg("Cannot kick %s. (+Q)" % nick) 1612 | else: 1613 | for op_list in channel.ops: 1614 | if user.nick in op_list: op_list.remove(user.nick) 1615 | if c['signkick']: client.broadcast(channel.name, ':%s KICK %s %s :%s (%s)' % \ 1616 | (CS_IDENT, channel.name, user.nick, reason, client.nick)) 1617 | else: client.broadcast(channel.name, ':%s KICK %s %s :%s' % \ 1618 | (CS_IDENT, channel.name, user.nick, reason)) 1619 | user.channels.pop(channel.name) 1620 | channel.clients.remove(user) 1621 | del c 1622 | 1623 | elif cmd == 'topic' or cmd == 'appendtopic': 1624 | if not args or len(args.split(' ',1)) < 2: csmsg("Usage: \x02/CHANSERV TOPIC \x1Fchannel\x0F \x02\x1Ftopic\x0F") 1625 | else: 1626 | chan = args.split()[0] 1627 | topic = params.split(' ',2)[2] 1628 | c = Channel(chan) 1629 | channel = client.server.channels.get(chan) 1630 | if not c.r: csmsg("%s isn't registered." % chan) 1631 | elif c['topiclock'] == 'on' and client.nick != c['owner']: 1632 | csmsg("Topic of %s is locked." % chan) 1633 | else: 1634 | ops = c['operators'] 1635 | if not client.nick in ops and client.nick != c['owner'] and client.nick != c['successor']: 1636 | csmsg("You are not a channel operator.") 1637 | elif client.nick in ops and ops[client.nick] == 'v': 1638 | csmsg("You are not a channel operator.") 1639 | else: 1640 | if cmd == 'appendtopic': 1641 | if channel and channel.topic: topic = '%s %s' % (channel.topic,topic) 1642 | elif c['topic']: topic = '%s %s' % (c['topic'],topic) 1643 | if topic != c['topic']: 1644 | c['topic'] = topic 1645 | c['topic_by'] = client.nick 1646 | c['topic_time'] = str(time.time())[:10] 1647 | if not channel: csmsg("Stored topic for %s changed to \x02%s\x0F." % (chan, topic)) 1648 | if channel and channel.topic != topic: 1649 | channel.topic = topic 1650 | channel.topic_time = str(time.time())[:10] 1651 | client.broadcast(channel.name,':%s TOPIC %s :%s' % (CS_IDENT, chan, topic)) 1652 | csmsg("Topic of %s changed to \x02%s\x0F" % (chan,topic)) 1653 | del c 1654 | 1655 | elif cmd == 'list': 1656 | if not args: csmsg("Usage \x02/CHANSERV LIST \x1Fpattern\x0F") 1657 | else: 1658 | if not client.oper or 'R' not in client.modes: 1659 | csmsg("Access denied.") 1660 | else: 1661 | if not args.startswith('#') and not args.startswith('*'): 1662 | args = '#'+args 1663 | args = escape(args.replace('*','%')) 1664 | db = cache['db'] 1665 | c = db.cursor() 1666 | c.execute("SELECT * FROM %s WHERE channel LIKE ?" % TABLE, (args,)) 1667 | t = c.fetchall() 1668 | csmsg_list(t) 1669 | del db,c,t 1670 | 1671 | elif cmd == 'info': 1672 | if not args: csmsg("Usage: \x02/CHANSERV INFO \x1FCHANNEL\x0F") 1673 | else: 1674 | c = Channel(escape(args)) 1675 | if not c.r: csmsg("\x02%s\x0F isn't registered." % args) 1676 | else: 1677 | channel = client.server.channels.get(args) 1678 | bans = c['bans'].items() 1679 | ops = c['operators'] 1680 | if channel: 1681 | csmsg(" \x02%s\x0F is active with %i client(s)" % (args, len(channel.clients))) 1682 | else: 1683 | csmsg("\x02%s\x0F:" % args) 1684 | if c['url']: 1685 | csmsg(" URL: %s" % c['url']) 1686 | if channel: 1687 | csmsg(" Topic: %s" % channel.topic) 1688 | csmsg(" About: %s" % c['description']) 1689 | if client.oper and c['email']: 1690 | csmsg(" E-Mail: \x02%s\x0F" % c['email']) 1691 | if client.oper or 'R' in client.modes: 1692 | csmsg(" Founder: %s" % c['owner']) 1693 | csmsg(" Last used: %s" % fmt_timestamp(c['time_use'])) 1694 | csmsg("Time registered: %s" % fmt_timestamp(c['time_reg'])) 1695 | if bans: 1696 | if client.oper or ('R' in client.modes and client.nick == c['owner']) \ 1697 | or ('R' in client.modes and client.nick in ops and ops[client.nick] != 'v'): 1698 | l = max([len(re_to_irc(i[0])) for i in bans]) 1699 | csmsg(" Bans: \x02%s\x0F %s(%s)" % \ 1700 | (re_to_irc(bans[0][0]), ' ' * int(l - len(re_to_irc(bans[0][0]))), bans[0][1])) 1701 | for index,(mask,setter) in enumerate(bans): 1702 | if index == 0: continue 1703 | mask = re_to_irc(mask) 1704 | csmsg(' '*17+'\x02%s\x0F %s(%s)' % (mask,' ' * int(l - len(mask)),setter)) 1705 | del c 1706 | 1707 | elif cmd == 'drop': 1708 | if not args or not ' ' in args: csmsg("Usage: \x02/CHANSERV DROP \x1Fchannel\x0F \x02\x1Fpassword\x0F") 1709 | else: 1710 | channel_name,password = args.split() 1711 | password = hashlib.sha1(password.encode('utf-8')).hexdigest() 1712 | db = cache['db'] 1713 | c = db.cursor() 1714 | c.execute("SELECT * FROM %s WHERE channel=?" % TABLE, (channel_name,)) 1715 | r = c.fetchone() 1716 | if not r: csmsg("\x02%s\x0F isn't registered." % channel_name) 1717 | else: 1718 | # IRC Operators can supply anything as a password. 1719 | if client.oper or (r['password'] == password): 1720 | c.execute("DELETE FROM %s WHERE channel=?" % TABLE, (channel_name,)) 1721 | db.commit() 1722 | csmsg("Dropped \x02%s\x0F." % channel_name) 1723 | client.broadcast('umode:W',':%s NOTICE * :%s has dropped the channel \x02%s\x0F.' % \ 1724 | (CS_IDENT, client.nick, channel_name)) 1725 | channel = client.server.channels.get(channel_name) 1726 | if channel and 'R' in channel.modes: 1727 | del channel.modes['R'] 1728 | client.broadcast(channel_name, ":%s MODE %s -R" % \ 1729 | (CS_IDENT,channel_name)) 1730 | else: 1731 | csmsg("Incorrect password.") 1732 | warn = ":%s NOTICE * :\x034WARNING\x0F :%s tried to drop %s with an incorrect password." % \ 1733 | (CS_IDENT, client.nick, nick) 1734 | client.broadcast('umode:W', warn) 1735 | del db,c,r 1736 | 1737 | elif cmd == "expire": 1738 | if not client.oper: 1739 | csmsg("Unknown command.") 1740 | csmsg("Use \x02/CHANSERV HELP\x0F for a list of available commands.") 1741 | else: 1742 | db = cache['db'] 1743 | c = db.cursor() 1744 | c.execute("SELECT * FROM %s" % TABLE) 1745 | t = c.fetchall() 1746 | for r in t: 1747 | if is_expired(r['time_use']): 1748 | csmsg("\x02%s\x0F has expired due to inactivity." % r['channel']) 1749 | client.broadcast('umode:W',':%s NOTICE * :%s expired \x02%s\x0F.' % \ 1750 | (CS_IDENT, client.nick, r['channel'])) 1751 | c.execute("DELETE FROM %s WHERE channel=?" % TABLE, (r['channel'],)) 1752 | db.commit() 1753 | csmsg("All registrations have been cycled through.") 1754 | del db,c,r,t 1755 | 1756 | elif cmd == "xyzzy": 1757 | c = Channel(args) 1758 | if c.r and client.oper: 1759 | csmsg(c) 1760 | for i in c.keys(): csmsg('%s: %s' % (i,c[i])) 1761 | csmsg("") 1762 | csmsg("Nothing happens.") 1763 | if c.r and client.oper: csmsg("") 1764 | del c 1765 | 1766 | else: 1767 | csmsg("Unknown command.") 1768 | csmsg("Use \x02/CHANSERV HELP\x0F for a list of available commands.") 1769 | 1770 | -------------------------------------------------------------------------------- /scripts/NickServ.py: -------------------------------------------------------------------------------- 1 | # NickServ.py for Psyrcd. 2 | # Many thanks to the contributors of Anope. 3 | # Implements /nickserv, usermode R and channel mode c. 4 | # MIT License 5 | 6 | # Schema: ip | ident | nick | password | time_reg | time_use 7 | # Colour key: 8 | # \x02 bold 9 | # \x03 coloured text 10 | # \x1D italic text 11 | # \x0F colour reset 12 | # \x16 reverse colour 13 | # \x1F underlined text 14 | 15 | import time 16 | import hashlib 17 | import datetime 18 | 19 | log = cache['config']['logging'] 20 | TABLE = "nickserv" 21 | DB_FILE = "./services.db" 22 | NS_IDENT = "NickServ!services@" + cache['config']['SRV_DOMAIN'] 23 | MAX_NICKS = 3 24 | MAX_RECORDS = 8192 25 | WAIT_MINUTES = 0 26 | MAX_DAYS_UNPRESENT = 31 27 | 28 | def escape(query): return query.replace("'","") 29 | 30 | def nsmsg(msg): 31 | client.broadcast(client.nick, ":%s NOTICE %s :%s" % \ 32 | (NS_IDENT, client.nick, msg)) 33 | 34 | def fmt_timestamp(ts): return datetime.datetime.fromtimestamp(int(ts)).strftime('%b %d %H:%M:%S %Y') 35 | 36 | def nsmsg_list(t): 37 | for r in t: 38 | if client.oper: ip = " IP: %s," % r['ip'] 39 | else: ip = '' 40 | user = client.server.clients.get(r['nick']) 41 | if user: 42 | if 'R' in user.modes: nsmsg("\x02\x033%s\x0F:%s Ident: %s, Registered: %s" % \ 43 | (r['nick'], ip, r['ident'], fmt_timestamp(r['time_reg']))) 44 | else: nsmsg("\x02\x032%s\x0F:%s Ident: %s, Registered: %s" % \ 45 | (r['nick'], ip, r['ident'], fmt_timestamp(r['time_reg']))) 46 | else: nsmsg("\x02%s\x0F:%s Ident: %s, Registered: %s" % \ 47 | (r['nick'], ip, r['ident'], fmt_timestamp(r['time_reg']))) 48 | 49 | def is_expired(seconds): 50 | t = time.time() 51 | seconds = t - seconds 52 | minutes, seconds = divmod(seconds, 60) 53 | hours, minutes = divmod(minutes, 60) 54 | days, hours = divmod(hours, 24) 55 | weeks, days = divmod(days, 7) 56 | if MAX_DAYS_UNPRESENT >= days+(weeks*7): 57 | return False 58 | else: 59 | return True 60 | 61 | class NSError(Exception): 62 | def __init__(self, value): self.value = value # causes error messages to be 63 | def __str__(self): return(repr(self.value)) # dispersed to umode:W users 64 | 65 | if 'init' in dir(): 66 | provides=['command:nickserv,ns:Nickname registration service.', 'umode:R:Registered nickname.', 67 | 'cmode:c:Registered nicknames only.'] 68 | if init: 69 | # You generally want to have your imports here and then put them on the 70 | # cache so they're not recomputed for every sentence said in an associated channel 71 | # or command executed by a similar user, just because you're using the --debug flag. 72 | 73 | # Reader beware: sqlite3 is only being used in keeping with the ethos "only the stdlib". 74 | # Feel free to implement /your/ modules with SQLAlchemy, Dataset, PyMongo, PyTables.. SciKit.. NLTK.. 75 | if not 'db' in cache: 76 | import sqlite3 77 | db = sqlite3.connect(DB_FILE, check_same_thread=False) 78 | db.row_factory = sqlite3.Row 79 | cache['db'] = db 80 | db.execute("CREATE TABLE IF NOT EXISTS %s (ip, ident, nick, password, time_reg REAL, time_use REAL)" % TABLE) 81 | db.commit() 82 | else: 83 | if 'db' in cache: 84 | cache['db'].close() 85 | del cache['db'] 86 | 87 | if 'display' in dir() and 'channel' in dir(): output = 'Registered nicks only.' 88 | 89 | # The following happens when the server detects 90 | # that a user carrying our umode is doing something. 91 | # Here we can determine what the client is doing, and then 92 | # modify the client, the server, and/or command parameters. 93 | if 'func' in dir(): 94 | if func.__name__ == 'handle_join': 95 | if 'channel' in dir(): 96 | if 'c' in channel.modes and not 'R' in client.modes: 97 | nsmsg("A registered nick is required to join %s." % channel.name) 98 | params = '' 99 | 100 | if func.__name__ == 'handle_nick': 101 | db = cache['db'] 102 | c = db.cursor() 103 | c.execute("SELECT * FROM %s WHERE nick=?" % TABLE, (params,)) 104 | r = c.fetchone() 105 | if 'R' in client.modes: 106 | del client.modes['R'] 107 | client.broadcast(client.nick, ":%s MODE %s -R" % (NS_IDENT,client.nick)) 108 | if r: nsmsg("This nickname is owned by \x02%s\x0F." % r['ident']) 109 | del db,c,r 110 | 111 | # This namespace indicates a client is connecting: 112 | if 'new' in dir() and 'channel' not in dir(): 113 | # In this instance we only care if the client 114 | # obtained their default nickname. 115 | if client.nick: 116 | db = cache['db'] 117 | c = db.cursor() 118 | c.execute("SELECT * FROM %s WHERE nick=?" % TABLE, (client.nick,)) 119 | r = c.fetchone() 120 | if r: nsmsg("This nickname is owned by \x02%s\x0F." % r['ident']) 121 | else: 122 | nsmsg("Use \x02/NICKSERV HELP REGISTER\x0F for information on registering this nick.") 123 | if WAIT_MINUTES: 124 | nsmsg("You must be connected for at least %i minutes before you can register." % WAIT_MINUTES) 125 | del c, r 126 | 127 | if 'command' in dir(): 128 | client.last_activity = str(time.time())[:10] 129 | cmd=params 130 | args='' 131 | if ' ' in params: 132 | cmd,args = params.split(' ',1) 133 | cmd,args=(cmd.lower(),args.lower()) 134 | if cmd == 'help' or not cmd: 135 | if not args: 136 | nsmsg("\x02/NICKSERV\x0F allows you to \"register\" a nickname") 137 | nsmsg("and prevent other people from using it. The following") 138 | nsmsg("commands allow for registration and maintenance of") 139 | nsmsg("nicknames; to use them, type \x02/NICKSERV \x1Fcommand\x0F.") 140 | nsmsg("For more information on a specific command, type") 141 | nsmsg("\x02/NICKSERV HELP \x1Fcommand\x0F.") 142 | nsmsg("") 143 | nsmsg(" REGISTER Register a nickname") 144 | nsmsg(" IDENTIFY Identify yourself with your password") 145 | nsmsg(" PASSWORD Set your nickname password") 146 | nsmsg(" DROP Cancel the registration of a nickname") 147 | nsmsg(" GHOST Disconnects a \"ghost\" IRC session using your nick") 148 | nsmsg(" INFO Displays information about a given nickname") 149 | nsmsg(" LIST List all registered nicknames that match a given pattern") 150 | nsmsg(" LOGOUT Reverses the effect of the IDENTIFY command") 151 | if client.oper: 152 | nsmsg(" EXPIRE Manually purge expired registrations") 153 | nsmsg("") 154 | nsmsg("Nicknames that are not used anymore are subject to") 155 | nsmsg("the automatic expiration, i.e. they will be deleted") 156 | nsmsg("after %i days if not used." % MAX_DAYS_UNPRESENT) 157 | nsmsg("") 158 | nsmsg("\x02NOTICE:\x0F This service is intended to provide a way for") 159 | nsmsg("IRC users to ensure their identity is not compromised.") 160 | nsmsg("It is \x02NOT\x0F intended to facilitate \"stealing\" of") 161 | nsmsg("nicknames or other malicious actions. Abuse of NickServ") 162 | nsmsg("will result in, at minimum, loss of the abused") 163 | nsmsg("nickname(s).") 164 | 165 | elif args == 'register': 166 | nsmsg("Syntax: \x02REGISTER \x0F") 167 | nsmsg("Up to \x02%i\x0F nicknames may be registered per IP address." % MAX_NICKS) 168 | nsmsg("Nicknames will be forgotten about after %i days if not used." % MAX_DAYS_UNPRESENT) 169 | 170 | elif args == 'identify': 171 | nsmsg("Syntax: \x02IDENTIFY \x1Fpassword\x0F") 172 | nsmsg("") 173 | nsmsg("Tells NickServ that you are really the owner of this") 174 | nsmsg("nick. The password should be the same one you sent with") 175 | nsmsg("the \x02REGISTER\x0F command.") 176 | 177 | elif args == 'drop': 178 | nsmsg("Syntax: \x02DROP \x1Fnickname\x0F \x02\x1Fpassword\x0F") 179 | nsmsg("") 180 | nsmsg("Drops your nickname from the NickServ database. A nick") 181 | nsmsg("that has been dropped is free for anyone to re-register.") 182 | nsmsg("") 183 | if client.oper: 184 | nsmsg("IRC Operators may supply anything as a password.") 185 | 186 | elif args == 'ghost': 187 | nsmsg("Syntax: \x02GHOST \x1Fnickname\x0F \x02\x1Fpassword\x0F") 188 | nsmsg("") 189 | nsmsg("Terminates a \"ghost\" IRC session using your nick. A") 190 | nsmsg("\"ghost\" session is one which is not actually connected,") 191 | nsmsg("but which the IRC server believes is still online for one") 192 | nsmsg("reason or another. Typically, this happens if your") 193 | nsmsg("computer crashes or your Internet or modem connection") 194 | nsmsg("goes down while you're on IRC. This command will also") 195 | nsmsg("disconnect any other users attempting to use a nickname") 196 | nsmsg("you own.") 197 | 198 | elif args == 'list': 199 | nsmsg("Syntax: \x02LIST \x1Fpattern\x0F") 200 | nsmsg("") 201 | nsmsg("Lists all registered nicknames which match the given") 202 | nsmsg("pattern, in \x1Fnick!user@host\x0F format.") 203 | nsmsg("") 204 | nsmsg("Examples:") 205 | nsmsg("") 206 | nsmsg(" \x02LIST *!user@foo.com\x0F") 207 | nsmsg(" Lists all nicks owned by \x02user@foo.com\x0F.") 208 | nsmsg(" \x02LIST *Bot*!*@*\x0F") 209 | nsmsg(" Lists all registered nicks with \x02Bot\x0F in their") 210 | nsmsg(" names (case insensitive).") 211 | nsmsg(" \x02LIST *!*@*.bar.org\x0F") 212 | nsmsg(" Lists all nicks owned by users in the \x02bar.org\x0F") 213 | nsmsg(" domain.") 214 | 215 | elif args == 'info': 216 | nsmsg("Syntax: \x02INFO \x1Fnickname\x0F") 217 | nsmsg("") 218 | nsmsg("Displays information about the given nickname, such as") 219 | nsmsg("the ident registered with, whether the user is online, and") 220 | nsmsg("when the nickname was last logged into.") 221 | 222 | elif args == 'logout': 223 | nsmsg("Syntax: \x02LOGOUT\x0F") 224 | nsmsg("") 225 | nsmsg("This reverses the effect of the \x02IDENTIFY\x0F command, i.e.") 226 | nsmsg("make you not recognized as the real owner of the nick") 227 | nsmsg("anymore. Note, however, that you won't be asked to reidentify") 228 | nsmsg("yourself.") 229 | 230 | elif args == 'password': 231 | nsmsg("Syntax: \x02PASSWORD \x1Fnew-password\x0F") 232 | nsmsg("") 233 | nsmsg("Sets the password for a nickname, providing you are") 234 | nsmsg("already identified with NickServ. If you have forgotten") 235 | nsmsg("your password and need it resetting, you will need to") 236 | nsmsg("speak to an IRC Operator.") 237 | if client.oper: 238 | nsmsg("") 239 | nsmsg("Syntax: \x02PASSWORD \x1Fnick\x0F \x02\x1Fnew-password\x0F") 240 | nsmsg("IRC Operators may redefine passwords at will.") 241 | 242 | elif args == 'expire' and client.oper: 243 | nsmsg("Syntax: \x02EXPIRE\x0F") 244 | nsmsg("") 245 | nsmsg("This iterates through every record in the database") 246 | nsmsg("and purges records that haven't been used for over") 247 | nsmsg("MAX_DAYS_UNPRESENT days, which is currently set to \x02%i\x0F." % MAX_DAYS_UNPRESENT) 248 | nsmsg("") 249 | nsmsg("Expiration of a nickname is checked when the \x02IDENTIFY\x0F") 250 | nsmsg("command is used, however, nicknames may never be claimed") 251 | nsmsg("at all. This command may take a few seconds to work over ") 252 | nsmsg("large databases.") 253 | 254 | else: 255 | nsmsg("Unknown help topic.") 256 | 257 | elif cmd == 'register': 258 | if 'R' in client.modes: 259 | nsmsg("You are already identified.") 260 | else: 261 | t = divmod(int(client.last_activity) - int(client.connected_at), 60) 262 | if not args: nsmsg("Usage: \x02/NICKSERV REGISTER \x1Fpassword\x0F") 263 | elif t[0] < WAIT_MINUTES: 264 | nsmsg("You must be connected for at least %i minutes before you can register." % WAIT_MINUTES) 265 | else: 266 | password = hashlib.sha1(args.encode('utf-8')).hexdigest() 267 | db = cache['db'] 268 | c = db.cursor() 269 | r = None 270 | if MAX_RECORDS: 271 | c.execute("select count(*) from %s" % TABLE) 272 | r = c.fetchone() 273 | if r['count(*)'] >= MAX_RECORDS: 274 | nsmsg("The NickServ database is full.") 275 | raise NSError("MAX_RECORDS has been reached") 276 | c.execute("SELECT * FROM %s WHERE nick=?" % TABLE, (client.nick,)) 277 | r = c.fetchone() 278 | if r: nsmsg("Nickname \x02%s\x0F is already registered." % escape(client.nick)) 279 | else: 280 | c.execute("SELECT * FROM %s WHERE ip=?" % TABLE, (client.host[0],)) 281 | r = c.fetchall() 282 | if len(r) >= MAX_NICKS: 283 | nsmsg("You already have %i nicknames registered to this ip address:" % MAX_NICKS) 284 | for i in r: nsmsg("\x02%s\x0F, %s" % (i['nick'],fmt_timestamp(i['time_reg']))) 285 | del i 286 | else: 287 | t = time.time() 288 | db.execute("INSERT INTO %s VALUES (?,?,?,?,?,?)" % \ 289 | TABLE, (client.host[0], client.client_ident(True), client.nick, password, t,t,)) 290 | db.commit() 291 | nsmsg("Registered \x02%s\x0F to \x02%s\x0F." % (client.nick,client.client_ident(True))) 292 | if 'R' in client.supported_modes: 293 | client.modes['R'] = 1 294 | client.broadcast(client.nick,':%s MODE %s +R' % (NS_IDENT,client.nick)) 295 | del db,c,r 296 | 297 | elif cmd == 'identify': 298 | if 'R' in client.modes: 299 | nsmsg("You are already identified.") 300 | else: 301 | if not args: nsmsg("Usage: \x02/NICKSERV IDENTIFY \x1Fpassword\x0F") 302 | else: 303 | warn = ":%s NOTICE * :\x034WARNING\x0F: %s tried to authenticate with an incorrect password." % \ 304 | (NS_IDENT, client.nick) 305 | password = hashlib.sha1(args.encode('utf-8')).hexdigest() 306 | db = cache['db'] 307 | c = db.cursor() 308 | c.execute("SELECT * FROM %s WHERE nick=?" % TABLE, (client.nick,)) 309 | r = c.fetchone() 310 | if not r: nsmsg("Your nick isn't registered.") 311 | else: 312 | if is_expired(r['time_use']): 313 | c.execute("DELETE FROM %s WHERE nick=?" % TABLE, (client.nick,)) 314 | db.commit() 315 | nsmsg("\x02%s\x0F has expired due to inactivity." % nick) 316 | client.broadcast('umode:W',':%s NOTICE * :\x02%s\x0F has expired.' % \ 317 | (NS_IDENT, client.nick, client.nick)) 318 | elif r['password'] == password: 319 | c.execute("UPDATE %s SET time_use = %f WHERE nick=?" % \ 320 | (TABLE, time.time()), (client.nick,)) 321 | db.commit() 322 | if 'R' in client.supported_modes: 323 | client.modes['R'] = 1 324 | nsmsg("Authentication successful for \x02%s\x0F." % client.nick) 325 | client.broadcast(client.nick,':%s MODE %s +R' % (NS_IDENT,client.nick)) 326 | else: 327 | nsmsg("Incorrect password.") 328 | client.broadcast('umode:W', warn) 329 | del warn,db,c,r 330 | 331 | elif cmd == 'password': 332 | if not args or (' ' in args and not client.oper): 333 | nsmsg("Usage: \x02/NICKSERV PASSWORD \x1Fnew-password\x0F") 334 | # Opers: /ns password target-nick new-password 335 | elif client.oper and ' ' in args: 336 | nick,password = args.split() 337 | password = hashlib.sha1(password).hexdigest() 338 | db = cache['db'] 339 | c = db.cursor() 340 | c.execute("SELECT * FROM %s WHERE nick=?" % TABLE, (nick,)) 341 | r = c.fetchone() 342 | if not r: nsmsg("\x02%s\x0F isn't registered." % nick) 343 | else: 344 | c.execute("UPDATE %s SET password=? WHERE nick=?" % \ 345 | TABLE, (password, nick)) 346 | nsmsg("Changed password for \x02%s\x0F." % escape(nick)) 347 | del db,c,r 348 | else: 349 | password = hashlib.sha1(args).hexdigest() 350 | db = cache['db'] 351 | c = db.cursor() 352 | c.execute("SELECT * FROM %s WHERE nick=?" % \ 353 | TABLE, (client.nick,)) 354 | r = c.fetchone() 355 | if not r: nsmsg("\x02%s\x0F isn't registered." % nick) 356 | else: 357 | if not 'R' in client.modes: 358 | nsmsg("You must be identified.") 359 | nsmsg("Contact an IRC Operator for help retrieving accounts.") 360 | client.broadcast('umode:W', ':%s NOTICE * :\x02%s\x0F needs help resetting their password.' % (NS_IDENT, client.nick)) 361 | client.broadcast('umode:W', ':%s NOTICE * :%s is connecting from \x02%s\x0F and registered from \x02%s\x0F.' % \ 362 | (NS_IDENT, client.nick,client.host[0],r['ip'])) 363 | else: 364 | c.execute("UPDATE %s SET password=? WHERE nick=?" % \ 365 | TABLE, (password, client.nick)) 366 | nsmsg("Changed password for \x02%s\x0F." % escape(client.nick)) 367 | del db,c,r 368 | 369 | elif cmd == 'ghost': 370 | if not args or not ' ' in args: nsmsg("Usage: \x02/NICKSERV GHOST \x1Fnick\x0F \x02\x1Fpassword\x0F") 371 | else: 372 | nick,password = args.split() 373 | password = hashlib.sha1(password).hexdigest() 374 | user = client.server.clients.get(nick) 375 | if not user: nsmsg("Unknown nick.") 376 | else: 377 | db = cache['db'] 378 | c = db.cursor() 379 | c.execute("SELECT * FROM %s WHERE nick=?" % \ 380 | TABLE, (nick,)) 381 | r = c.fetchone() 382 | if not r: nsmsg("\x02%s\x0F isn't registered." % client.nick) 383 | else: 384 | if r['password'] != password: 385 | nsmsg("Incorrect password.") 386 | warn = ":%s NOTICE * :\x034WARNING\x0F: %s tried to ghost %s with an incorrect password." % \ 387 | (NS_IDENT,client.nick,nick) 388 | client.broadcast('umode:W', warn) 389 | else: 390 | user.finish(':%s QUIT :GHOST command used by %s.' % (user.client_ident(True), client.nick)) 391 | nsmsg("Client with your nickname has been killed.") 392 | del db,c,r 393 | 394 | elif cmd == 'list': 395 | if not args: nsmsg("Usage \x02/NICKSERV LIST \x1Fpattern\x0F") 396 | else: 397 | if not client.oper and 'R' not in client.modes: 398 | nsmsg("Access denied.") 399 | else: 400 | args = escape(args.replace('*','%')) 401 | db = cache['db'] 402 | c = db.cursor() 403 | c.execute("SELECT * FROM %s WHERE ident LIKE ?" % TABLE,(args,)) 404 | t = c.fetchall() 405 | nsmsg_list(t) 406 | del db,c,t 407 | 408 | elif cmd == 'logout': 409 | if 'R' in client.modes: 410 | del client.modes['R'] 411 | client.broadcast(client.nick, ":%s MODE %s -R" % (NS_IDENT,client.nick)) 412 | nsmsg("Successfully logged out.") 413 | else: 414 | nsmsg("You're not logged in.") 415 | 416 | elif cmd == 'info': 417 | if not args: nsmsg("Usage: \x02/NICKSERV INFO \x1FNICK\x0F") 418 | else: 419 | db =cache['db'] 420 | c = db.cursor() 421 | c.execute("SELECT * FROM %s WHERE nick=?" % TABLE, (args,)) 422 | r = c.fetchone() 423 | if not r: nsmsg("\x02%s\x0F isn't registered." % args) 424 | else: 425 | nsmsg("%s is %s" % (args,r['ident'])) 426 | if client.oper: 427 | nsmsg("Registered from: %s" % r['ip']) 428 | user = client.server.clients.get(r['nick']) 429 | if user: 430 | nsmsg(" Is online from: %s" % user.client_ident(True).split("!")[1]) 431 | nsmsg(" Last login: %s" % fmt_timestamp(r['time_use'])) 432 | if user: 433 | nsmsg(" Last seen time: %s" % \ 434 | fmt_timestamp(user.last_activity)) 435 | nsmsg("Time registered: %s" % fmt_timestamp(r['time_reg'])) 436 | del db,c,r 437 | 438 | elif cmd == 'drop': 439 | if not args or not ' ' in args: nsmsg("Usage: \x02/NICKSERV DROP \x1Fnick\x0F \x02\x1Fpassword\x0F") 440 | else: 441 | nick,password = args.split() 442 | password = hashlib.sha1(password).hexdigest() 443 | db = cache['db'] 444 | c = db.cursor() 445 | c.execute("SELECT * FROM %s WHERE nick=?" % \ 446 | TABLE, (nick,)) 447 | r = c.fetchone() 448 | if not r: nsmsg("\x02%s\x0F isn't registered." % nick) 449 | else: 450 | # IRC Operators can supply anything as a password. 451 | if client.oper or (r['password'] == password): 452 | c.execute("DELETE FROM %s WHERE nick=?" % \ 453 | TABLE, (nick,)) 454 | db.commit() 455 | nsmsg("\x02%s\x0F has been dropped." % nick) 456 | client.broadcast('umode:W',':%s NOTICE * :%s has dropped the nick \x02%s\x0F.' % \ 457 | (NS_IDENT, client.nick, nick)) 458 | user = client.server.clients.get(nick) 459 | if user and 'R' in user.modes: 460 | del user.modes['R'] 461 | client.broadcast(user.nick, ":%s MODE %s -R" % (NS_IDENT,client.nick)) 462 | else: 463 | nsmsg("Incorrect password.") 464 | warn = ":%s NOTICE * :\x034WARNING\x0F: %s tried to DROP %s with an incorrect password." % \ 465 | (NS_IDENT, client.nick, nick) 466 | client.broadcast('umode:W', warn) 467 | del db,c,r 468 | 469 | elif cmd == "expire": 470 | if not client.oper: 471 | nsmsg("Unknown command.") 472 | nsmsg("Use \x02/NICKSERV HELP\x0F for a list of available commands.") 473 | else: 474 | db = cache['db'] 475 | c = db.cursor() 476 | c.execute("SELECT * FROM %s" % TABLE) 477 | t = c.fetchall() 478 | for r in t: 479 | if is_expired(r['time_use']): 480 | c.execute("DELETE FROM %s WHERE nick=?" % TABLE, (r['nick'],)) 481 | nsmsg("\x02%s\x0F has expired due to inactivity." % r['nick']) 482 | client.broadcast('umode:W',':%s NOTICE * :%s expired \x02%s\x0F.' % (NS_IDENT, client.nick, r['nick'])) 483 | db.commit() 484 | nsmsg("All registrations have been cycled through.") 485 | del db,c,r,t 486 | 487 | elif cmd == "debug": 488 | if client.oper: 489 | db = cache['db'] 490 | c = db.cursor() 491 | c.execute("select count(*) from %s" % TABLE) 492 | r = c.fetchone() 493 | nsmsg(r['count(*)']) 494 | nsmsg("You are likely to be eaten by a \x02\x034Grue\x0F.") 495 | 496 | else: 497 | nsmsg("Unknown command.") 498 | nsmsg("Use \x02/NICKSERV HELP\x0F for a list of available commands.") 499 | 500 | -------------------------------------------------------------------------------- /scripts/capabilities.py: -------------------------------------------------------------------------------- 1 | if 'init' in dir(): 2 | provides = "command:cap:Simple response to IRCv3 capabilities requests." 3 | elif "client" in dir() and client.nick: 4 | client.broadcast( 5 | client.nick, 6 | ": This server doesn't currently implement the IRC v3 capabilities model.") 7 | -------------------------------------------------------------------------------- /scripts/disect.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | if 'init' in dir(): 3 | provides = "command:disect:Displays information about channel and user objects." 4 | else: 5 | if not client.oper: client.broadcast(client.nick, ': IRCops Only.') 6 | else: 7 | if params: 8 | if params.startswith('#'): 9 | channel = client.server.channels.get(params) 10 | if channel: 11 | message = 'Channel %s: %s\n' % (channel.name, repr(channel)) 12 | if channel.topic: 13 | message += 'Topic: %s\n' % channel.topic 14 | message += 'Clients: %s\n' % pprint.pformat(channel.clients) 15 | message += 'Supported modes:\n' 16 | for m, d in channel.supported_modes.items(): 17 | message += " %s %s\n" % (m, d) 18 | message += 'Active mode(s): %s\n' % str(channel.modes) 19 | for line in message.split('\n'): 20 | client.broadcast(client.nick, ': %s' % line) 21 | else: 22 | c = client.server.clients.get(params) 23 | if c: 24 | message = 'Client %s: %s\n' % (c.nick, repr(c)) 25 | if c.vhost: 26 | message += 'Vhost: %s\n' % c.vhost 27 | if c.user: 28 | message += 'User: %s\n' % c.user 29 | if c.host: 30 | message += 'host: %s\n' % str(c.host) 31 | message += 'Channels: %s\n' % str(c.channels) 32 | message += 'Supported modes:\n' 33 | for m, d in c.supported_modes.items(): 34 | message += " %s %s\n" % (m, d) 35 | message += 'Active mode(s): %s\n' % str(c.modes) 36 | for line in message.split('\n'): 37 | client.broadcast(client.nick, ': %s' % line) 38 | else: 39 | import sys, resource 40 | rusage_denom = 1024 41 | if sys.platform == 'darwin': 42 | # ... it seems that in OSX the output is different units ... 43 | rusage_denom = rusage_denom * rusage_denom 44 | mem = resource.getrusage( 45 | resource.RUSAGE_SELF).ru_maxrss / rusage_denom 46 | client.broadcast(client.nick, 47 | ": Currently consuming %iMb of memory." % int(mem)) 48 | client.broadcast(client.nick, ':%s' % client.server.channels) 49 | client.broadcast(client.nick, ':%s' % client.server.clients) 50 | -------------------------------------------------------------------------------- /scripts/news.py: -------------------------------------------------------------------------------- 1 | # news.py for Psyrcd. 2 | # Implements channel mode +news 3 | # This permits users to read from a live Emissary instance in-situ, including 4 | # searching through articles, reviewing feed status and reading articles 5 | # over IRC. All of the usual Emissary endpoints are supported. 6 | # 7 | # /operserv scripts load news.py 8 | # /news feeds?per_page=5 9 | # /news articles/search/photosynthesis 10 | # /news read ad8aaf28-0d1d-48c3-a3b9-85bdcaa9ee5b 11 | # 12 | # Luke Brooks, 2015 13 | # MIT License 14 | # 15 | # Emissary can be found at https://github.com/LukeB42/Emissary 16 | # 17 | 18 | # Colour key: 19 | # \x02 bold 20 | # \x03 coloured text 21 | # \x1D italic text 22 | # \x0F colour reset 23 | # \x16 reverse colour 24 | # \x1F underlined text 25 | import datetime as dt 26 | logging = cache['config']['logging'] 27 | API_KEY = "" 28 | EMISSARY_HOST = "localhost:6362" 29 | EM_IDENT = "Emissary!services@" + cache['config']['SRV_DOMAIN'] 30 | 31 | def emsg(msg, indent=0): 32 | if indent: 33 | client.broadcast(client.nick, ":%s NOTICE * :%s%s" % \ 34 | (EM_IDENT, " " * indent, msg)) 35 | else: 36 | client.broadcast(client.nick, ":%s NOTICE * :%s" % \ 37 | (EM_IDENT, msg)) 38 | 39 | def transmit_article_titles(res): 40 | if 'message' in res[0]: 41 | emsg(res[0]['message']) 42 | elif res[1] == 200: 43 | for d in res[0]['data']: 44 | if not 'feed' in d: 45 | d['feed'] = '' 46 | if not '://' in d['url']: 47 | d['url'] = "http://" + d['url'] 48 | if d['content_available']: 49 | emsg("%s: \x037%s\x0F" % (d['feed'], d['title'])) 50 | else: 51 | emsg("%s: %s" % (d['feed'], d['title'])) 52 | if 'created' in d: 53 | emsg("%s %s \x0314%s\x0F" % \ 54 | (str(dt.datetime.fromtimestamp(int(d['created']))\ 55 | .strftime('%H:%M:%S %d/%m/%Y')), 56 | d['uid'], d['url'])) 57 | else: 58 | emsg("Error.") 59 | 60 | def transmit_feed_objects(res): 61 | (resp, status) = res 62 | if 'data' in resp.keys(): 63 | resp = resp['data'] 64 | if type(resp) == list: 65 | for fg in resp: 66 | transmit_feed_group(fg) 67 | else: 68 | transmit_feed_group(resp) 69 | 70 | def transmit_feed_group(resp): 71 | if 'message' in resp: 72 | emsg(resp['message']) 73 | elif 'feeds' in resp and 'created' in resp: 74 | created = dt.datetime.fromtimestamp(int(resp['created'])).strftime('%H:%M:%S %d/%m/%Y') 75 | if 'active' in resp and resp['active'] == True: 76 | emsg("\x033%s\x0F (created %s)" % (resp['name'], created)) 77 | else: 78 | emsg("%s (created %s)" % (resp['name'], created)) 79 | for feed in resp['feeds']: 80 | transmit_feed(feed) 81 | else: 82 | transmit_feed(resp) 83 | 84 | def transmit_feed(feed): 85 | if 'created' in feed: 86 | created = dt.datetime.fromtimestamp(int(feed['created'])).strftime('%H:%M:%S %d/%m/%Y') 87 | if 'active' in feed and feed['active'] == True: 88 | emsg("\x033%s\x0F" % feed['name'], 2) 89 | else: 90 | emsg("%s" % feed['name'], 2) 91 | emsg(" URL: %s" % feed['url'], 2) 92 | if feed['running'] == True: 93 | emsg(" Running: \x033%s\x0F" % (feed['running']), 2) 94 | elif feed['running'] == False: 95 | emsg(" Running: \x031%s\x0F" % (feed['running']), 2) 96 | else: 97 | emsg(" Running: \x0314Unknown\x0F", 2) 98 | emsg(" Created: %s" % created, 2) 99 | emsg(" Schedule: %s" % feed['schedule'], 2) 100 | emsg("Article count: %s" % "{:,}".format(feed['article_count']), 2) 101 | 102 | if 'init' in dir(): 103 | provides = "command:news:Provides a command interface to Emissary." 104 | if init: 105 | if not API_KEY: 106 | logging.error("API key undefined in news.py.") 107 | # client.broadcast("umode:W", ": There's no API key defined in news.py.") 108 | if not 'client' in cache: 109 | from emissary.client import Client 110 | client = Client(API_KEY,'https://%s/v1/' % EMISSARY_HOST, 111 | verify=False, timeout=5.5) 112 | cache['client'] = client 113 | else: 114 | if 'client' in cache: 115 | del cache['client'] 116 | 117 | if 'command' in dir(): 118 | cancel = True 119 | params = params.split() 120 | c = cache['client'] 121 | 122 | if params[0].startswith('articles'): 123 | res = c.get(params[0]) 124 | if res: 125 | transmit_article_titles(res) 126 | 127 | elif params[0].startswith('feeds'): 128 | params = ' '.join(params[0:]) 129 | res = c.get(params) 130 | if res[1] != 200: emsg("Error.") 131 | if '/search/' in params or '/articles' in params: 132 | transmit_article_titles(res) 133 | else: 134 | transmit_feed_objects(res) 135 | 136 | elif params[0] == 'read': 137 | (resp, status) = c.get('articles/' + params[1]) 138 | if status != 200: 139 | emsg("Error status %i" % status) 140 | else: 141 | if not resp['content']: 142 | emsg("No content for %s" % resp['url']) 143 | else: 144 | title = resp['title'] 145 | url = resp['url'] 146 | created = resp['created'] 147 | content = resp['content'] 148 | emsg(title) 149 | emsg(url) 150 | emsg("") 151 | for line in content.split('\n'): 152 | emsg(line) 153 | 154 | if 'c' in dir(): 155 | del c 156 | 157 | del API_KEY 158 | -------------------------------------------------------------------------------- /scripts/proctitle.py: -------------------------------------------------------------------------------- 1 | if 'init' in dir(): 2 | provides = "command:spt:Sets process title." 3 | try: 4 | import setproctitle 5 | setproctitle.setproctitle('psyrcd') 6 | except: 7 | pass 8 | -------------------------------------------------------------------------------- /scripts/replace.py: -------------------------------------------------------------------------------- 1 | # replace.py for Psyrcd. 2 | # Implements channel mode +G. 3 | # 4 | # This channel mode replaces phrases in privmsg lines. 5 | # Usage: /quote mode #channel +G:from_phrase,to_phrase,... 6 | # /quote mode #channel -G:phrase1,... 7 | # 8 | # Luke Brooks, 2015 9 | # MIT License 10 | # 11 | 12 | if 'init' in dir(): 13 | provides = "cmode:G:Substitutes phrases." 14 | 15 | if 'display' in dir(): 16 | if client.oper: 17 | phrases = str(channel.modes["G"]) 18 | output = "phrase%s: %s" % ('s' if len(phrases) > 1 else '', phrases) 19 | 20 | if 'setting_mode' in dir(): 21 | if set: 22 | if not "G" in channel.modes or \ 23 | not isinstance(channel.modes["G"], list): 24 | channel.modes["G"] = [] 25 | 26 | # Ensure only tuple pairs are in the structure for this mode 27 | f = lambda x: isinstance(x, tuple) 28 | channel.modes["G"] = list(filter(f, channel.modes["G"])) 29 | 30 | # Check args are an even number 31 | if not len(args) % 2: 32 | 33 | # Turn into tuples 34 | def chunks(l, n): 35 | for i in range(0, len(l), n): 36 | yield l[i:i+n] 37 | 38 | args = [tuple(x) for x in chunks(args, 2)] 39 | 40 | new_phrases = [] 41 | current_phrases = [x[0] for x in channel.modes["G"]] 42 | 43 | for new_phrase_pair in args: 44 | if new_phrase_pair[0] in current_phrases: 45 | continue 46 | new_phrases.append(new_phrase_pair) 47 | 48 | channel.modes["G"].extend(new_phrases) 49 | 50 | cancel = "" 51 | response = ":%s MODE %s +G: %s" % \ 52 | (client.client_ident(True), channel.name, str(new_phrases)) 53 | client.broadcast(channel.name, response) 54 | 55 | else: # Handle removing individual phrases 56 | from copy import deepcopy 57 | removed_phrases = [] 58 | current_pairs = deepcopy(channel.modes["G"]) 59 | for phrase in args: 60 | for i, pair in enumerate(current_pairs): 61 | if phrase.lower() == pair[0].lower(): 62 | del channel.modes["G"][i] 63 | removed_phrases.append(phrase.lower()) 64 | if args: 65 | cancel = "" 66 | response = ":%s MODE %s -G: %s" % \ 67 | (client.client_ident(True), channel.name, str(removed_phrases)) 68 | client.broadcast(channel.name, response) 69 | 70 | # Replace phrases when encountered 71 | if 'func' in dir() and func.__name__ == "handle_privmsg": 72 | params = params.split(":",1) 73 | line = params[1].split() 74 | for arg in channel.modes["G"]: 75 | if isinstance(arg, tuple): 76 | for i, phrase in enumerate(line): 77 | if phrase.lower() == arg[0].lower(): 78 | line[i] = arg[1] 79 | params[1] = ' '.join(line) 80 | params = ':'.join(params) 81 | 82 | 83 | -------------------------------------------------------------------------------- /scripts/sortition.py: -------------------------------------------------------------------------------- 1 | # sortition.py for Psyrcd. 2 | # Implements channel mode +sortition 3 | # 4 | # Usage: /mode #chan +sortition:5 5 | # Where 5 denotes an interval of five minutes. 6 | # 7 | # This channel mode de-ops active channel operators and selects a random 1/4 of 8 | # the channel to be operators every n minutes. 9 | # https://en.wikipedia.org/wiki/Sortition 10 | # 11 | # Luke Brooks, 2015 12 | # MIT License 13 | 14 | # Colour key: 15 | # \x02 bold 16 | # \x03 coloured text 17 | # \x1D italic text 18 | # \x0F colour reset 19 | # \x16 reverse colour 20 | # \x1F underlined text 21 | 22 | import time 23 | import random 24 | MODE_NAME = "sortition" 25 | SRV_DOMAIN = cache['config']['SRV_DOMAIN'] 26 | 27 | if 'init' in dir(): 28 | provides = "cmode:%s:Implements administrative sortition." % MODE_NAME 29 | 30 | if 'display' in dir(): 31 | # The structure in channel.modes is a list where 32 | # the zeroth element is the duration between changes, in minutes, 33 | # the first element is the timestamp of the previous change and 34 | # the second element is the cloaked hostmask of the user who set 35 | # the mode initially. This prevents randomly elected chan ops from 36 | # doing away with sortition. 37 | # Duration 38 | d = int(channel.modes[MODE_NAME][0]) * 60 39 | # Elapsed 40 | e = int(time.time()) - channel.modes[MODE_NAME][1] 41 | # Minutes to election 42 | m = int((d - e) / 60) 43 | if m == 0: 44 | m += 1 45 | output = "(Next election in %i minute%s.)" % (m, 's' if m > 1 else '') 46 | 47 | # Randomly elected operators can alter the duration but can't remove the mode. 48 | if 'setting_mode' in dir(): 49 | if set: 50 | if not args: 51 | message = "Please specify a duration in minutes. Eg: +%s:20" % MODE_NAME 52 | client.broadcast(client.nick, ':%s NOTICE %s :%s\n' % \ 53 | (SRV_DOMAIN, client.nick, message)) 54 | cancel = True 55 | else: 56 | # Duration in minutes, last change, clients' cloaked ident 57 | channel.modes[MODE_NAME] = [int(args[0]), 0, client.client_ident(True)] 58 | else: 59 | ident = channel.modes[MODE_NAME][2] 60 | if client.oper or client.client_ident(True) == ident: 61 | del channel.modes[MODE_NAME] 62 | else: 63 | message = "You must be an IRC Operator or %s to unset +%s from %s." % \ 64 | (ident, MODE_NAME, channel.name) 65 | client.broadcast(client.nick, ':%s NOTICE %s :%s\n' % \ 66 | (SRV_DOMAIN, client.nick, message)) 67 | cancel = True 68 | 69 | if 'func' in dir(): 70 | duration = int(channel.modes[MODE_NAME][0]) * 60 71 | then = channel.modes[MODE_NAME][1] 72 | now = int(time.time()) 73 | if (now-then) >= duration: 74 | channel.modes[MODE_NAME][1] = now 75 | # Select a new administration 76 | count = int(len(channel.clients) / 4) 77 | if count == 0: 78 | count += 1 79 | 80 | administration = random.sample(channel.clients, count) 81 | 82 | # Grab a client object to broadcast de-op messages with 83 | for client in channel.clients: 84 | break 85 | 86 | # Remove existing channel operators 87 | for o in channel.ops: 88 | for n in o: 89 | client.broadcast(channel.name, ':%s MODE %s: -qaohv %s' % \ 90 | (SRV_DOMAIN, channel.name, n)) 91 | del o[:] 92 | 93 | # Instate the new administration 94 | for mode in ['q', 'a', 'o', 'h']: 95 | if mode in channel.supported_modes and mode in channel.modes: 96 | for c in administration: 97 | channel.modes[mode].append(c.nick) 98 | c.broadcast(channel.name, ':%s MODE %s: +%s %s' % \ 99 | (SRV_DOMAIN, channel.name, mode, c.nick)) 100 | break 101 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # _*_ coding: utf-8 _*_ 3 | import os 4 | import sys 5 | import shutil 6 | from setuptools import setup, find_packages 7 | 8 | banner = """ 9 | ██████╗ ███████╗██╗ ██╗██████╗ ███████╗██████╗ ███╗ ██╗███████╗████████╗██╗ ██████╗███████╗ 10 | ██╔══██╗██╔════╝╚██╗ ██╔╝██╔══██╗██╔════╝██╔══██╗████╗ ██║██╔════╝╚══██╔══╝██║██╔════╝██╔════╝ 11 | ██████╔╝███████╗ ╚████╔╝ ██████╔╝█████╗ ██████╔╝██╔██╗ ██║█████╗ ██║ ██║██║ ███████╗ 12 | ██╔═══╝ ╚════██║ ╚██╔╝ ██╔══██╗██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ██║ ██║██║ ╚════██║ 13 | ██║ ███████║ ██║ ██████╔╝███████╗██║ ██║██║ ╚████║███████╗ ██║ ██║╚██████╗███████║██╗ 14 | ╚═╝ ╚══════╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝ 15 | """ 16 | 17 | def main(): 18 | if len(sys.argv) < 2: 19 | print "Accepted arguments are: install, uninstall" 20 | raise SystemExit 21 | 22 | if sys.argv[1].lower() == "install": 23 | install() 24 | 25 | if sys.argv[1].lower() == "uninstall": 26 | uninstall() 27 | 28 | def install(): 29 | print banner 30 | # install deps and module 31 | data_files = () 32 | setup(name='psyrcd', 33 | version="psyrcd-21", 34 | description='A pure-python IRCD', 35 | author='Luke Brooks', 36 | author_email='luke@psybernetics.org', 37 | url='http://src.psybernetics.org', 38 | download_url = 'https://github.com/LukeB42/psyrcd/tarball/0.0.1', 39 | data_files = data_files, 40 | packages=[], 41 | include_package_data=True, 42 | install_requires=[ 43 | ], 44 | keywords=["irc", "ircd"] 45 | ) 46 | 47 | print "Moving psyrcd.py to /usr/bin/psyrcd" 48 | shutil.copyfile("./psyrcd.py", "/usr/bin/psyrcd") 49 | 50 | print "Making /usr/bin/psyrcd executable." 51 | os.chmod("/usr/bin/psyrcd", 0755) 52 | print 'Check "psyrcd --help" for options.' 53 | 54 | if os.path.exists('/etc/systemd/system'): 55 | init_path = '/etc/systemd/system' 56 | print "Installing systemd service definition." 57 | shutil.copyfile("psyrcd.service", init_path+"/psyrcd.service") 58 | print "Please define a user for the --run-as paramater in %s/psyrcd.service" % init_path 59 | else: 60 | init_path = '/etc/init.d' 61 | print "Installing init script to %s/psyrcd" % init_path 62 | shutil.copyfile("psyrcd", init_path+"/psyrcd") 63 | print "Please define a user for the --run as parameter in %s/psyrcd" % init_path 64 | 65 | def uninstall(): 66 | 67 | # remove systemd unit/init script 68 | if os.path.exists("/etc/systemd/system/psyrcd.service"): 69 | print "Removing /etc/systemd/system/psyrcd.service" 70 | os.remove("/etc/systemd/system/psyrcd.service") 71 | 72 | if os.path.exists("/etc/init.d/psyrcd"): 73 | print "Removing /etc/init.d/psyrcd" 74 | os.remove("/etc/init.d/psyrcd") 75 | 76 | if os.path.exists("/usr/bin/psyrcd"): 77 | print "Removing /usr/bin/psyrcd" 78 | os.remove("/usr/bin/psyrcd") 79 | 80 | if __name__ == "__main__": 81 | if sys.version_info[0] != 2: 82 | print("Python 3.x isn't supported by Psyrcd yet.") 83 | sys.exit(0) 84 | main() 85 | --------------------------------------------------------------------------------