├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------