mirror of
https://github.com/jlu5/PyLink.git
synced 2024-11-01 09:19:23 +01:00
Merge the long-awaited 2.0 branch into master
Merge branch 'devel' Conflicts: RELNOTES.md VERSION classes.py conf.py coremods/control.py coremods/corecommands.py coremods/service_support.py docs/advanced-relay-config.md docs/faq.md example-conf.yml launcher.py plugins/global.py plugins/relay.py plugins/relay_clientbot.py protocols/p10.py utils.py
This commit is contained in:
commit
16ac91a718
@ -1,13 +0,0 @@
|
||||
# This file is used for the configuration of https://codeclimate.com/github/GLolol/PyLink/
|
||||
# You needn't change this if you're running your own copy of PyLink.
|
||||
|
||||
engines:
|
||||
duplication:
|
||||
enabled: true
|
||||
config:
|
||||
languages:
|
||||
- python
|
||||
pep8:
|
||||
enabled: true
|
||||
fixme:
|
||||
enabled: true
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -9,9 +9,11 @@
|
||||
env/
|
||||
build/
|
||||
__pycache__/
|
||||
.idea/
|
||||
*.py[cod]
|
||||
*.bak
|
||||
*~
|
||||
*#
|
||||
*.save*
|
||||
*.db
|
||||
*.pid
|
||||
@ -19,3 +21,4 @@ __pycache__/
|
||||
.eggs
|
||||
*.egg-info/
|
||||
dist/
|
||||
log/
|
||||
|
9
.mailmap
9
.mailmap
@ -1,4 +1,7 @@
|
||||
James Lu <GLolol@overdrivenetworks.com> <GLolol1@hotmail.com>
|
||||
James Lu <GLolol@overdrivenetworks.com> <GLolol@overdrive.pw>
|
||||
Ken Spencer <ken@electrocode.net> <iota@electrocode.net>
|
||||
James Lu <james@overdrivenetworks.com> <GLolol@overdrivenetworks.com>
|
||||
James Lu <james@overdrivenetworks.com> <bitflip3+github@gmail.com>
|
||||
James Lu <james@overdrivenetworks.com> <GLolol1@hotmail.com>
|
||||
James Lu <james@overdrivenetworks.com> <GLolol@overdrive.pw>
|
||||
Ken Spencer <ken@electrocode.net> <kspencer@electrocode.net>
|
||||
Ken Spencer <ken@electrocode.net> <iota@electrocode.net>
|
||||
Ken Spencer <ken@electrocode.net> <iota@e-code.in>
|
||||
|
40
README.md
40
README.md
@ -24,24 +24,30 @@ PyLink and any bundled software are licensed under the Mozilla Public License, v
|
||||
|
||||
**When upgrading between major versions, remember to read the [release notes](RELNOTES.md) for any breaking changes!**
|
||||
|
||||
Please report any bugs you find to the [issue tracker](https://github.com/GLolol/PyLink/issues). Pull requests are open if you'd like to contribute, though new stuff generally goes to the **devel** branch.
|
||||
Please report any bugs you find to the [issue tracker](https://github.com/jlu5/PyLink/issues). Pull requests are open if you'd like to contribute, though new stuff generally goes to the **devel** branch.
|
||||
|
||||
You can also find support via our IRC channel at `#PyLink @ irc.overdrivenetworks.com `([webchat][webchatlink]). Ask your questions and be patient for a response.
|
||||
|
||||
## Installation
|
||||
|
||||
### Pre-requisites
|
||||
* CPython 3.4 or above (other intepreters are untested and unsupported)
|
||||
* A Unix-like operating system: PyLink is actively developed on Linux only, so we cannot guarantee that things will work properly on other systems.
|
||||
|
||||
If you are a developer and want to help make PyLink more portable, patches are highly appreciated. Talk to us!
|
||||
|
||||
### Installing from source
|
||||
|
||||
1) First, make sure the following dependencies are met:
|
||||
|
||||
* Python 3.4+
|
||||
* Setuptools (`pip3 install setuptools`)
|
||||
* PyYAML (`pip3 install pyyaml`)
|
||||
* ircmatch (`pip3 install ircmatch`)
|
||||
* *For password encryption*: Passlib (`pip3 install passlib`)
|
||||
* *For enhanced cron support (by removing stale PID files): psutil (`pip3 install psutil`)
|
||||
* *For the servprotect plugin*: expiringdict (`pip3 install expiringdict`)
|
||||
|
||||
2) Clone the repository: `git clone https://github.com/GLolol/PyLink && cd PyLink`
|
||||
2) Clone the repository: `git clone https://github.com/jlu5/PyLink && cd PyLink`
|
||||
|
||||
3) Pick your branch.
|
||||
* By default you'll be on the **master** branch, which contains the latest stable code. This branch is recommended for production networks that don't require new features or intensive bug fixes as they are developed.
|
||||
@ -56,7 +62,7 @@ You can also find support via our IRC channel at `#PyLink @ irc.overdrivenetwork
|
||||
|
||||
2) Run `pip3 install pylinkirc` to download and install PyLink. pip will automatically resolve dependencies.
|
||||
|
||||
3) Download or copy https://github.com/GLolol/PyLink/blob/master/example-conf.yml for an example configuration.
|
||||
3) Download or copy https://github.com/jlu5/PyLink/blob/master/example-conf.yml for an example configuration.
|
||||
|
||||
### Installing via Ubuntu PPA (14.04/Trusty and above)
|
||||
|
||||
@ -80,11 +86,12 @@ Upon installing the `pylink` package, example configuration and docs will be in
|
||||
These IRCds (in alphabetical order) are frequently tested and well supported. If any issues occur, please file a bug on the issue tracker.
|
||||
|
||||
* [charybdis](https://github.com/charybdis-ircd/charybdis) (3.5+) - module `ts6`
|
||||
- For KLINE support to work, a `shared{}` block should be added for PyLink on all servers.
|
||||
* [InspIRCd](http://www.inspircd.org/) 2.0.x - module `inspircd`
|
||||
- For vHost setting to work, `m_chghost.so` must be loaded.
|
||||
- For vHost setting to work, `m_chghost.so` must be loaded. For ident and realname changing support, `m_chgident.so` and `m_chgname.so` must be loaded respectively.
|
||||
- Supported channel, user, and prefix modes are negotiated on connect, but hotloading modules that change these is not supported. After changing module configuration, it is recommended to SQUIT PyLink to force a protocol renegotiation.
|
||||
* [Nefarious IRCu](https://github.com/evilnet/nefarious2) (2.0.0+) - module `p10`
|
||||
- Note: Both account cloaks (user and oper) and hashed IP cloaks are optionally supported (HOST_HIDING_STYLE settings 0 to 3). Make sure you configure PyLink to match your IRCd settings.
|
||||
- Note: Both account cloaks (user and oper) and hashed IP cloaks are optionally supported (`HOST_HIDING_STYLE` settings 0 to 3). Make sure you configure PyLink to match your IRCd settings.
|
||||
* [UnrealIRCd](https://www.unrealircd.org/) 4.x (4.0.12+) - module `unreal`
|
||||
- UnrealIRCd 4.x before version 4.0.12 suffers from [bug #4890](https://bugs.unrealircd.org/view.php?id=4890) which causes hostname desyncs on servers not directly linked to PyLink (e.g. `pylink<->serverA<->serverB` creates desynced hostnames on server B). This problem is fixed by upgrading your IRCds.
|
||||
- Linking to UnrealIRCd 3.2 servers is only possible when using an UnrealIRCd 4.x server as a hub, with topology such as `pylink<->unreal4<->unreal3.2`. We nevertheless encourage you to upgrade so all your IRCds are running the same version.
|
||||
@ -93,16 +100,29 @@ These IRCds (in alphabetical order) are frequently tested and well supported. If
|
||||
|
||||
Support for these IRCds exist, but are not tested as frequently and thoroughly. Bugs should be filed if there are any issues, though they may not always be fixed in a timely fashion.
|
||||
|
||||
* [beware-ircd](http://ircd.bircd.org/) (1.6.3) - module `p10`
|
||||
- Because bircd disallows BURST after ENDBURST for regular servers, U-lines are required for all PyLink servers. Fortunately, wildcards are supported in U-lines, so you can add something along the lines of `U:<your pylink server>:` and `U:*.relay:` (adjust accordingly for your relay server suffix).
|
||||
- Use `ircd: snircd` as the target IRCd.
|
||||
- Halfops, `sethost` (`+h`), and account-based cloaking (`VHostStyle=1`) are supported. Crypted IPs and static hosts (`VHostStyle` 2 and 3) are NOT.
|
||||
* [ChatIRCd](http://www.chatlounge.net/software) (1.2.x / git master) - module `ts6`
|
||||
- For KLINE support to work, a `shared{}` block should be added for PyLink on all servers.
|
||||
* [Elemental-IRCd](https://github.com/Elemental-IRCd/elemental-ircd) (6.6.x / git master) - module `ts6`
|
||||
- For KLINE support to work, a `shared{}` block should be added for PyLink on all servers.
|
||||
* [InspIRCd](http://www.inspircd.org/) 3.0.x (git master) - module `inspircd`
|
||||
- The same notes for InspIRCd 2.x apply here as well.
|
||||
* [IRCd-Hybrid](http://www.ircd-hybrid.org/) (8.2.x / svn trunk) - module `hybrid`
|
||||
- Note: for host changing support and optimal functionality, a `service{}` block / U-line should be added for PyLink on every IRCd across your network.
|
||||
* [ircd-ratbox](http://www.ratbox.org/) (3.x) - module `ratbox`
|
||||
- Host changing is not supported on ircd-ratbox.
|
||||
- For host changing support and optimal functionality, a `service{}` block / U-line should be added for PyLink on every IRCd across your network.
|
||||
- For KLINE support to work, a `shared{}` block should also be added for PyLink on all servers.
|
||||
* [ircd-ratbox](http://www.ratbox.org/) (3.x) - module `ts6`
|
||||
- Host changing is not supported.
|
||||
- On ircd-ratbox, all known IPs of users will be shown in `/whois`, even if the client is a cloaked relay client: if you're paranoid about this, turn off Relay IP forwarding by setting the `relay_no_ips` option in the ratbox network's `server:` block.
|
||||
- For KLINE support to work, a `shared{}` block should be added for PyLink on all servers.
|
||||
* [IRCu](http://coder-com.undernet.org/) (u2.10.12.16+) - module `p10`
|
||||
- Host changing is not supported.
|
||||
* [juno-ircd](https://github.com/cooper/yiria) (11.x / janet) - module `ts6` (see [configuration example](https://github.com/cooper/juno/blob/master/doc/ts6.md#pylink))
|
||||
* [juno-ircd](https://github.com/cooper/juno) (13.x / ava) - module `ts6` (see [configuration example](https://github.com/cooper/juno/blob/master/doc/ts6.md#pylink))
|
||||
* [ngIRCd](https://ngircd.barton.de/) (24+) - module `ngircd`
|
||||
- For GLINEs to propagate, the `AllowRemoteOper` option must be enabled in ngIRCd.
|
||||
- `+` (modeless) channels are not supported, and should be disabled for PyLink to function correctly.
|
||||
* [snircd](https://development.quakenet.org/) (1.3.x+) - module `p10`
|
||||
- Outbound host changing (i.e. for the `changehost` plugin) is not supported on P10 variants other than Nefarious.
|
||||
|
||||
|
259
RELNOTES.md
259
RELNOTES.md
@ -1,3 +1,258 @@
|
||||
# PyLink 2.0-rc1 (unreleased)
|
||||
|
||||
#### Bug fixes
|
||||
- relay: CHANDESC permissions are now given to opers if the `relay::allow_free_oper_links` option is true.
|
||||
- Relay no longer forwards kills from servers, preventing extraneous kills for nick collisions and the like.
|
||||
- bots: the `join` command now correctly uses `bots.join` as its permission name (keeping it consistent with the command name).
|
||||
- The previous name `bots.joinclient` is still supported for compatibility reasons.
|
||||
|
||||
#### Documentation updates
|
||||
- Rewrote the Relay Quick Start Guide. [issue#619](https://github.com/jlu5/PyLink/issues/619)
|
||||
- FAQ: expanded Relay section with common questions regarding Relay mechanics (i.e. kill, mode, and server bans handling)
|
||||
- docs/technical, docs/permissions-reference: many updates to bring up to date with PyLink 2.0
|
||||
- various: updated the GitHub repository address
|
||||
|
||||
# PyLink 2.0-beta1 (2018-06-27)
|
||||
|
||||
This release contains all changes from 2.0-alpha3 as well as the following:
|
||||
|
||||
#### New features
|
||||
- **Added TLS certificate verification, which is now enabled by default on Clientbot networks**. [issue#592](https://github.com/jlu5/PyLink/issues/592)
|
||||
- This adds the options `ssl_validate_hostname` and `ssl_accept_invalid_certs` options which have defaults as follows:
|
||||
- | Server type | `ssl_validate_hostname` | `ssl_accept_invalid_certs` |
|
||||
|--------------------------|-----------------------------------------------------|----------------------------|
|
||||
| Full links (S2S) | false (implied by `ssl_accept_invalid_certs: true`) | true |
|
||||
| Clientbot networks (C2S) | true | false |
|
||||
- `ssl_validate_hostname` determines whether a network's TLS certificate will be checked for a matching hostname.
|
||||
- `ssl_accept_invalid_certs` disables certificate checking entirely when enabled, and also turns off `ssl_validate_hostname`.
|
||||
- The existing TLS certificate fingerprint options are unchanged by this and can be turned on and off regardless of these new options.
|
||||
- New Relay features:
|
||||
- **LINKACL now supports whitelisting networks in addition to the original blacklist implementation** (see `help LINKACL`). [issue#394](https://github.com/jlu5/PyLink/issues/394)
|
||||
- relay: The defaults for CLAIM (on or off) and LINKACL (whitelist or blacklist mode) can now be pre-configured for new channels. [issue#581](https://github.com/jlu5/PyLink/issues/581)
|
||||
- You can now set descriptions for channels in `LINKED` via the `CHANDESC` command. [issue#576](https://github.com/jlu5/PyLink/issues/576)
|
||||
- `relay_clientbot` now supports setting clientbot styles by network. [issue#455](https://github.com/jlu5/PyLink/issues/455)
|
||||
- New `relay::allow_free_oper_links` option allows disabling oper access to `CREATE/LINK/DELINK/DESTROY/CLAIM` by default
|
||||
- New Antispam features (see the `antispam:` example block for configuration details):
|
||||
- Antispam now supports text filtering with configured bad strings. [issue#359](https://github.com/jlu5/PyLink/issues/359)
|
||||
- Added `block` as a punishment to only hide messages from other plugins like relay. [issue#616](https://github.com/jlu5/PyLink/issues/616)
|
||||
- Antispam can now process PMs to PyLink clients for spam - this can be limited to service bots, enabled for all PyLink clients (including relay clones), or disabled entirely (the default).
|
||||
- IRC formatting (bold, underline, colors, etc.) is now removed before processing text. [issue#615](https://github.com/jlu5/PyLink/issues/615)
|
||||
- IPv4/IPv6 address selection is now automatic, detecting when an IPv6 address or bindhost is given. [issue#212](https://github.com/jlu5/PyLink/issues/212)
|
||||
- Messages sent by most commands are now transparently word-wrapped to prevent cutoff. [issue#153](https://github.com/jlu5/PyLink/issues/153)
|
||||
- The Global plugin now supports configuring exempt channels. [issue#453](https://github.com/jlu5/PyLink/issues/453)
|
||||
- Automode now allows removing entries by entry numbers. [issue#506](https://github.com/jlu5/PyLink/issues/506)
|
||||
|
||||
#### Feature changes
|
||||
- Relay feature changes:
|
||||
- Relay IP sharing now uses a pool-based configuration scheme (`relay::ip_share_pools`), deprecating the `relay::show_ips` and `relay_no_ips` options.
|
||||
- IPs and real hosts are shared bidirectionally between all networks in an ipshare pool, and masked as `0.0.0.0` when sending to a network not in a pool and when receiving those networks' users.
|
||||
- **KILL handling received a major rework** ([issue#520](https://github.com/jlu5/PyLink/issues/520):
|
||||
- Instead of always bouncing, kills to a relay client can now be forwarded between networks in a killshare pool (`relay::kill_share_pools`).
|
||||
- If the sender and target's networks are not in a killshare pool, the kill is forwarded as a kick to all shared channels that the sender
|
||||
has CLAIM access on (e.g. when they are the home network, whitelisted in `CLAIM`, and/or an op).
|
||||
- The PyLink service client no longer needs to be in channels to log to them.
|
||||
|
||||
#### Bug fixes
|
||||
- Fixed ping timeout handling (this was broken sometime during the port to select).
|
||||
- relay: block networks not on the claim list from merging in modes when relinking (better support for modes set by e.g. services DEFCON).
|
||||
- Reworked relay+clientbot op checks on to [be more consistent](https://github.com/jlu5/PyLink/compare/fee64ece045ad9dc49a07d1b438caa019c90a778~...d4bf407).
|
||||
- inspircd: fix potential desyncs when sending a kill by removing the target immediately. [issue#607](https://github.com/jlu5/PyLink/issues/607)
|
||||
- UserMapping: fixed a missing reference to the parent `irc` instance causing errors on nick collisions.
|
||||
- clientbot: suppress warnings if `/mode #channel` doesn't show arguments to `+lk`, etc. [issue#537](https://github.com/jlu5/PyLink/issues/537)
|
||||
- Relay now removes service persistent channels on unload, and when the home network for a link disconnects.
|
||||
- relay: raise an error when trying to delink a leaf channel from another leaf network.
|
||||
- Previously this would (confusingly) delink the channel from the network the command was called on instead of the intended target.
|
||||
- opercmds: forbid killing the main PyLink client.
|
||||
|
||||
#### Internal changes
|
||||
- Login handling was rewritten and moved entirely from `coremods.corecommands` to `coremods.login`. [issue#590](https://github.com/jlu5/PyLink/issues/590)
|
||||
- New stuff for the core network classes:
|
||||
- `is_privileged_service(entityid)` returns whether the given UID or SID belongs to a privileged service (IRC U:line).
|
||||
- `User`, `Channel` TS values are now consistently stored as `int`. [issue#594](https://github.com/jlu5/PyLink/issues/594)
|
||||
- `match_host()` was split into `match_host()` and `match_text()`; the latter is now preferred as a simple text matcher for IRC-style globs.
|
||||
- New stuff in utils:
|
||||
- `remove_range()` removes a range string of (one-indexed) items from the list, where range strings are indices or ranges of them joined together with a "," (e.g. "5", "2", "2-10", "1,3,5-8") - [issue#506](https://github.com/jlu5/PyLink/issues/506)
|
||||
- `get_hostname_type()` takes in an IP or hostname and returns an int representing the detected address type: 0 (none detected), 1 (IPv4), 2 (IPv6) - [issue#212](https://github.com/jlu5/PyLink/issues/212)
|
||||
- `parse_duration()` takes in a duration string (in the form `1w2d3h4m5s`, etc.) and returns the equiv. amount of seconds - [issue#504](https://github.com/jlu5/PyLink/issues/504)
|
||||
- The TLS/SSL setup bits in `IRCNetwork` were broken into multiple functions: `_make_ssl_context()`, `_setup_ssl`, and `_verify_ssl()`
|
||||
- Removed deprecated attributes: `irc.botdata`, `irc.conf`, `utils.is*` methods, `PyLinkNetworkCoreWithUtils.check_authenticated()` - [issue#422](https://github.com/jlu5/PyLink/issues/422)
|
||||
- PyLinkNCWUtils: The `allowAuthed`, `allowOper` options in `is_oper()` are now deprecated no-ops (they are set to False and True respectively)
|
||||
|
||||
# PyLink 2.0-alpha3 (2018-05-10)
|
||||
|
||||
This release contains all changes from 1.3.0, as well as the following:
|
||||
|
||||
#### New features
|
||||
- **Experimental daemonization support via `pylink -d`**. [issue#187](https://github.com/GLolol/PyLink/issues/187)
|
||||
- New (alpha-quality) `antispam` plugin targeting mass-highlight spam: it supports any combination of kick, ban, quiet (mute), and kill as punishment. [issue#359](https://github.com/GLolol/PyLink/issues/359)
|
||||
- Clientbot now supports expansions such as `$nick` in autoperform.
|
||||
- Relay now translates STATUSMSG messages (e.g. `@#channel` messages) for target networks instead of passing them on as-is. [issue#570](https://github.com/GLolol/PyLink/issues/570)
|
||||
- Relay endburst delay on InspIRCd networks is now configurable via the `servers::NETNAME::relay_endburst_delay` option.
|
||||
- The servermaps plugin now shows the uplink server name for Clientbot links
|
||||
- Added `--trace / -t` options to the launcher for integration with Python's `trace` module.
|
||||
|
||||
#### Feature changes
|
||||
- **Reverted the commit making SIGHUP shutdown the PyLink daemon**. Now, SIGUSR1 and SIGHUP both trigger a rehash, while SIGTERM triggers a shutdown.
|
||||
- The `raw` command has been split into a new plugin (`plugins/raw.py`) with two permissions: `raw.raw` for Clientbot networks, and `raw.raw.unsupported_network` for other protocols. Using raw commands outside Clientbot is not supported. [issue#565](https://github.com/GLolol/PyLink/issues/565)
|
||||
- The servermaps plugin now uses two permissions for `map` and `localmap`: `servermaps.map` and `servermaps.localmap` respectively
|
||||
- `showuser` and `showchan` now consistently report times in UTC
|
||||
|
||||
#### Bug fixes
|
||||
- protocols/clientbot: fix errors when connecting to networks with mixed-case server names (e.g. AfterNET)
|
||||
- relay: fix KeyError when a local client is kicked from a claimed channel. [issue#572](https://github.com/GLolol/PyLink/issues/572)
|
||||
- Fix `irc.parse_modes()` incorrectly mangling modes changes like `+b-b *!*@test.host *!*@test.host` into `+b *!*@test.host`. [issue#573](https://github.com/GLolol/PyLink/issues/573)
|
||||
- automode: fix handling of channels with multiple \#'s in them
|
||||
- launcher: prevent protocol module loading errors (e.g. non-existent protocol module) from blocking the setup of other networks.
|
||||
- This fixes a side-effect which can cause relay to stop functioning (`world.started` is never set)
|
||||
- relay_clientbot: fix `STATUSMSG` (`@#channel`) notices from being relayed to channels that it shouldn't
|
||||
- Fixed various 2.0-alpha2 regressions:
|
||||
- Relay now relays service client messages as PRIVMSG and P10 WALL\* commands as NOTICE
|
||||
- protocols/inspircd: fix supported modules list being corrupted when an indirectly linked server shuts down. [issue#567](https://github.com/GLolol/PyLink/issues/567)
|
||||
- networks: `remote` now properly errors if the target service is not available on a network. [issue#554](https://github.com/GLolol/PyLink/issues/554)
|
||||
- commands: fix `showchan` displaying status prefixes in reverse
|
||||
- stats: route permission error replies to notice instead of PRIVMSG
|
||||
- This prevents "Unknown command" flood loops with services which poll `/stats` on link.
|
||||
- clientbot: fixed sending duplicate JOIN hooks and AWAY status updates. [issue#551](https://github.com/GLolol/PyLink/issues/551)
|
||||
|
||||
#### Internal improvements
|
||||
- **Reading from sockets now uses select instead of one thread per network.**
|
||||
- This new code uses the Python selectors module, which automatically chooses the fastest polling backend available ([`epoll|kqueue|devpoll > poll > select`](https://github.com/python/cpython/blob/v3.6.5/Lib/selectors.py#L599-L601)).
|
||||
- **API Break: significantly reworked channel handling for service bots**. [issue#265](https://github.com/GLolol/PyLink/issues/265)
|
||||
- The `ServiceBot.extra_channels` attribute in previous versions is replaced with `ServiceBot.dynamic_channels`, which is accessed indirectly via new functions `ServiceBot.add_persistent_channel()`, `ServiceBot.remove_persistent_channel()`, `ServiceBot.get_persistent_channels()`. This API also replaces `ServiceBot.join()` for most plugins, which now joins channels *non-persistently*.
|
||||
- This API change provides plugins with a way of registering dynamic persistent channels, which are consistently rejoined on kick or kill.
|
||||
- Persistent channels are also "dynamic" in the sense that PyLink service bots will now part channels marked persistent when they become empty, and rejoin when it is recreated.
|
||||
- This new implementation is also plugin specific, as plugins must provide a namespace (usually the plugin name) when managing persistent channels using `ServiceBot.(add|remove)_persistent_channel()`.
|
||||
- New abstraction: `ServiceBot.get_persistent_channels()` which fetches the list of all persistent channels on a network (i.e. *both* the config defined channels and what's registered in `dynamic_channels`).
|
||||
- New abstraction: `ServiceBot.part()` sends a part request to channels and only succeeds if it is not marked persistent by any plugin. This effectively works around the long-standing issue of relay-services conflicts. [issue#265](https://github.com/GLolol/PyLink/issues/265)
|
||||
- Major optimizations to `irc.nick_to_uid`: `PyLinkNetworkCore.users` and `classes.User` now transparently maintain an index mapping nicks to UIDs instead of doing reverse lookup on every call.
|
||||
- This is done via a new `UserMapping` class in `pylinkirc.classes`, which stores User objects by UID and provides a `bynick` attribute mapping case-normalized nicks to lists of UIDs.
|
||||
- `classes.User.nick` is now a property, where the setter implicitly updates the `bynick` index with a pre-computed case-normalized version of the nick (also stored to `User.lower_nick`)
|
||||
- Various relay optimizations: reuse target SID when bursting joins, and only look up nick once in `normalize_nick`
|
||||
- Rewritten CTCP plugin, now extending to all service bots. [issue#468](https://github.com/GLolol/PyLink/issues/468), [issue#407](https://github.com/GLolol/PyLink/issues/407)
|
||||
- Relay no longer spams configured U-lines with "message dropped because you aren't in a common channel" errors
|
||||
- The `endburst_delay` option to `spawn_server()` was removed from the protocol spec, and replaced by a private API used by protocols/inspircd and relay.
|
||||
- New API: hook handlers can now filter messages from lower-priority handlers by returning `False`. [issue#547](https://github.com/GLolol/PyLink/issues/547)
|
||||
- New API: added `irc.get_server_option()` to fetch server-specific config variables and global settings as a fallback. [issue#574](https://github.com/GLolol/PyLink/issues/574)
|
||||
- automode: replace assert checks with proper exceptions
|
||||
- Renamed methods in log, utils, conf to snake case. [issue#523](https://github.com/GLolol/PyLink/issues/523)
|
||||
- Remove `structures.DeprecatedAttributesObject`; it's vastly inefficient for what it accomplishes
|
||||
- clientbot: removed unreliable pre-/WHO join bursting with `userhost-in-names`
|
||||
- API change: `kick` and `kill` command funcitons now raise `NotImplementedError` when not supported by a protocol
|
||||
- relay, utils: remove remaining references to deprecated `irc.proto`
|
||||
|
||||
# PyLink 2.0-alpha2 (2018-01-16)
|
||||
This release includes all changes from 1.2.2-dev, plus the following:
|
||||
|
||||
#### New features
|
||||
- relay_clientbot: add support for showing prefix modes in relay text, via a new `$mode_prefix` expansion. [issue#540](https://github.com/GLolol/PyLink/issues/540)
|
||||
- Added new modedelta feature to Relay:
|
||||
- Modedelta allows specifying a list of (named) modes to only apply on leaf channels, which can be helpful to fight spam if leaf networks don't have adequate spam protection.
|
||||
- relay: added new option `server::<networkname>:relay_forcetag_nicks`, a per-network list of nick globs to always tag when introducing users onto a network. [issue#564](https://github.com/GLolol/PyLink/issues/564)
|
||||
- Added support for more channel modes in Relay:
|
||||
* blockcaps: inspircd +B, elemental-ircd +G
|
||||
* exemptchanops: inspircd +X
|
||||
* filter: inspircd +g, unreal extban ~T:block ([issue#557](https://github.com/GLolol/PyLink/issues/557))
|
||||
* hidequits: nefarious +Q, snircd +u
|
||||
* history: inspircd +H
|
||||
* largebanlist: ts6 +L
|
||||
* noamsg: snircd/nefarious +T
|
||||
* blockhighlight: inspircd +V (extras module)
|
||||
* kicknorejoin: elemental-ircd +J ([issue#559](https://github.com/GLolol/PyLink/issues/559))
|
||||
* kicknorejoin_insp: inspircd +J (with argument; [issue#559](https://github.com/GLolol/PyLink/issues/559))
|
||||
* repeat: elemental-ircd +E ([issue#559](https://github.com/GLolol/PyLink/issues/559))
|
||||
* repeat_insp: inspircd +K (with argument; [issue#559](https://github.com/GLolol/PyLink/issues/559))
|
||||
- Added support for UnrealIRCd extban `~T` in Relay. [issue#557](https://github.com/GLolol/PyLink/issues/557)
|
||||
- p10: added proper support for STATUSMSG notices (i.e. messages to `@#channel` and the like) via WALLCHOPS/WALLHOPS/WALLVOICES
|
||||
- p10: added outgoing /knock support by sending it as a notice
|
||||
- ts6: added incoming /knock handling
|
||||
- relay: added support for relaying /knock
|
||||
|
||||
#### Backwards incompatible changes
|
||||
- **The ratbox protocol module has been merged into ts6**, with a new `ircd: ratbox` option introduced to declare Ratbox as the target IRCd. [issue#543](https://github.com/GLolol/PyLink/issues/543)
|
||||
|
||||
#### Bug fixes
|
||||
- Fix default permissions not applying on startup (2.0-alpha1 regression). [issue#542](https://github.com/GLolol/PyLink/issues/542)
|
||||
- Fix rejoin-on-kill for the main PyLink bot not working (2.0-alpha1/[94e05a6](https://github.com/GLolol/PyLink/commit/94e05a623314e9b0607de4eb01fab28be2e0c7e1) regression).
|
||||
- Clientbot fixes:
|
||||
- Fix desyncs caused by incomplete nick collision checking when a user on a Clientbot link changes their nick to match an existing virtual client. [issue#535](https://github.com/GLolol/PyLink/issues/535)
|
||||
- Fix desync involving ghost users when a person leaves a channel, changes their nick, and rejoins. [issue#536](https://github.com/GLolol/PyLink/issues/536)
|
||||
- Treat 0 as "no account" when parsing WHOX responses; this fixes incorrect "X is logged in as 0" output on WHOIS.
|
||||
- protocols/p10: fix the `use_hashed_cloaks` server option not being effective.
|
||||
- Fix long standing issues where relay would sometimes burst users multiple times on connect. [issue#529](https://github.com/GLolol/PyLink/issues/529)
|
||||
- Also fix a regression from 2.0-alpha1 where users would not be joined if the hub link is down ([issue#548](https://github.com/GLolol/PyLink/issues/548))
|
||||
- Fix `$a:account` extbans being dropped by relay (they were being confused with `$a`). [issue#560](https://github.com/GLolol/PyLink/issues/560)
|
||||
- Fix corrupt arguments when mixing the `remote` and `mode` commands. [issue#538](https://github.com/GLolol/PyLink/issues/538)
|
||||
- Fix lingering queue threads when networks disconnect. [issue#558](https://github.com/GLolol/PyLink/issues/558)
|
||||
- The relay and global plugins now better handle empty / poorly formed config blocks.
|
||||
- bots: don't allow `spawnclient` on protocol modules with virtual clients (e.g. clientbot)
|
||||
- bots: fix KeyError when trying to join previously nonexistent channels
|
||||
|
||||
#### Internal improvements
|
||||
- `Channel.sort_prefixes()` now consistently sorts modes from highest to lowest (i.e. from owner to voice). Also removed workaround code added to deal with the wonkiness of this function.
|
||||
- ircs2s_common: add handling for `nick@servername` messages.
|
||||
- `IRCNetwork` should no longer send multiple disconnect hooks for one disconnection.
|
||||
- protocols/ts6 no longer requires `SAVE` support from the uplink. [issue#545](https://github.com/GLolol/PyLink/issues/545)
|
||||
- ts6, hybrid: miscellaneous cleanup
|
||||
- protocols/inspircd now tracks module (un)loads for `m_chghost.so` and friends. [issue#555](https://github.com/GLolol/PyLink/issues/555)
|
||||
- Clientbot now logs failed attempts in joining channels. [issue#533](https://github.com/GLolol/PyLink/issues/533)
|
||||
|
||||
# PyLink 2.0-alpha1 (2017-10-07)
|
||||
The "Eclectic" release. This release includes all changes from 1.2.1, plus the following:
|
||||
|
||||
#### New features
|
||||
- **Login blocks can now be limited by network, user hostname, and IRCop status**: see the new "require_oper", "hosts", and "networks" options in the example config.
|
||||
- **Added support for ngIRCd, ChatIRCd, and beware-ircd** (via protocol modules `ngircd`, `ts6`, `p10` respectively)
|
||||
- **Add support for extbans in protocols and relay**: this is supported on UnrealIRCd, Charybdis (and derivatives), InspIRCd, and Nefarious P10.
|
||||
- Yes, this means you can finally mute bothersome relay users.
|
||||
- Clientbot is now more featureful:
|
||||
- Added support for IRCv3 caps `account-notify`, `account-tag`, `away-notify`, `chghost`, `extended-join`, and `userhost-in-names`
|
||||
- Configurable alternate / fallback nicks are now supported: look for the `pylink_altnicks` option in the example config.
|
||||
- Added support for WHOX, to complement IRCv3 `account-notify` and `account-tag`.
|
||||
- Relay_clientbot can now relay mode changes as text.
|
||||
- Clientbot can now optionally sync ban lists when it joins a channel, allowing Relay modesync to unset bans properly. See the `fetch_ban_lists` option in the example config.
|
||||
- PyLink received a new launcher, which now checks for stale PID files (when `psutil` is installed) and supports shutdown/restart/rehash via the command line.
|
||||
- New commands for the opercmds plugin, including:
|
||||
- `chghost`, `chgident`, and `chgname`, for IRCds that don't expose them as commands.
|
||||
- `massban`, `masskill`, `massbanre`, and `masskillre` - these commands allow setting kickbans, kills, or glines on users matching a PyLink mask (`n!u@h` mask/exttarget) or regular expression. The hope is that these tools can help opers actively fight botnets as they are connected, similar to atheme's `clearchan` and Anope's `chankill` commands.
|
||||
- `checkbanre` - a companion to `checkban`, using regex matching
|
||||
- Better support for (pre-defined) U-lined services servers in relay:
|
||||
- `CLAIM` restrictions are relaxed for service bots, which may now join with ops and set simple modes. This prevents mode floods when features such as `DEFCON` are enabled, and when a channel is accidentally registered on a network not on the CLAIM list.
|
||||
- `DEFCON` modes set by services are ignored by Relay instead of bounced, and do not forward onto other networks unless the setting network is also in the channel's `CLAIM` list.
|
||||
- To keep the spirit of `CLAIM` alive, opped services not in a channel's `CLAIM` list are still not allowed to kick remote users, set prefix modes (e.g. op) on others, or set list modes such as bans.
|
||||
- Service bots' hostnames and real names are now fully configurable, globally and per network.
|
||||
- Added per-network configuration of relay server suffixes.
|
||||
- Added IRC `/STATS` support via the `stats` plugin (`/stats c`, `u`, and `o` are supported so far)
|
||||
- PyLink's connection time is now displayed when WHOISing service bots. This info can be turned off using the `pylink:whois_show_startup_time` option.
|
||||
- More specific permissions for the `remote` command, which now allows assigning permissions by target network, service bot, and command.
|
||||
- New `$service` exttarget which matches service bots by name.
|
||||
|
||||
#### Backwards incompatible changes
|
||||
- Signal handling on Unix was updated to use `SIGUSR1` for rehash and `SIGHUP` for shutdown - this changes PyLink to be more in line with foreground programs, which generally close with the owning terminal.
|
||||
- Some options were deprecated and renamed:
|
||||
- The `p10_ircd` option for P10 servers is now named `ircd`, though the old option will still be read from.
|
||||
- The `use_elemental_modes` setting on ts6 networks has been deprecated and replaced with an `ircd` option targeting charybdis, elemental-ircd, or chatircd. Supported values for `ircd` include `charybdis`, `elemental`, and `chatircd`.
|
||||
- PID file checking is now enabled by default, along with checks for stale PID files *only* when [`psutil`](https://pythonhosted.org/psutil/) is installed. Users upgrading from PyLink < 1.1-dev without `psutil` installed will need remove PyLink's PID files before starting the service.
|
||||
- The `fml` command in the `games` plugin was removed.
|
||||
|
||||
#### Bug fixes
|
||||
- Relay should stop bursting channels multiple times on startup now. (`initialize_channel` now skips execution if another thread is currently initializing the same channel)
|
||||
- Fixed a long standing bug where fantasy responses would relay before a user's original command if the `fantasy` plugin was loaded before `relay`. (Bug #123)
|
||||
|
||||
#### Internal changes
|
||||
- **API Break**: The protocol module layer is completely rewritten, with the `Irc` and `Protocol`-derived classes combining into one. Porting **will** be needed for old protocol modules and plugins targetting 1.x; see the [new (WIP) protocol specification](https://github.com/GLolol/PyLink/blob/devel/docs/technical/pmodule-spec.md) for details.
|
||||
- **API Break**: Channels are now stored in two linked dictionaries per IRC object: once in `irc._channels`, and again in `irc.channels`. The main difference is that `irc._channels` implicitly creates new channels when accessing them if they didn't previously exist (prefer this for protocol modules), while `irc.channels` does not raises and raises KeyError instead (prefer this for plugins).
|
||||
- **API Break**: Most methods in `utils` and `classes` were renamed from camel case to snake case. `log`, `conf`, and others will be ported too before the final 2.0 release.
|
||||
- **API Break**: IRC protocol modules' server introductions must now use **`post_connect()`** instead of **`connect()`** to prevent name collisions with the base connection handling code.
|
||||
- Channels are now stored case insensitively internally, so protocol modules and new plugins no longer need to manually coerse names to lowercase.
|
||||
- Plugins can now bind hooks as specific priorities via an optional `priority` option in `utils.add_hook`. Hooks with higher priorities will be called first; the default priority value us 500.
|
||||
- Commands can now be properly marked as aliases, so that duplicates don't show in the `list` command.
|
||||
- Added basic `GLINE/KLINE` support for most IRCds; work is ongoing to polish this off.
|
||||
- PyLink accounts are now implicitly matched: i.e. `user1` is now equivalent to `$pylinkacc:user1`
|
||||
- Added complete support for "Network Administrator" and "Network Service" as oper types on IRCds using user modes to denote them (e.g. UnrealIRCd, charybdis).
|
||||
- User and server hop counts are now tracked properly instead of being hardcoded as 1.
|
||||
- protocols/p10 now bursts IPv6 IPs to supported uplinks.
|
||||
- Fixed compatibility with ircd-hybrid trunk after commit 981c61e (EX and IE are no longer sent in the capability list)
|
||||
|
||||
# PyLink 1.3.0 (2018-05-08)
|
||||
The 1.3 update focuses on backporting some commonly requested and useful features from the WIP 2.0 branch. This release includes all changes from 1.3-beta1, plus the following:
|
||||
|
||||
@ -32,7 +287,7 @@ The 1.3 update focuses on backporting some commonly requested and useful feature
|
||||
- Significantly revised example-conf for wording and consistency.
|
||||
- protocols/unreal: bumped protocol version to 4017 (no changes needed)
|
||||
|
||||
# PyLink 1.2.1
|
||||
# PyLink 1.2.1 (2017-09-19)
|
||||
The "Dancer" release. Changes from 1.2.0:
|
||||
|
||||
#### Bug fixes
|
||||
@ -56,7 +311,7 @@ The "Dancer" release. Changes from 1.2.0:
|
||||
- Minor logging cleanup for relay and `Irc.matchHost()`.
|
||||
- Fix cmode `+p` mapping on TS6 networks.
|
||||
|
||||
# PyLink 1.2.0
|
||||
# PyLink 1.2.0 (2017-08-14)
|
||||
The "Dragons" release. Changes since 1.2.0-rc1:
|
||||
|
||||
#### Feature changes
|
||||
|
2414
classes.py
2414
classes.py
File diff suppressed because it is too large
Load Diff
46
conf.py
46
conf.py
@ -17,7 +17,7 @@ from collections import defaultdict
|
||||
|
||||
from . import world
|
||||
|
||||
class ConfigValidationError(Exception):
|
||||
class ConfigurationError(RuntimeError):
|
||||
"""Error when config conditions aren't met."""
|
||||
|
||||
conf = {'bot':
|
||||
@ -50,19 +50,19 @@ conf['pylink'] = conf['bot']
|
||||
confname = 'unconfigured'
|
||||
|
||||
def validate(condition, errmsg):
|
||||
"""Convenience function to validate conditions in validateConf()."""
|
||||
"""Raises ConfigurationError with errmsg unless the given condition is met."""
|
||||
if not condition:
|
||||
raise ConfigValidationError(errmsg)
|
||||
raise ConfigurationError(errmsg)
|
||||
|
||||
def _log(level, text, *args, logger=None, **kwargs):
|
||||
if logger:
|
||||
logger.log(level, text, *args, **kwargs)
|
||||
else:
|
||||
world.log_queue.append((level, text))
|
||||
world._log_queue.append((level, text))
|
||||
|
||||
def validateConf(conf, logger=None):
|
||||
def _validate_conf(conf, logger=None):
|
||||
"""Validates a parsed configuration dict."""
|
||||
validate(type(conf) == dict,
|
||||
validate(isinstance(conf, dict),
|
||||
"Invalid configuration given: should be type dict, not %s."
|
||||
% type(conf).__name__)
|
||||
|
||||
@ -88,13 +88,13 @@ def validateConf(conf, logger=None):
|
||||
_log(logging.WARNING, "The 'login:user' and 'login:password' options are deprecated since PyLink 1.1. "
|
||||
"Please switch to the new 'login:accounts' format as outlined in the example config.", logger=logger)
|
||||
|
||||
old_login_valid = type(conf['login'].get('password')) == type(conf['login'].get('user')) == str
|
||||
old_login_valid = isinstance(conf['login'].get('password'), str) and isinstance(conf['login'].get('user'), str)
|
||||
newlogins = conf['login'].get('accounts', {})
|
||||
|
||||
validate(old_login_valid or newlogins, "No accounts were set, aborting!")
|
||||
for account, block in newlogins.items():
|
||||
validate(type(account) == str, "Bad username format %s" % account)
|
||||
validate(type(block.get('password')) == str, "Bad password %s for account %s" % (block.get('password'), account))
|
||||
validate(isinstance(account, str), "Bad username format %s" % account)
|
||||
validate(isinstance(block.get('password'), str), "Bad password %s for account %s" % (block.get('password'), account))
|
||||
|
||||
validate(conf['login'].get('password') != "changeme", "You have not set the login details correctly!")
|
||||
|
||||
@ -108,8 +108,7 @@ def validateConf(conf, logger=None):
|
||||
|
||||
return conf
|
||||
|
||||
|
||||
def loadConf(filename, errors_fatal=True, logger=None):
|
||||
def load_conf(filename, errors_fatal=True, logger=None):
|
||||
"""Loads a PyLink configuration file from the filename given."""
|
||||
global confname, conf, fname
|
||||
# Note: store globally the last loaded conf filename, for REHASH in coremods/control.
|
||||
@ -119,13 +118,32 @@ def loadConf(filename, errors_fatal=True, logger=None):
|
||||
try:
|
||||
with open(filename, 'r') as f:
|
||||
conf = yaml.safe_load(f)
|
||||
conf = validateConf(conf, logger=logger)
|
||||
conf = _validate_conf(conf, logger=logger)
|
||||
except Exception as e:
|
||||
print('ERROR: Failed to load config from %r: %s: %s' % (filename, type(e).__name__, e), file=sys.stderr)
|
||||
print(' Users upgrading from users < 0.9-alpha1 should note that the default configuration has been renamed to *pylink.yml*, not *config.yml*', file=sys.stderr)
|
||||
e = 'Failed to load config from %r: %s: %s' % (filename, type(e).__name__, e)
|
||||
|
||||
if logger: # Prefer using the Python logger when available
|
||||
logger.exception(e)
|
||||
else: # Otherwise, fall back to a print() call.
|
||||
print('ERROR: %s' % e, file=sys.stderr)
|
||||
|
||||
if errors_fatal:
|
||||
sys.exit(1)
|
||||
|
||||
raise
|
||||
else:
|
||||
return conf
|
||||
|
||||
def get_database_name(dbname):
|
||||
"""
|
||||
Returns a database filename with the given base DB name appropriate for the
|
||||
current PyLink instance.
|
||||
|
||||
This returns '<dbname>.db' if the running config name is PyLink's default
|
||||
(pylink.yml), and '<dbname>-<config name>.db' for anything else. For example,
|
||||
if this is called from an instance running as './pylink testing.yml', it
|
||||
would return '<dbname>-testing.db'."""
|
||||
if confname != 'pylink':
|
||||
dbname += '-%s' % confname
|
||||
dbname += '.db'
|
||||
return dbname
|
||||
|
@ -7,12 +7,10 @@ import threading
|
||||
import sys
|
||||
import atexit
|
||||
|
||||
from pylinkirc import world, utils, conf, classes
|
||||
from pylinkirc.log import log, makeFileLogger, stopFileLoggers, getConsoleLogLevel
|
||||
from pylinkirc import world, utils, conf # Do not import classes, it'll import loop
|
||||
from pylinkirc.log import log, _make_file_logger, _stop_file_loggers, _get_console_log_level
|
||||
from . import permissions
|
||||
|
||||
tried_shutdown = False
|
||||
|
||||
def remove_network(ircobj):
|
||||
"""Removes a network object from the pool."""
|
||||
# Disable autoconnect first by setting the delay negative.
|
||||
@ -21,7 +19,7 @@ def remove_network(ircobj):
|
||||
del world.networkobjects[ircobj.name]
|
||||
|
||||
def _print_remaining_threads():
|
||||
log.debug('_shutdown(): Remaining threads: %s', ['%s/%s' % (t.name, t.ident) for t in threading.enumerate()])
|
||||
log.debug('shutdown(): Remaining threads: %s', ['%s/%s' % (t.name, t.ident) for t in threading.enumerate()])
|
||||
|
||||
def _remove_pid():
|
||||
pidfile = "%s.pid" % conf.confname
|
||||
@ -46,7 +44,7 @@ def _kill_plugins(irc=None):
|
||||
if hasattr(plugin, 'die'):
|
||||
log.debug('coremods.control: Running die() on plugin %s due to shutdown.', name)
|
||||
try:
|
||||
plugin.die(irc)
|
||||
plugin.die(irc=irc)
|
||||
except: # But don't allow it to crash the server.
|
||||
log.exception('coremods.control: Error occurred in die() of plugin %s, skipping...', name)
|
||||
|
||||
@ -55,22 +53,21 @@ def _kill_plugins(irc=None):
|
||||
atexit.register(_remove_pid)
|
||||
atexit.register(_kill_plugins)
|
||||
|
||||
def _shutdown(irc=None):
|
||||
def shutdown(irc=None):
|
||||
"""Shuts down the Pylink daemon."""
|
||||
global tried_shutdown
|
||||
if tried_shutdown: # We froze on shutdown last time, so immediately abort.
|
||||
if world.shutting_down.is_set(): # We froze on shutdown last time, so immediately abort.
|
||||
_print_remaining_threads()
|
||||
raise KeyboardInterrupt("Forcing shutdown.")
|
||||
|
||||
tried_shutdown = True
|
||||
world.shutting_down.set()
|
||||
|
||||
# HACK: run the _kill_plugins trigger with the current IRC object. XXX: We should really consider removing this
|
||||
# argument, since no plugins actually use it to do anything.
|
||||
atexit.unregister(_kill_plugins)
|
||||
_kill_plugins(irc)
|
||||
_kill_plugins(irc=irc)
|
||||
|
||||
# Remove our main PyLink bot as well.
|
||||
utils.unregisterService('pylink')
|
||||
utils.unregister_service('pylink')
|
||||
|
||||
for ircobj in world.networkobjects.copy().values():
|
||||
# Disconnect all our networks.
|
||||
@ -82,35 +79,31 @@ def _shutdown(irc=None):
|
||||
|
||||
# Done.
|
||||
|
||||
def sigterm_handler(signo, stack_frame):
|
||||
def _sigterm_handler(signo, stack_frame):
|
||||
"""Handles SIGTERM and SIGINT gracefully by shutting down the PyLink daemon."""
|
||||
log.info("Shutting down on signal %s." % signo)
|
||||
_shutdown()
|
||||
shutdown()
|
||||
|
||||
signal.signal(signal.SIGTERM, sigterm_handler)
|
||||
signal.signal(signal.SIGINT, sigterm_handler)
|
||||
signal.signal(signal.SIGTERM, _sigterm_handler)
|
||||
signal.signal(signal.SIGINT, _sigterm_handler)
|
||||
|
||||
def _rehash():
|
||||
def rehash():
|
||||
"""Rehashes the PyLink daemon."""
|
||||
log.info('Reloading PyLink configuration...')
|
||||
old_conf = conf.conf.copy()
|
||||
fname = conf.fname
|
||||
new_conf = conf.loadConf(fname, errors_fatal=False, logger=log)
|
||||
new_conf = conf.load_conf(fname, errors_fatal=False, logger=log)
|
||||
conf.conf = new_conf
|
||||
|
||||
# Reset any file logger options.
|
||||
stopFileLoggers()
|
||||
_stop_file_loggers()
|
||||
files = new_conf['logging'].get('files')
|
||||
if files:
|
||||
for filename, config in files.items():
|
||||
makeFileLogger(filename, config.get('loglevel'))
|
||||
_make_file_logger(filename, config.get('loglevel'))
|
||||
|
||||
log.debug('rehash: updating console log level')
|
||||
world.console_handler.setLevel(getConsoleLogLevel())
|
||||
|
||||
# Reset permissions.
|
||||
log.debug('rehash: resetting permissions')
|
||||
permissions.resetPermissions()
|
||||
world.console_handler.setLevel(_get_console_log_level())
|
||||
|
||||
for network, ircobj in world.networkobjects.copy().items():
|
||||
# Server was removed from the config file, disconnect them.
|
||||
@ -120,25 +113,31 @@ def _rehash():
|
||||
remove_network(ircobj)
|
||||
else:
|
||||
# XXX: we should really just add abstraction to Irc to update config settings...
|
||||
ircobj.conf = new_conf
|
||||
ircobj.serverdata = new_conf['servers'][network]
|
||||
ircobj.botdata = new_conf['bot']
|
||||
|
||||
ircobj.autoconnect_active_multiplier = 1
|
||||
|
||||
# Clear the IRC object's channel loggers and replace them with
|
||||
# new ones by re-running logSetup().
|
||||
# new ones by re-running log_setup().
|
||||
while ircobj.loghandlers:
|
||||
log.removeHandler(ircobj.loghandlers.pop())
|
||||
|
||||
ircobj.logSetup()
|
||||
ircobj.log_setup()
|
||||
|
||||
utils.resetModuleDirs()
|
||||
utils._reset_module_dirs()
|
||||
|
||||
for network, sdata in new_conf['servers'].items():
|
||||
# Connect any new networks or disconnected networks if they aren't already.
|
||||
if (network not in world.networkobjects) or (not world.networkobjects[network].connection_thread.is_alive()):
|
||||
proto = utils.getProtocolModule(sdata['protocol'])
|
||||
world.networkobjects[network] = classes.Irc(network, proto, new_conf)
|
||||
if network not in world.networkobjects:
|
||||
try:
|
||||
proto = utils._get_protocol_module(sdata['protocol'])
|
||||
|
||||
# API note: 2.0.x style of starting network connections
|
||||
world.networkobjects[network] = newirc = proto.Class(network)
|
||||
newirc.connect()
|
||||
except:
|
||||
log.exception('Failed to initialize network %r, skipping it...', network)
|
||||
|
||||
log.info('Finished reloading PyLink configuration.')
|
||||
|
||||
if os.name == 'posix':
|
||||
@ -146,7 +145,7 @@ if os.name == 'posix':
|
||||
def _sighup_handler(signo, _stack_frame):
|
||||
"""Handles SIGHUP/SIGUSR1 by rehashing the PyLink daemon."""
|
||||
log.info("Signal %s received, reloading config." % signo)
|
||||
_rehash()
|
||||
rehash()
|
||||
|
||||
signal.signal(signal.SIGHUP, _sighup_handler)
|
||||
signal.signal(signal.SIGUSR1, _sighup_handler)
|
||||
|
@ -13,84 +13,14 @@ from pylinkirc.log import log
|
||||
# Essential, core commands go here so that the "commands" plugin with less-important,
|
||||
# but still generic functions can be reloaded.
|
||||
|
||||
def _login(irc, source, username):
|
||||
"""Internal function to process logins."""
|
||||
# Mangle case before we start checking for login data.
|
||||
accounts = {k.lower(): v for k, v in conf.conf['login'].get('accounts', {}).items()}
|
||||
|
||||
logindata = accounts.get(username.lower(), {})
|
||||
network_filter = logindata.get('networks')
|
||||
require_oper = logindata.get('require_oper', False)
|
||||
hosts_filter = logindata.get('hosts', [])
|
||||
|
||||
if network_filter and irc.name not in network_filter:
|
||||
irc.error("You are not authorized to log in to %r on this network." % username)
|
||||
log.warning("(%s) Failed login to %r from %s (wrong network: networks filter says %r but we got %r)", irc.name, username, irc.getHostmask(source), ', '.join(network_filter), irc.name)
|
||||
return
|
||||
|
||||
elif require_oper and not irc.isOper(source, allowAuthed=False):
|
||||
irc.error("You must be opered to log in to %r." % username)
|
||||
log.warning("(%s) Failed login to %r from %s (needs oper)", irc.name, username, irc.getHostmask(source))
|
||||
return
|
||||
|
||||
elif hosts_filter and not any(irc.matchHost(host, source) for host in hosts_filter):
|
||||
irc.error("Failed to log in to %r: hostname mismatch." % username)
|
||||
log.warning("(%s) Failed login to %r from %s (hostname mismatch)", irc.name, username, irc.getHostmask(source))
|
||||
return
|
||||
|
||||
irc.users[source].account = username
|
||||
irc.reply('Successfully logged in as %s.' % username)
|
||||
log.info("(%s) Successful login to %r by %s",
|
||||
irc.name, username, irc.getHostmask(source))
|
||||
|
||||
def _loginfail(irc, source, username):
|
||||
"""Internal function to process login failures."""
|
||||
irc.error('Incorrect credentials.')
|
||||
log.warning("(%s) Failed login to %r from %s", irc.name, username, irc.getHostmask(source))
|
||||
|
||||
@utils.add_cmd
|
||||
def identify(irc, source, args):
|
||||
"""<username> <password>
|
||||
|
||||
Logs in to PyLink using the configured administrator account."""
|
||||
if utils.isChannel(irc.called_in):
|
||||
irc.reply('Error: This command must be sent in private. '
|
||||
'(Would you really type a password inside a channel?)')
|
||||
return
|
||||
try:
|
||||
username, password = args[0], args[1]
|
||||
except IndexError:
|
||||
irc.reply('Error: Not enough arguments.')
|
||||
return
|
||||
|
||||
# Process new-style accounts.
|
||||
if login.checkLogin(username, password):
|
||||
_login(irc, source, username)
|
||||
return
|
||||
|
||||
# Process legacy logins (login:user).
|
||||
if username.lower() == conf.conf['login'].get('user', '').lower() and password == conf.conf['login'].get('password'):
|
||||
realuser = conf.conf['login']['user']
|
||||
_login(irc, source, realuser)
|
||||
else:
|
||||
# Username not found.
|
||||
_loginfail(irc, source, username)
|
||||
|
||||
|
||||
@utils.add_cmd
|
||||
def shutdown(irc, source, args):
|
||||
"""takes no arguments.
|
||||
|
||||
Exits PyLink by disconnecting all networks."""
|
||||
|
||||
permissions.checkPermissions(irc, source, ['core.shutdown'])
|
||||
|
||||
u = irc.users[source]
|
||||
|
||||
log.info('(%s) SHUTDOWN requested by "%s!%s@%s", exiting...', irc.name, u.nick,
|
||||
u.ident, u.host)
|
||||
|
||||
control._shutdown(irc)
|
||||
permissions.check_permissions(irc, source, ['core.shutdown'])
|
||||
log.info('(%s) SHUTDOWN requested by %s, exiting...', irc.name, irc.get_hostmask(source))
|
||||
control.shutdown(irc=irc)
|
||||
|
||||
@utils.add_cmd
|
||||
def load(irc, source, args):
|
||||
@ -99,7 +29,7 @@ def load(irc, source, args):
|
||||
Loads a plugin from the plugin folder."""
|
||||
# Note: reload capability is acceptable here, because all it actually does is call
|
||||
# load after unload.
|
||||
permissions.checkPermissions(irc, source, ['core.load', 'core.reload'])
|
||||
permissions.check_permissions(irc, source, ['core.load', 'core.reload'])
|
||||
|
||||
try:
|
||||
name = args[0]
|
||||
@ -109,9 +39,9 @@ def load(irc, source, args):
|
||||
if name in world.plugins:
|
||||
irc.reply("Error: %r is already loaded." % name)
|
||||
return
|
||||
log.info('(%s) Loading plugin %r for %s', irc.name, name, irc.getHostmask(source))
|
||||
log.info('(%s) Loading plugin %r for %s', irc.name, name, irc.get_hostmask(source))
|
||||
try:
|
||||
world.plugins[name] = pl = utils.loadPlugin(name)
|
||||
world.plugins[name] = pl = utils._load_plugin(name)
|
||||
except ImportError as e:
|
||||
if str(e) == ('No module named %r' % name):
|
||||
log.exception('Failed to load plugin %r: The plugin could not be found.', name)
|
||||
@ -129,7 +59,7 @@ def unload(irc, source, args):
|
||||
"""<plugin name>.
|
||||
|
||||
Unloads a currently loaded plugin."""
|
||||
permissions.checkPermissions(irc, source, ['core.unload', 'core.reload'])
|
||||
permissions.check_permissions(irc, source, ['core.unload', 'core.reload'])
|
||||
|
||||
try:
|
||||
name = args[0]
|
||||
@ -142,7 +72,7 @@ def unload(irc, source, args):
|
||||
modulename = utils.PLUGIN_PREFIX + name
|
||||
|
||||
if name in world.plugins:
|
||||
log.info('(%s) Unloading plugin %r for %s', irc.name, name, irc.getHostmask(source))
|
||||
log.info('(%s) Unloading plugin %r for %s', irc.name, name, irc.get_hostmask(source))
|
||||
pl = world.plugins[name]
|
||||
log.debug('sys.getrefcount of plugin %s is %s', pl, sys.getrefcount(pl))
|
||||
|
||||
@ -162,12 +92,14 @@ def unload(irc, source, args):
|
||||
del world.services['pylink'].commands[cmdname]
|
||||
|
||||
# Remove any command hooks set by the plugin.
|
||||
for hookname, hookfuncs in world.hooks.copy().items():
|
||||
for hookfunc in hookfuncs:
|
||||
for hookname, hookpairs in world.hooks.copy().items():
|
||||
for hookpair in hookpairs:
|
||||
hookfunc = hookpair[1]
|
||||
if hookfunc.__module__ == modulename:
|
||||
world.hooks[hookname].remove(hookfunc)
|
||||
log.debug('Trying to remove hook func %s (%s) from plugin %s', hookfunc, hookname, modulename)
|
||||
world.hooks[hookname].remove(hookpair)
|
||||
# If the hookfuncs list is empty, remove it.
|
||||
if not hookfuncs:
|
||||
if not hookpairs:
|
||||
del world.hooks[hookname]
|
||||
|
||||
# Call the die() function in the plugin, if present.
|
||||
@ -215,11 +147,10 @@ def rehash(irc, source, args):
|
||||
Reloads the configuration file for PyLink, (dis)connecting added/removed networks.
|
||||
|
||||
Note: plugins must be manually reloaded."""
|
||||
permissions.checkPermissions(irc, source, ['core.rehash'])
|
||||
permissions.check_permissions(irc, source, ['core.rehash'])
|
||||
try:
|
||||
control._rehash()
|
||||
control.rehash()
|
||||
except Exception as e: # Something went wrong, abort.
|
||||
log.exception("Error REHASHing config: ")
|
||||
irc.reply("Error loading configuration file: %s: %s" % (type(e).__name__, e))
|
||||
return
|
||||
else:
|
||||
@ -230,5 +161,5 @@ def clearqueue(irc, source, args):
|
||||
"""takes no arguments.
|
||||
|
||||
Clears the outgoing text queue for the current connection."""
|
||||
permissions.checkPermissions(irc, source, ['core.clearqueue'])
|
||||
irc.queue.queue.clear()
|
||||
permissions.check_permissions(irc, source, ['core.clearqueue'])
|
||||
irc._queue.queue.clear()
|
||||
|
@ -41,10 +41,10 @@ def account(irc, host, uid):
|
||||
homenet, realuid)
|
||||
return False
|
||||
|
||||
slogin = irc.toLower(userobj.services_account)
|
||||
slogin = irc.to_lower(userobj.services_account)
|
||||
|
||||
# Split the given exttarget host into parts, so we know how many to look for.
|
||||
groups = list(map(irc.toLower, host.split(':')))
|
||||
groups = list(map(irc.to_lower, host.split(':')))
|
||||
log.debug('(%s) exttargets.account: groups to match: %s', irc.name, groups)
|
||||
|
||||
if len(groups) == 1:
|
||||
@ -74,10 +74,10 @@ def ircop(irc, host, uid):
|
||||
|
||||
if len(groups) == 1:
|
||||
# 1st scenario.
|
||||
return irc.isOper(uid, allowAuthed=False)
|
||||
return irc.is_oper(uid)
|
||||
else:
|
||||
# 2nd scenario. Use matchHost (ircmatch) to match the opertype glob to the opertype.
|
||||
return irc.matchHost(groups[1], irc.users[uid].opertype)
|
||||
# 2nd scenario. Use match_host (ircmatch) to match the opertype glob to the opertype.
|
||||
return irc.match_host(groups[1], irc.users[uid].opertype)
|
||||
|
||||
@bind
|
||||
def server(irc, host, uid):
|
||||
@ -93,10 +93,10 @@ def server(irc, host, uid):
|
||||
log.debug('(%s) exttargets.server: groups to match: %s', irc.name, groups)
|
||||
|
||||
if len(groups) >= 2:
|
||||
sid = irc.getServer(uid)
|
||||
sid = irc.get_server(uid)
|
||||
query = groups[1]
|
||||
# Return True if the SID matches the query or the server's name glob matches it.
|
||||
return sid == query or irc.matchHost(query, irc.getFriendlyName(sid))
|
||||
return sid == query or irc.match_host(query, irc.get_friendly_name(sid))
|
||||
# $server alone is invalid. Don't match anything.
|
||||
return False
|
||||
|
||||
@ -118,12 +118,16 @@ def channel(irc, host, uid):
|
||||
except IndexError: # No channel given, abort.
|
||||
return False
|
||||
|
||||
if channel not in irc.channels:
|
||||
# Channel doesn't even exist...
|
||||
return False
|
||||
|
||||
if len(groups) == 2:
|
||||
# Just #channel was given as query
|
||||
return uid in irc.channels[channel].users
|
||||
elif len(groups) >= 3:
|
||||
# For things like #channel:op, check if the query is in the user's prefix modes.
|
||||
return (uid in irc.channels[channel].users) and (groups[2].lower() in irc.channels[channel].getPrefixModes(uid))
|
||||
return (uid in irc.channels[channel].users) and (groups[2].lower() in irc.channels[channel].get_prefix_modes(uid))
|
||||
|
||||
@bind
|
||||
def pylinkacc(irc, host, uid):
|
||||
@ -134,8 +138,8 @@ def pylinkacc(irc, host, uid):
|
||||
$pylinkacc -> Returns True if the target is logged in to PyLink.
|
||||
$pylinkacc:accountname -> Returns True if the target's PyLink login matches the one given.
|
||||
"""
|
||||
login = irc.toLower(irc.users[uid].account)
|
||||
groups = list(map(irc.toLower, host.split(':')))
|
||||
login = irc.to_lower(irc.users[uid].account)
|
||||
groups = list(map(irc.to_lower, host.split(':')))
|
||||
log.debug('(%s) exttargets.pylinkacc: groups to match: %s', irc.name, groups)
|
||||
|
||||
if len(groups) == 1:
|
||||
@ -187,6 +191,36 @@ def exttarget_and(irc, host, uid):
|
||||
targets = targets[1:-1]
|
||||
targets = list(filter(None, targets.split('+')))
|
||||
log.debug('exttargets_and: using raw subtargets list %r (original query=%r)', targets, host)
|
||||
# Wrap every subtarget into irc.matchHost and return True if all subtargets return True.
|
||||
return all(map(lambda sub_exttarget: irc.matchHost(sub_exttarget, uid), targets))
|
||||
# Wrap every subtarget into irc.match_host and return True if all subtargets return True.
|
||||
return all(map(lambda sub_exttarget: irc.match_host(sub_exttarget, uid), targets))
|
||||
world.exttarget_handlers['and'] = exttarget_and
|
||||
|
||||
@bind
|
||||
def realname(irc, host, uid):
|
||||
"""
|
||||
$realname exttarget handler. This takes one argument: a glob, which is compared case-insensitively to the user's real name.
|
||||
|
||||
Examples:
|
||||
$realname:*James* -> matches anyone with "James" in their real name.
|
||||
"""
|
||||
groups = host.split(':')
|
||||
if len(groups) >= 2:
|
||||
return irc.match_host(groups[1], irc.users[uid].realname)
|
||||
|
||||
@bind
|
||||
def service(irc, host, uid):
|
||||
"""
|
||||
$service exttarget handler. This takes one optional argument: a glob, which is compared case-insensitively to the target user's service name (if present).
|
||||
|
||||
Examples:
|
||||
$service -> Matches any PyLink service bot.
|
||||
$service:automode -> Matches the Automode service bot.
|
||||
"""
|
||||
if not irc.users[uid].service:
|
||||
return False
|
||||
|
||||
groups = host.split(':')
|
||||
|
||||
if len(groups) >= 2:
|
||||
return irc.match_host(groups[1], irc.users[uid].service)
|
||||
return True # It *is* a service bot because of the check at the top.
|
||||
|
@ -11,10 +11,10 @@ def handle_whois(irc, source, command, args):
|
||||
target = args['target']
|
||||
user = irc.users.get(target)
|
||||
|
||||
f = lambda num, source, text: irc.proto.numeric(irc.sid, num, source, text)
|
||||
f = lambda num, source, text: irc.numeric(irc.sid, num, source, text)
|
||||
|
||||
# Get the server that the target is on.
|
||||
server = irc.getServer(target)
|
||||
server = irc.get_server(target)
|
||||
|
||||
if user is None: # User doesn't exist
|
||||
# <- :42X 401 7PYAAAAAB GL- :No such nick/channel
|
||||
@ -22,8 +22,8 @@ def handle_whois(irc, source, command, args):
|
||||
f(401, source, "%s :No such nick/channel" % nick)
|
||||
else:
|
||||
nick = user.nick
|
||||
sourceisOper = ('o', None) in irc.users[source].modes
|
||||
sourceisBot = (irc.umodes.get('bot'), None) in irc.users[source].modes
|
||||
source_is_oper = ('o', None) in irc.users[source].modes
|
||||
source_is_bot = (irc.umodes.get('bot'), None) in irc.users[source].modes
|
||||
|
||||
# Get the full network name.
|
||||
netname = irc.serverdata.get('netname', irc.name)
|
||||
@ -35,7 +35,7 @@ def handle_whois(irc, source, command, args):
|
||||
# 319: RPL_WHOISCHANNELS; Show public channels of the target, respecting
|
||||
# hidechans umodes for non-oper callers.
|
||||
isHideChans = (irc.umodes.get('hidechans'), None) in user.modes
|
||||
if (not isHideChans) or (isHideChans and sourceisOper):
|
||||
if (not isHideChans) or (isHideChans and source_is_oper):
|
||||
public_chans = []
|
||||
for chan in user.channels:
|
||||
c = irc.channels[chan]
|
||||
@ -44,13 +44,13 @@ def handle_whois(irc, source, command, args):
|
||||
|
||||
if ((irc.cmodes.get('secret'), None) in c.modes or \
|
||||
(irc.cmodes.get('private'), None) in c.modes) \
|
||||
and not (sourceisOper or source in c.users):
|
||||
and not (source_is_oper or source in c.users):
|
||||
continue
|
||||
|
||||
# Show the highest prefix mode like a regular IRCd does, if there are any.
|
||||
prefixes = c.getPrefixModes(target)
|
||||
prefixes = c.get_prefix_modes(target)
|
||||
if prefixes:
|
||||
highest = prefixes[-1]
|
||||
highest = prefixes[0]
|
||||
|
||||
# Fetch the prefix mode letter from the named mode.
|
||||
modechar = irc.cmodes[highest]
|
||||
@ -74,19 +74,27 @@ def handle_whois(irc, source, command, args):
|
||||
# 2) +H is set, but the caller is oper
|
||||
# 3) +H is set, but whois_use_hideoper is disabled in config
|
||||
isHideOper = (irc.umodes.get('hideoper'), None) in user.modes
|
||||
if (not isHideOper) or (isHideOper and sourceisOper) or \
|
||||
(isHideOper and not conf.conf['bot'].get('whois_use_hideoper', True)):
|
||||
if (not isHideOper) or (isHideOper and source_is_oper) or \
|
||||
(isHideOper and not conf.conf['pylink'].get('whois_use_hideoper', True)):
|
||||
opertype = user.opertype
|
||||
|
||||
# Let's be gramatically correct. (If the opertype starts with a vowel,
|
||||
# write "an Operator" instead of "a Operator")
|
||||
n = 'n' if user.opertype[0].lower() in 'aeiou' else ''
|
||||
n = 'n' if opertype[0].lower() in 'aeiou' else ''
|
||||
|
||||
f(313, source, "%s :is a%s %s" % (nick, n, user.opertype))
|
||||
# Remove the "(on $network)" bit in relay oper types if the target network is the
|
||||
# same - this prevents duplicate text such as "GL/ovd is a Network Administrator
|
||||
# (on OVERdrive-IRC) on OVERdrive-IRC" from showing.
|
||||
# XXX: does this post-processing really belong here?
|
||||
opertype = opertype.replace(' (on %s)' % irc.get_full_network_name(), '')
|
||||
|
||||
f(313, source, "%s :is a%s %s" % (nick, n, opertype))
|
||||
|
||||
# 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd to show user modes.
|
||||
# Only show this to opers!
|
||||
if sourceisOper:
|
||||
if source_is_oper:
|
||||
f(378, source, "%s :is connecting from %s@%s %s" % (nick, user.ident, user.realhost, user.ip))
|
||||
f(379, source, '%s :is using modes %s' % (nick, irc.joinModes(user.modes, sort=True)))
|
||||
f(379, source, '%s :is using modes %s' % (nick, irc.join_modes(user.modes, sort=True)))
|
||||
|
||||
# 301: used to show away information if present
|
||||
away_text = user.away
|
||||
@ -98,10 +106,14 @@ def handle_whois(irc, source, command, args):
|
||||
# Show botmode info in WHOIS.
|
||||
f(335, source, "%s :is a bot" % nick)
|
||||
|
||||
# :charybdis.midnight.vpn 317 GL GL 1946 1499867833 :seconds idle, signon time
|
||||
if irc.get_service_bot(target) and conf.conf['pylink'].get('whois_show_startup_time', True):
|
||||
f(317, source, "%s 0 %s :seconds idle (placeholder), signon time" % (nick, irc.start_ts))
|
||||
|
||||
# Call custom WHOIS handlers via the PYLINK_CUSTOM_WHOIS hook, unless the
|
||||
# caller is marked a bot and the whois_show_extensions_to_bots option is False
|
||||
if (sourceisBot and conf.conf['bot'].get('whois_show_extensions_to_bots')) or (not sourceisBot):
|
||||
irc.callHooks([source, 'PYLINK_CUSTOM_WHOIS', {'target': target, 'server': server}])
|
||||
if (source_is_bot and conf.conf['pylink'].get('whois_show_extensions_to_bots')) or (not source_is_bot):
|
||||
irc.call_hooks([source, 'PYLINK_CUSTOM_WHOIS', {'target': target, 'server': server}])
|
||||
else:
|
||||
log.debug('(%s) coremods.handlers.handle_whois: skipping custom whois handlers because '
|
||||
'caller %s is marked as a bot', irc.name, source)
|
||||
@ -116,15 +128,15 @@ def handle_mode(irc, source, command, args):
|
||||
modes = args['modes']
|
||||
# If the sender is not a PyLink client, and the target IS a protected
|
||||
# client, revert any forced deoper attempts.
|
||||
if irc.isInternalClient(target) and not irc.isInternalClient(source):
|
||||
if ('-o', None) in modes and (target == irc.pseudoclient.uid or not irc.isManipulatableClient(target)):
|
||||
irc.proto.mode(irc.sid, target, {('+o', None)})
|
||||
if irc.is_internal_client(target) and not irc.is_internal_client(source):
|
||||
if ('-o', None) in modes and (target == irc.pseudoclient.uid or not irc.is_manipulatable_client(target)):
|
||||
irc.mode(irc.sid, target, {('+o', None)})
|
||||
utils.add_hook(handle_mode, 'MODE')
|
||||
|
||||
def handle_operup(irc, source, command, args):
|
||||
"""Logs successful oper-ups on networks."""
|
||||
otype = args.get('text', 'IRC Operator')
|
||||
log.debug("(%s) Successful oper-up (opertype %r) from %s", irc.name, otype, irc.getHostmask(source))
|
||||
log.debug("(%s) Successful oper-up (opertype %r) from %s", irc.name, otype, irc.get_hostmask(source))
|
||||
irc.users[source].opertype = otype
|
||||
|
||||
utils.add_hook(handle_operup, 'CLIENT_OPERED')
|
||||
@ -143,11 +155,58 @@ def handle_version(irc, source, command, args):
|
||||
"""Handles requests for the PyLink server version."""
|
||||
# 351 syntax is usually "<server version>. <server hostname> :<anything else you want to add>
|
||||
fullversion = irc.version()
|
||||
irc.proto.numeric(irc.sid, 351, source, fullversion)
|
||||
irc.numeric(irc.sid, 351, source, fullversion)
|
||||
utils.add_hook(handle_version, 'VERSION')
|
||||
|
||||
def handle_time(irc, source, command, args):
|
||||
"""Handles requests for the PyLink server time."""
|
||||
timestring = time.ctime()
|
||||
irc.proto.numeric(irc.sid, 391, source, '%s :%s' % (irc.hostname(), timestring))
|
||||
irc.numeric(irc.sid, 391, source, '%s :%s' % (irc.hostname(), timestring))
|
||||
utils.add_hook(handle_time, 'TIME')
|
||||
|
||||
def _state_cleanup_core(irc, source, channel):
|
||||
"""
|
||||
Handles PART and KICK on clientbot-like networks (where only the users and channels we see are available)
|
||||
by deleting channels when we leave and users when they leave all shared channels.
|
||||
"""
|
||||
if irc.has_cap('visible-state-only'):
|
||||
# Delete channels that we were removed from.
|
||||
if irc.pseudoclient and source == irc.pseudoclient.uid:
|
||||
log.debug('(%s) state_cleanup: removing channel %s since we have left', irc.name, channel)
|
||||
del irc._channels[channel]
|
||||
|
||||
# Delete users no longer sharing a channel with us.
|
||||
if not irc.users[source].channels:
|
||||
log.debug('(%s) state_cleanup: removing user %s/%s who no longer shares a channel with us',
|
||||
irc.name, source, irc.users[source].nick)
|
||||
irc._remove_client(source)
|
||||
|
||||
# Clear empty non-permanent channels.
|
||||
if channel in irc.channels and not (irc._channels[channel].users or ((irc.cmodes.get('permanent'), None) \
|
||||
in irc._channels[channel].modes)):
|
||||
log.debug('(%s) state_cleanup: removing empty channel %s', irc.name, channel)
|
||||
del irc._channels[channel]
|
||||
|
||||
def _state_cleanup_part(irc, source, command, args):
|
||||
for channel in args['channels']:
|
||||
_state_cleanup_core(irc, source, channel)
|
||||
utils.add_hook(_state_cleanup_part, 'PART', priority=-100)
|
||||
|
||||
def _state_cleanup_kick(irc, source, command, args):
|
||||
_state_cleanup_core(irc, args['target'], args['channel'])
|
||||
utils.add_hook(_state_cleanup_kick, 'KICK', priority=-100)
|
||||
|
||||
def _state_cleanup_mode(irc, source, command, args):
|
||||
"""
|
||||
Cleans up and removes empty channels when -P (permanent mode) is removed from them.
|
||||
"""
|
||||
target = args['target']
|
||||
if target in irc.channels and 'permanent' in irc.cmodes:
|
||||
c = irc.channels[target]
|
||||
mode = '-%s' % irc.cmodes['permanent']
|
||||
|
||||
if (not c.users) and (mode, None) in args['modes']:
|
||||
log.debug('(%s) _state_cleanup_mode: deleting empty channel %s as %s was set', irc.name, target, mode)
|
||||
del irc._channels[target]
|
||||
return False # Block further hooks from running
|
||||
utils.add_hook(_state_cleanup_mode, 'MODE', priority=10000)
|
||||
|
@ -18,37 +18,39 @@ if CryptContext:
|
||||
sha256_crypt__default_rounds=180000,
|
||||
sha512_crypt__default_rounds=90000)
|
||||
|
||||
def checkLogin(user, password):
|
||||
"""Checks whether the given user and password is a valid combination."""
|
||||
accounts = conf.conf['login'].get('accounts')
|
||||
if not accounts:
|
||||
# No accounts specified, return.
|
||||
return False
|
||||
|
||||
# Lowercase account names to make them case insensitive. TODO: check for
|
||||
# duplicates.
|
||||
user = user.lower()
|
||||
accounts = {k.lower(): v for k, v in accounts.items()}
|
||||
def _get_account(accountname):
|
||||
"""
|
||||
Returns the login data block for the given account name (case-insensitive), or False if none
|
||||
exists.
|
||||
"""
|
||||
accounts = {k.lower(): v for k, v in
|
||||
conf.conf['login'].get('accounts', {}).items()}
|
||||
|
||||
try:
|
||||
account = accounts[user]
|
||||
except KeyError: # Invalid combination
|
||||
return accounts[accountname.lower()]
|
||||
except KeyError:
|
||||
return False
|
||||
else:
|
||||
|
||||
def check_login(user, password):
|
||||
"""Checks whether the given user and password is a valid combination."""
|
||||
account = _get_account(user)
|
||||
|
||||
if account:
|
||||
passhash = account.get('password')
|
||||
if not passhash:
|
||||
# No password given, return. XXX: we should allow plugins to override
|
||||
# this in the future.
|
||||
return False
|
||||
|
||||
# Encryption in account passwords is optional (to not break backwards
|
||||
# compatibility).
|
||||
# Hashing in account passwords is optional.
|
||||
if account.get('encrypted', False):
|
||||
return verifyHash(password, passhash)
|
||||
return verify_hash(password, passhash)
|
||||
else:
|
||||
return password == passhash
|
||||
|
||||
def verifyHash(password, passhash):
|
||||
return False
|
||||
|
||||
def verify_hash(password, passhash):
|
||||
"""Checks whether the password given matches the hash."""
|
||||
if password:
|
||||
if not pwd_context:
|
||||
@ -57,3 +59,66 @@ def verifyHash(password, passhash):
|
||||
|
||||
return pwd_context.verify(password, passhash)
|
||||
return False # No password given!
|
||||
|
||||
def _irc_try_login(irc, source, username, skip_checks=False):
|
||||
"""Internal function to process logins via IRC."""
|
||||
if irc.is_internal_client(source):
|
||||
irc.error("Cannot use 'identify' via a command proxy.")
|
||||
return
|
||||
|
||||
if not skip_checks:
|
||||
logindata = _get_account(username)
|
||||
|
||||
network_filter = logindata.get('networks')
|
||||
require_oper = logindata.get('require_oper', False)
|
||||
hosts_filter = logindata.get('hosts', [])
|
||||
|
||||
if network_filter and irc.name not in network_filter:
|
||||
log.warning("(%s) Failed login to %r from %s (wrong network: networks filter says %r but we got %r)",
|
||||
irc.name, username, irc.get_hostmask(source), ', '.join(network_filter), irc.name)
|
||||
raise utils.NotAuthorizedError("Account is not authorized to login on this network.")
|
||||
|
||||
elif require_oper and not irc.is_oper(source):
|
||||
log.warning("(%s) Failed login to %r from %s (needs oper)", irc.name, username, irc.get_hostmask(source))
|
||||
raise utils.NotAuthorizedError("You must be opered.")
|
||||
|
||||
elif hosts_filter and not any(irc.match_host(host, source) for host in hosts_filter):
|
||||
log.warning("(%s) Failed login to %r from %s (hostname mismatch)", irc.name, username, irc.get_hostmask(source))
|
||||
raise utils.NotAuthorizedError("Hostname mismatch.")
|
||||
|
||||
irc.users[source].account = username
|
||||
irc.reply('Successfully logged in as %s.' % username)
|
||||
log.info("(%s) Successful login to %r by %s",
|
||||
irc.name, username, irc.get_hostmask(source))
|
||||
return True
|
||||
|
||||
def identify(irc, source, args):
|
||||
"""<username> <password>
|
||||
|
||||
Logs in to PyLink using the configured administrator account."""
|
||||
if irc.is_channel(irc.called_in):
|
||||
irc.reply('Error: This command must be sent in private. '
|
||||
'(Would you really type a password inside a channel?)')
|
||||
return
|
||||
try:
|
||||
username, password = args[0], args[1]
|
||||
except IndexError:
|
||||
irc.reply('Error: Not enough arguments.')
|
||||
return
|
||||
|
||||
# Process new-style accounts.
|
||||
if check_login(username, password):
|
||||
_irc_try_login(irc, source, username)
|
||||
return
|
||||
|
||||
# Process legacy logins (login:user).
|
||||
if username.lower() == conf.conf['login'].get('user', '').lower() and password == conf.conf['login'].get('password'):
|
||||
realuser = conf.conf['login']['user']
|
||||
_irc_try_login(irc, source, realuser, skip_checks=True)
|
||||
return
|
||||
|
||||
# Username not found or password incorrect.
|
||||
log.warning("(%s) Failed login to %r from %s", irc.name, username, irc.get_hostmask(source))
|
||||
raise utils.NotAuthorizedError('Bad username or password.')
|
||||
|
||||
utils.add_cmd(identify, aliases=('login', 'id'))
|
||||
|
@ -7,75 +7,57 @@ import threading
|
||||
|
||||
# Global variables: these store mappings of hostmasks/exttargets to lists of permissions each target has.
|
||||
default_permissions = defaultdict(set)
|
||||
permissions = defaultdict(set)
|
||||
|
||||
# Only allow one thread to change the permissions index at once.
|
||||
permissions_lock = threading.Lock()
|
||||
|
||||
from pylinkirc import conf, utils
|
||||
from pylinkirc.log import log
|
||||
|
||||
def resetPermissions():
|
||||
"""
|
||||
Loads the permissions specified in the permissions: block of the PyLink configuration,
|
||||
if such a block exists. Otherwise, fallback to the default permissions specified by plugins.
|
||||
"""
|
||||
with permissions_lock:
|
||||
global permissions
|
||||
log.debug('permissions.resetPermissions: old perm list: %s', permissions)
|
||||
|
||||
new_permissions = default_permissions.copy()
|
||||
log.debug('permissions.resetPermissions: new_permissions %s', new_permissions)
|
||||
if not conf.conf.get('permissions_merge_defaults', True):
|
||||
log.debug('permissions.resetPermissions: clearing perm list due to permissions_merge_defaults set False.')
|
||||
new_permissions.clear()
|
||||
|
||||
# Convert all perm lists to sets.
|
||||
for k, v in conf.conf.get('permissions', {}).items():
|
||||
new_permissions[k] |= set(v)
|
||||
|
||||
log.debug('permissions.resetPermissions: new_permissions %s', new_permissions)
|
||||
permissions.clear()
|
||||
permissions.update(new_permissions)
|
||||
log.debug('permissions.resetPermissions: new perm list: %s', permissions)
|
||||
|
||||
def addDefaultPermissions(perms):
|
||||
def add_default_permissions(perms):
|
||||
"""Adds default permissions to the index."""
|
||||
with permissions_lock:
|
||||
global default_permissions
|
||||
for target, permlist in perms.items():
|
||||
default_permissions[target] |= set(permlist)
|
||||
global default_permissions
|
||||
for target, permlist in perms.items():
|
||||
default_permissions[target] |= set(permlist)
|
||||
addDefaultPermissions = add_default_permissions
|
||||
|
||||
def removeDefaultPermissions(perms):
|
||||
def remove_default_permissions(perms):
|
||||
"""Remove default permissions from the index."""
|
||||
with permissions_lock:
|
||||
global default_permissions
|
||||
for target, permlist in perms.items():
|
||||
default_permissions[target] -= set(permlist)
|
||||
global default_permissions
|
||||
for target, permlist in perms.items():
|
||||
default_permissions[target] -= set(permlist)
|
||||
removeDefaultPermissions = remove_default_permissions
|
||||
|
||||
def checkPermissions(irc, uid, perms, also_show=[]):
|
||||
def check_permissions(irc, uid, perms, also_show=[]):
|
||||
"""
|
||||
Checks permissions of the caller. If the caller has any of the permissions listed in perms,
|
||||
this function returns True. Otherwise, NotAuthorizedError is raised.
|
||||
"""
|
||||
# For old (< 1.1 login blocks):
|
||||
# If the user is logged in, they automatically have all permissions.
|
||||
if irc.matchHost('$pylinkacc', uid) and conf.conf['login'].get('user'):
|
||||
if irc.match_host('$pylinkacc', uid) and conf.conf['login'].get('user'):
|
||||
log.debug('permissions: overriding permissions check for old-style admin user %s',
|
||||
irc.getHostmask(uid))
|
||||
irc.get_hostmask(uid))
|
||||
return True
|
||||
|
||||
# Iterate over all hostmask->permission list mappings.
|
||||
for host, permlist in permissions.copy().items():
|
||||
permissions = defaultdict(set)
|
||||
# Enumerate the configured permissions list.
|
||||
for k, v in (conf.conf.get('permissions') or {}).items():
|
||||
permissions[k] |= set(v)
|
||||
|
||||
# Merge in default permissions if enabled.
|
||||
if conf.conf.get('permissions_merge_defaults', True):
|
||||
for k, v in default_permissions.items():
|
||||
permissions[k] |= v
|
||||
|
||||
for host, permlist in permissions.items():
|
||||
log.debug('permissions: permlist for %s: %s', host, permlist)
|
||||
if irc.matchHost(host, uid):
|
||||
if irc.match_host(host, uid):
|
||||
# Now, iterate over all the perms we are looking for.
|
||||
for perm in permlist:
|
||||
# Use irc.matchHost to expand globs in an IRC-case insensitive and wildcard
|
||||
# Use irc.match_host to expand globs in an IRC-case insensitive and wildcard
|
||||
# friendly way. e.g. 'xyz.*.#Channel\' will match 'xyz.manage.#channel|' on IRCds
|
||||
# using the RFC1459 casemapping.
|
||||
log.debug('permissions: checking if %s glob matches anything in %s', perm, permlist)
|
||||
if any(irc.matchHost(perm, p) for p in perms):
|
||||
if any(irc.match_host(perm, p) for p in perms):
|
||||
return True
|
||||
raise utils.NotAuthorizedError("You are missing one of the following permissions: %s" %
|
||||
(', '.join(perms+also_show)))
|
||||
checkPermissions = check_permissions
|
||||
|
@ -14,7 +14,7 @@ def spawn_service(irc, source, command, args):
|
||||
# Service name
|
||||
name = args['name']
|
||||
|
||||
if name != 'pylink' and not irc.proto.hasCap('can-spawn-clients'):
|
||||
if name != 'pylink' and not irc.has_cap('can-spawn-clients'):
|
||||
log.debug("(%s) Not spawning service %s because the server doesn't support spawning clients",
|
||||
irc.name, name)
|
||||
return
|
||||
@ -22,60 +22,59 @@ def spawn_service(irc, source, command, args):
|
||||
# Get the ServiceBot object.
|
||||
sbot = world.services[name]
|
||||
|
||||
# Look up the nick or ident in the following order:
|
||||
# 1) Network specific nick/ident settings for this service (servers::irc.name::servicename_nick)
|
||||
# 2) Global settings for this service (servicename::nick)
|
||||
# 3) The preferred nick/ident combination defined by the plugin (sbot.nick / sbot.ident)
|
||||
# 4) The literal service name.
|
||||
# settings, and then falling back to the literal service name.
|
||||
sbconf = conf.conf.get(name, {})
|
||||
nick = irc.serverdata.get("%s_nick" % name) or sbconf.get('nick') or sbot.nick or name
|
||||
ident = irc.serverdata.get("%s_ident" % name) or sbconf.get('ident') or sbot.ident or name
|
||||
old_userobj = irc.users.get(sbot.uids.get(irc.name))
|
||||
if old_userobj and old_userobj.service:
|
||||
# A client already exists, so don't respawn it.
|
||||
log.debug('(%s) spawn_service: Not respawning service %r as service client %r already exists.', irc.name, name,
|
||||
irc.pseudoclient.nick)
|
||||
return
|
||||
|
||||
# Determine host the same way as above, except fall back to server hostname.
|
||||
host = irc.serverdata.get("%s_host" % name) or sbconf.get('host') or irc.hostname()
|
||||
|
||||
# Determine realname the same way as above, except fall back to pylink:realname.
|
||||
realname = irc.serverdata.get("%s_realname" % name) or sbconf.get('realname') or conf.conf['bot']['realname']
|
||||
|
||||
# Spawning service clients with these umodes where supported. servprotect usage is a
|
||||
# configuration option.
|
||||
preferred_modes = ['oper', 'hideoper', 'hidechans', 'invisible', 'bot']
|
||||
modes = []
|
||||
|
||||
if conf.conf['bot'].get('protect_services'):
|
||||
preferred_modes.append('servprotect')
|
||||
|
||||
for mode in preferred_modes:
|
||||
mode = irc.umodes.get(mode)
|
||||
if mode:
|
||||
modes.append((mode, None))
|
||||
|
||||
# Track the service's UIDs on each network.
|
||||
log.debug('(%s) spawn_service: Using nick %s for service %s', irc.name, nick, name)
|
||||
u = irc.nickToUid(nick)
|
||||
if u and irc.isInternalClient(u): # If an internal client exists, reuse it.
|
||||
log.debug('(%s) spawn_service: Using existing client %s/%s', irc.name, u, nick)
|
||||
userobj = irc.users[u]
|
||||
if name == 'pylink' and irc.pseudoclient:
|
||||
# irc.pseudoclient already exists, reuse values from it but
|
||||
# spawn a new client. This is used for protocols like Clientbot,
|
||||
# so that they can override the main service nick, among other things.
|
||||
log.debug('(%s) spawn_service: Using existing nick %r for service %r', irc.name, irc.pseudoclient.nick, name)
|
||||
userobj = irc.pseudoclient
|
||||
userobj.opertype = "PyLink Service"
|
||||
userobj.manipulatable = sbot.manipulatable
|
||||
else:
|
||||
log.debug('(%s) spawn_service: Spawning new client %s', irc.name, nick)
|
||||
userobj = irc.proto.spawnClient(nick, ident, host, modes=modes, opertype="PyLink Service",
|
||||
realname=realname, manipulatable=sbot.manipulatable)
|
||||
# No client exists, spawn a new one
|
||||
nick = sbot.get_nick(irc)
|
||||
ident = sbot.get_ident(irc)
|
||||
host = sbot.get_host(irc)
|
||||
realname = sbot.get_realname(irc)
|
||||
|
||||
# Store the service name in the IrcUser object for easier access.
|
||||
# Spawning service clients with these umodes where supported. servprotect usage is a
|
||||
# configuration option.
|
||||
preferred_modes = ['oper', 'hideoper', 'hidechans', 'invisible', 'bot']
|
||||
modes = []
|
||||
|
||||
if conf.conf['pylink'].get('protect_services'):
|
||||
preferred_modes.append('servprotect')
|
||||
|
||||
for mode in preferred_modes:
|
||||
mode = irc.umodes.get(mode)
|
||||
if mode:
|
||||
modes.append((mode, None))
|
||||
|
||||
# Track the service's UIDs on each network.
|
||||
log.debug('(%s) spawn_service: Spawning new client %s for service %s', irc.name, nick, name)
|
||||
userobj = irc.spawn_client(nick, ident, host, modes=modes, opertype="PyLink Service",
|
||||
realname=realname, manipulatable=sbot.manipulatable)
|
||||
|
||||
# Store the service name in the User object for easier access.
|
||||
userobj.service = name
|
||||
|
||||
sbot.uids[irc.name] = u = userobj.uid
|
||||
|
||||
# Special case: if this is the main PyLink client being spawned,
|
||||
# assign this as irc.pseudoclient.
|
||||
if name == 'pylink':
|
||||
if name == 'pylink' and not irc.pseudoclient:
|
||||
log.debug('(%s) spawn_service: irc.pseudoclient set to UID %s', irc.name, u)
|
||||
irc.pseudoclient = userobj
|
||||
|
||||
channels = set(irc.serverdata.get(name+'_channels', [])) | set(irc.serverdata.get('channels', [])) | \
|
||||
sbot.extra_channels.get(irc.name, set())
|
||||
sbot.join(irc, channels)
|
||||
# Enumerate & join network defined channels.
|
||||
sbot.join(irc, sbot.get_persistent_channels(irc))
|
||||
|
||||
utils.add_hook(spawn_service, 'PYLINK_NEW_SERVICE')
|
||||
|
||||
@ -99,13 +98,15 @@ def handle_endburst(irc, source, command, args):
|
||||
for name, sbot in world.services.items():
|
||||
spawn_service(irc, source, command, {'name': name})
|
||||
|
||||
utils.add_hook(handle_endburst, 'ENDBURST')
|
||||
utils.add_hook(handle_endburst, 'ENDBURST', priority=500)
|
||||
|
||||
def handle_kill(irc, source, command, args):
|
||||
"""Handle KILLs to PyLink service bots, respawning them as needed."""
|
||||
target = args['target']
|
||||
if irc.pseudoclient and target == irc.pseudoclient.uid:
|
||||
irc.pseudoclient = None
|
||||
userdata = args.get('userdata')
|
||||
sbot = irc.getServiceBot(target)
|
||||
sbot = irc.get_service_bot(target)
|
||||
servicename = None
|
||||
|
||||
if userdata and hasattr(userdata, 'service'): # Look for the target's service name attribute
|
||||
@ -113,18 +114,58 @@ def handle_kill(irc, source, command, args):
|
||||
elif sbot: # Or their service bot instance
|
||||
servicename = sbot.name
|
||||
if servicename:
|
||||
log.debug('(%s) services_support: respawning service %s after KILL.', irc.name, servicename)
|
||||
log.info('(%s) Received kill to service %r (nick: %r) from %s (reason: %r).', irc.name, servicename,
|
||||
userdata.nick if userdata else irc.users[target].nick, irc.get_hostmask(source), args.get('text'))
|
||||
spawn_service(irc, source, command, {'name': servicename})
|
||||
|
||||
utils.add_hook(handle_kill, 'KILL')
|
||||
|
||||
def handle_join(irc, source, command, args):
|
||||
"""Monitors channel joins for dynamic service bot joining."""
|
||||
if irc.has_cap('visible-state-only'):
|
||||
# No-op on bot-only servers.
|
||||
return
|
||||
|
||||
channel = args['channel']
|
||||
users = irc.channels[channel].users
|
||||
for servicename, sbot in world.services.items():
|
||||
if channel in sbot.get_persistent_channels(irc) and \
|
||||
sbot.uids.get(irc.name) not in users:
|
||||
log.debug('(%s) Dynamically joining service %r to channel %r.', irc.name, servicename, channel)
|
||||
sbot.join(irc, channel)
|
||||
utils.add_hook(handle_join, 'JOIN')
|
||||
utils.add_hook(handle_join, 'PYLINK_SERVICE_JOIN')
|
||||
|
||||
def _services_dynamic_part(irc, channel):
|
||||
"""Dynamically removes service bots from empty channels."""
|
||||
if irc.has_cap('visible-state-only'):
|
||||
# No-op on bot-only servers.
|
||||
return
|
||||
|
||||
# If all remaining users in the channel are service bots, make them all part.
|
||||
if all(irc.get_service_bot(u) for u in irc.channels[channel].users):
|
||||
for u in irc.channels[channel].users.copy():
|
||||
sbot = irc.get_service_bot(u)
|
||||
if sbot:
|
||||
log.debug('(%s) Dynamically parting service %r from channel %r.', irc.name, sbot.name, channel)
|
||||
irc.part(u, channel)
|
||||
return True
|
||||
|
||||
def handle_part(irc, source, command, args):
|
||||
"""Monitors channel joins for dynamic service bot joining."""
|
||||
for channel in args['channels']:
|
||||
_services_dynamic_part(irc, channel)
|
||||
utils.add_hook(handle_part, 'PART')
|
||||
|
||||
def handle_kick(irc, source, command, args):
|
||||
"""Handle KICKs to the PyLink service bots, rejoining channels as needed."""
|
||||
kicked = args['target']
|
||||
channel = args['channel']
|
||||
sbot = irc.getServiceBot(kicked)
|
||||
if sbot:
|
||||
sbot.join(irc, channel)
|
||||
# Skip autorejoin routines if the channel is now empty.
|
||||
if not _services_dynamic_part(irc, channel):
|
||||
kicked = args['target']
|
||||
sbot = irc.get_service_bot(kicked)
|
||||
if sbot and channel in sbot.get_persistent_channels(irc):
|
||||
sbot.join(irc, channel)
|
||||
utils.add_hook(handle_kick, 'KICK')
|
||||
|
||||
def handle_commands(irc, source, command, args):
|
||||
@ -132,14 +173,14 @@ def handle_commands(irc, source, command, args):
|
||||
target = args['target']
|
||||
text = args['text']
|
||||
|
||||
sbot = irc.getServiceBot(target)
|
||||
sbot = irc.get_service_bot(target)
|
||||
if sbot:
|
||||
sbot.call_cmd(irc, source, text)
|
||||
|
||||
utils.add_hook(handle_commands, 'PRIVMSG')
|
||||
|
||||
# Register the main PyLink service. All command definitions MUST go after this!
|
||||
# TODO: be more specific, and possibly allow plugins to modify this to mention
|
||||
# TODO: be more specific in description, and possibly allow plugins to modify this to mention
|
||||
# their features?
|
||||
mydesc = "\x02PyLink\x02 provides extended network services for IRC."
|
||||
utils.registerService('pylink', desc=mydesc, manipulatable=True)
|
||||
utils.register_service('pylink', default_nick="PyLink", desc=mydesc, manipulatable=True)
|
||||
|
@ -5,7 +5,7 @@ This folder contains general documentation for PyLink IRC services.
|
||||
## Contents
|
||||
|
||||
- [PyLink FAQ (Frequently Asked Questions)](faq.md)
|
||||
- [PyLink Relay Tutorial & Oper Guide](pylink-opers.md)
|
||||
- [PyLink Relay Quick Start Guide](relay-quickstart.md)
|
||||
|
||||
----
|
||||
|
||||
@ -17,6 +17,7 @@ This folder contains general documentation for PyLink IRC services.
|
||||
|
||||
----
|
||||
|
||||
- [PyLink named modes tables](modelists/)
|
||||
- [Developer documentation](technical/)
|
||||
|
||||
There is also a Doxygen-powered API reference at https://pylink.github.io/
|
||||
|
@ -14,7 +14,9 @@ a:
|
||||
|
||||
### Custom Clientbot Styles
|
||||
|
||||
Custom Clientbot styles can be applied for any of Clientbot's supported events, by defining keys in the format `relay::clientbot_styles::<event name>`. See below for a list of supported events and their default values (as of 1.3.0).
|
||||
Custom Clientbot styles can be applied for any of Clientbot's supported events, by defining keys in the format `relay::clientbot_styles::<event name>`. As of 2.0-beta1, you can also set this per-network by defining options in the form `servers::<network name>::relay_clientbot_styles::<event names>` (Note: defining Clientbot styles locally will override the global `clientbot_styles` block and cause all values under it to be ignored for that network).
|
||||
|
||||
See below for a list of supported events and their default values (as of 2.0-beta1).
|
||||
|
||||
A common use case for this feature is to turn off or adjust colors/formatting; this is explicitly documented [below](#disabling-colorscontrol-codes).
|
||||
|
||||
@ -31,6 +33,7 @@ These options take template strings as documented here: https://docs.python.org/
|
||||
- For events that have a `$channel` field attached (e.g. JOIN, PART):
|
||||
- `$local_channel`: the *local* channel name (i.e. the channel on the clientbot network)
|
||||
- `$channel`: the real channel name on the sender's network
|
||||
- `$mode_prefix`: the highest prefix mode of the sender, if they are a user. This is normally either empty or one of (common prefix modes) `~&!@%+`.
|
||||
- For SJOIN, SQUIT:
|
||||
- `$nicks`: a comma-joined list of nicks that were bursted
|
||||
- `$colored_nicks`: a comma-joined list of each bursted nick, color hashed
|
||||
@ -85,3 +88,4 @@ This is a example clientbot_styles config block, which you can copy *into* your
|
||||
### Misc. options
|
||||
- `relay::clientbot_startup_delay`: Defines the amount of seconds Clientbot should wait after startup, before relaying any non-PRIVMSG events. This is used to prevent excess floods when the bot connects. Defaults to 5 seconds.
|
||||
- `servers::NETNAME::relay_force_slashes`: This network specific option forces Relay to use `/` in nickname separators. You should only use this option on TS6 or P10 variants that are less strict with nickname validation, as **it will cause protocol violations** on most IRCds. UnrealIRCd and InspIRCd users do not need to set this either, as `/` in nicks is automatically enabled.
|
||||
- `servers::NETNAME::relay_endburst_delay`: InspIRCd networks only: sets the endburst delay for relay subservers. If relay server bursts are causing +j (join flood) protection to trigger, raising this value can work around the issue.
|
||||
|
@ -1,32 +1,32 @@
|
||||
# Automode Tutorial
|
||||
|
||||
The Automode plugin was introduced in PyLink 0.9 as a simple way of managing channel access control lists with Relay. That said, it is not designed to entirely replace traditional IRC services such as ChanServ.
|
||||
The Automode plugin was introduced in PyLink 0.9 as a simple mechanism to manage channel access. That said, it is not designed to entirely replace traditional IRC services such as ChanServ.
|
||||
|
||||
## Starting steps
|
||||
|
||||
Upon loading the `automode` plugin, you should see an Automode service bot connect, using the name that you defined. This bot provides the commands used to manage access.
|
||||
Upon loading the `automode` plugin, you should see an Automode service bot connect, using the name that you defined (this guide uses the default, `Automode`). This service provides the commands used to manage access.
|
||||
|
||||
For a list of commands:
|
||||
- `/msg ModeBot help`
|
||||
- `/msg Automode help`
|
||||
|
||||
Adding access lists to a channel:
|
||||
- `/msg ModeBot setacc #channel [MASK] [MODE LIST]`
|
||||
- The mask can be a simple `nick!user@host` hostmask or any of the extended targets (exttargets) mentioned below. MODE LIST is a string of any prefix modes that you want to set (no `+` before needed), such as `qo`, `h`, or `ov`.
|
||||
Adding access to a channel:
|
||||
- `/msg Automode setacc #channel [MASK] [MODE LIST]`
|
||||
- The mask can be a simple `nick!user@host` hostmask or any of the extended targets (exttargets) mentioned below. MODE LIST is a string of any prefix modes that you want to set (no leading `+` needed): e.g. `qo`, `h`, or `ov`.
|
||||
|
||||
Removing access from a channel:
|
||||
- `/msg ModeBot delacc #channel [MASK]`
|
||||
- `/msg Automode delacc #channel [MASK]`
|
||||
|
||||
Listing access entries on a channel:
|
||||
- `/msg ModeBot listacc #channel`
|
||||
- `/msg Automode listacc #channel`
|
||||
|
||||
Applying all access entries on a channel (sync):
|
||||
- `/msg ModeBot syncacc #channel`
|
||||
- `/msg Automode syncacc #channel`
|
||||
|
||||
Clearing all access entries on a channel:
|
||||
- `/msg ModeBot clearacc #channel`
|
||||
- `/msg Automode clearacc #channel`
|
||||
|
||||
## Supported masks and extended targets
|
||||
Automode supports any hostmask or extended target implemented in PyLink; see the [Exttargets Guide](exttargets.md) for more details.
|
||||
Automode supports any hostmask or PyLink extended target; see the [Exttargets Guide](exttargets.md) for more details.
|
||||
|
||||
## Permissions
|
||||
|
||||
@ -34,4 +34,4 @@ See the [Permissions Reference](permissions-reference.md#automode) for a list of
|
||||
|
||||
## Caveats
|
||||
|
||||
- Service bot joining and Relay don't always behave consistently: see https://github.com/GLolol/PyLink/issues/265
|
||||
- Service bot joining and Relay don't always behave consistently: see https://github.com/jlu5/PyLink/issues/265
|
||||
|
@ -1,8 +1,8 @@
|
||||
# Exttargets Guide
|
||||
|
||||
In PyLink, **extended targets** or **exttargets** *replace* regular hostmasks with conditional matching based on specific situations. PyLink exttargets are supported by most plugins in the place of `nick!user@host` masks (provided they use the `Irc.matchHost()` API with a user object).
|
||||
In PyLink, **extended targets** or **exttargets** *replace* regular hostmasks with conditional matching based on specific situations. PyLink exttargets are supported by most plugins in the place of `nick!user@host` masks (provided they use the `IRCNetwork.match_host()` API with a user object).
|
||||
|
||||
Exttargets were introduced in PyLink 0.9 alongside [Automode](automode.md), with the goal of making user/ACL matching more versatile. As of PyLink 1.2-alpha2, the following exttargets are supported:
|
||||
Exttargets were introduced in PyLink 0.9 alongside [Automode](automode.md), with the goal of making user/ACL matching more versatile. As of PyLink 2.0-dev, the following exttargets are supported:
|
||||
|
||||
### The "$account" target (PyLink 0.9+)
|
||||
Used to match users by their services account.
|
||||
@ -59,3 +59,14 @@ Used to match users logged in to *PyLink* (i.e. via the `identify` command).
|
||||
|
||||
- `$pylinkacc` -> Returns True if the target is logged in to PyLink.
|
||||
- `$pylinkacc:accountname` -> Returns True if the target's PyLink login matches the one given (case insensitive).
|
||||
|
||||
### The "$realname" target (PyLink 2.0+)
|
||||
Used to match users with certain realnames.
|
||||
|
||||
- `$realname:*James*`: matches anyone with "James" in their real name (case insensitive).
|
||||
|
||||
### The "$service" target (PyLink 2.0+)
|
||||
Used to match service bots. This exttarget takes one optional argument: a glob, which is compared case-insensitively to the target user's service name if present.
|
||||
|
||||
- `$service`: matches any PyLink service bot.
|
||||
- `$service:automode`: matches the Automode service bot.
|
||||
|
45
docs/faq.md
45
docs/faq.md
@ -4,7 +4,7 @@
|
||||
|
||||
### I get errors like "ImportError: No module named 'yaml'" when I start PyLink
|
||||
|
||||
You are missing dependencies - re-read https://github.com/GLolol/PyLink/blob/master/README.md#installation
|
||||
You are missing dependencies - re-read https://github.com/jlu5/PyLink/blob/master/README.md#installation
|
||||
|
||||
### I get errors like "yaml.scanner.ScannerError: while scanning for the next token, found character '\t' that cannot start any token"
|
||||
|
||||
@ -66,7 +66,7 @@ If these steps haven't helped you so far, maybe you've found a bug...?
|
||||
|
||||
### My networks keep disconnecting with SSL errors!
|
||||
|
||||
See https://github.com/GLolol/PyLink/issues/463 - the problem appears to be caused somewhere in Python's SSL stack and/or OpenSSL, and not directly by our code.
|
||||
See https://github.com/jlu5/PyLink/issues/463 - the problem appears to be caused somewhere in Python's SSL stack and/or OpenSSL, and not directly by our code.
|
||||
|
||||
Unfortunately, the only workarounds so far are to either disable SSL/TLS, or wrap a plain IRC connection in an external service (stunnel, OpenVPN, etc.)
|
||||
|
||||
@ -80,35 +80,60 @@ PyLink does not support inbound connections - much like regular services such as
|
||||
|
||||
**No!** Only the PyLink administrator needs to host a PyLink instance with the `relay` plugin loaded, as each instance can connect to multiple networks. Everyone else only needs to add a link block on their IRCd.
|
||||
|
||||
InterJanus-style links between PyLink daemons are not supported yet; see https://github.com/GLolol/PyLink/issues/99 for any progress regarding that.
|
||||
InterJanus-style links between PyLink daemons are not supported yet; see https://github.com/jlu5/PyLink/issues/99 for any progress regarding that.
|
||||
|
||||
### What are PyLink's advantages over Janus?
|
||||
|
||||
PyLink provides, in no particular order:
|
||||
- More complete support for modern IRCds (UnrealIRCd 4.x, InspIRCd 2.0, charybdis 4, Nefarious IRCu, etc.).
|
||||
- A flexible, maintainable codebase extensible beyond Relay.
|
||||
- Cross platform functionality (*nix, Windows, and probably others too).
|
||||
- Proper protocol negotiation leading to fewer SQUIT/DoS possibilities:
|
||||
- Better support for channel modes such as +fjMOR, etc.
|
||||
- Proper support for nick length limits with relayed users.
|
||||
|
||||
### My IRCd SQUITs the relay server with errors like "Bad nickname introduced"!
|
||||
### My IRCd SQUITs the Relay server with errors like "Bad nickname introduced"!
|
||||
|
||||
First, check whether the SQUIT message includes the nick that triggered the netsplit. If this nick includes any characters not allowed in regular IRC, such as the slash ("/"), or is otherwise an invalid nick (e.g. beginning with a hyphen or number), this likely indicates a bug in PyLink Relay. These problems should be reported on the issue tracker.
|
||||
|
||||
However, if the nick mentioned is legal on IRC, this issue is likely caused by a max nick length misconfiguration: i.e. the relay server is introducing nicks too long for the target network. This can be fixed by setting the `maxnicklen` option in the affected network's PyLink `server:` block to the same value as that network's `005` `NICKLEN` (that is, the `NICKLEN=<num>` value in `/raw version`).
|
||||
However, if the nick mentioned is legal on IRC, this issue is likely caused by a max nick length misconfiguration: i.e. the Relay server is introducing nicks too long for the target network. This can be fixed by setting the `maxnicklen` option in the affected network's PyLink `server:` block to the same value as that network's `005` `NICKLEN` (that is, the `NICKLEN=<num>` value in `/raw version`).
|
||||
|
||||
### Clientbot doesn't relay both ways!
|
||||
|
||||
Load the `relay_clientbot` plugin. https://github.com/GLolol/PyLink/blob/1.3-beta1/example-conf.yml#L465-L468
|
||||
Load the `relay_clientbot` plugin. https://github.com/jlu5/PyLink/blob/1.3-beta1/example-conf.yml#L465-L468
|
||||
|
||||
### How do I turn off colors in Clientbot?
|
||||
See https://github.com/GLolol/PyLink/blob/master/docs/advanced-relay-config.md#custom-clientbot-styles, especially the section "Disabling Colors/Control Codes".
|
||||
See https://github.com/jlu5/PyLink/blob/master/docs/advanced-relay-config.md#custom-clientbot-styles, especially the section "Disabling Colors/Control Codes".
|
||||
|
||||
### Relay is occasionally dropping users from channels!
|
||||
|
||||
This usually indicates a serious bug in either Relay or PyLink's protocol modules, and should be reported as an issue. When asking for help, please state which IRCds your PyLink instance is linking to: specifically, which IRCd the missing users are *from* and which IRCd the users are missing *on*. Also, be prepared to send debug logs as you reproduce the issue!
|
||||
- Another tip in debugging this is to run `showchan` on the affected channels. If PyLink shows users in `showchan` that aren't in the actual user list, this is most likely a protocol module issue. If `showchan`'s output is correct, it is instead probably a relay issue where users aren't spawning correctly.
|
||||
- Another tip in debugging this is to run `showchan` on the affected channels. If PyLink shows users in `showchan` that aren't in the actual user list, this is most likely a protocol module issue. If `showchan`'s output is correct, it is instead probably a Relay issue where users aren't spawning correctly.
|
||||
|
||||
### Does Relay support mode +R, +M, etc.? How does Relay handle modes supported on one IRCd but not on another?
|
||||
Essentially, PyLink maps IRCd modes together by name, so modes that use different characters on different IRCds can be recognized as the same "mode". Tables of supported channel modes, user modes, and extbans (in 2.0+) can be found at https://github.com/jlu5/PyLink/tree/devel/docs/modelists. Note that third party/contrib modules implementing modes are generally *not* tested / supported.
|
||||
|
||||
Relay in particular uses whitelists to determine which modes are safe to relay: for 2.0.0, this is https://github.com/jlu5/PyLink/blob/71a24b8/plugins/relay.py#L903-L969. **Most *channel* modes recognized by PyLink are whitelisted and usable with Relay**, with the following exceptions:
|
||||
|
||||
- "registered" channel / user modes (InspIRCd, UnrealIRCd **+r**) - this is to prevent conflicts with local networks's services.
|
||||
- "permanent" channel modes (commonly **+P**) - it's not necessary for remote networks' channels to also be permanent locally.
|
||||
- Flood protection modes are only relayed between networks running the same IRCd (UnrealIRCd <-> UnrealIRCd or InspIRCd <-> InspIRCd).
|
||||
- Modes and extbans that specify a forwarding channel - mangling channel names between networks is far too complicated and desync prone.
|
||||
- InspIRCd's m_ojoin **+Y** and m_operprefix **+y** are ignored by Relay.
|
||||
- auditorium (InspIRCd **+u**), delayjoin (UnrealIRCd, P10, InspIRCd **+D**), and any other modes affecting join visibilites are not supported.
|
||||
|
||||
Support for user modes is not as complete:
|
||||
- Filter type modes such as callerid (**+g**), regonly (**+R**), noctcp (UnrealIRCd **+T**) are *not yet* supported by Relay.
|
||||
- Service protection modes (UnrealIRCD **+S**, InspIRCd **+k**, etc.) are not forwarded by Relay to prevent abuse.
|
||||
|
||||
### How does Relay handle kills?
|
||||
See https://github.com/jlu5/PyLink/blob/devel/docs/relay-quickstart.md#kill-handling
|
||||
|
||||
### How does Relay handle KLINE/GLINE/ZLINE?
|
||||
|
||||
It doesn't. https://github.com/jlu5/PyLink/issues/521#issuecomment-352316396 explains my reasons for skipping over this:
|
||||
|
||||
* The weakest link, whether this be a malicious/compromised/mistaken oper or a misconfigured services instance, can easily wreak havoc by banning something they shouldn't.
|
||||
* KLINE relaying goes against the concept of partial network links and creates serious animosity when opers disagree on policy. If KLINEs are shared, opers are essentially shared as well, and this is not the goal of Relay.
|
||||
|
||||
## Services issues
|
||||
|
||||
@ -116,4 +141,4 @@ This usually indicates a serious bug in either Relay or PyLink's protocol module
|
||||
|
||||
This indicates either a bug in PyLink's protocol module or (less commonly) a bug in your IRCd. Hint: ENDBURST is likely not being sent or received properly, which causes service bot spawning to never trigger.
|
||||
|
||||
Make sure you're using an [officially supported IRCd](https://github.com/GLolol/PyLink#supported-ircds) before requesting help, as custom IRCd code can potentially trigger S2S bugs and is not something we can support.
|
||||
Make sure you're using an [officially supported IRCd](https://github.com/jlu5/PyLink#supported-ircds) before requesting help, as custom IRCd code can potentially trigger S2S bugs and is not something we can support.
|
||||
|
5
docs/modelists/README.md
Normal file
5
docs/modelists/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
This folder contains tables of named modes defined by PyLink modules. The following are HTML versions of the raw .csv data:
|
||||
|
||||
- [Supported named channel modes](https://raw.githack.com/GLolol/PyLink/devel/docs/modelists/channel-modes.html)
|
||||
- [Supported named user modes](https://raw.githack.com/GLolol/PyLink/devel/docs/modelists/user-modes.html)
|
||||
- [Supported extbans](https://raw.githack.com/GLolol/PyLink/devel/docs/modelists/extbans.html)
|
68
docs/modelists/channel-modes.csv
Normal file
68
docs/modelists/channel-modes.csv
Normal file
@ -0,0 +1,68 @@
|
||||
Channel Mode / IRCd,rfc1459,hybrid,inspircd,ngircd,p10/ircu,p10/nefarious,p10/snircd,ts6/charybdis,ts6/chatircd,ts6/elemental,ts6/ratbox,unreal
|
||||
admin,,,"a (m_customprefix, m_chanprotect)",a,,,,,a (when enabled),a (when enabled),,a
|
||||
adminonly,,,,,,a,,A (ext/chm_adminonly),A (ext/chm_adminonly),A (ext/chm_adminonly.so),,
|
||||
allowinvite,,,A (m_allowinvite),,,,,g,g,g,,
|
||||
auditorium,,,u (m_auditorium),,,,,,,,,
|
||||
autoop,,,w (m_autoop),,,,,,,,,
|
||||
ban,b,b,b,b,b,b,b,b,b,b,b,b
|
||||
banexception,,e,e (m_banexception),e,,e,,e,e,e,e,e
|
||||
blockcaps,,,B (m_blockcaps),,,,,,,G (ext/chm_nocaps.so),,
|
||||
blockcolor,,c,c (m_blockcolor),,c,c,c,,,,,c
|
||||
blockhighlight,,,V (extras/m_blockhighlight),,,,,,,,,
|
||||
delayjoin,,,D (m_delayjoin),,D,D,D,,,,,D
|
||||
exemptchanops,,,X (m_exemptchanops),,,,,,,,,
|
||||
filter,,,g (m_filter),,,,,,,,,
|
||||
flood,,,f (m_messageflood),,,,,,,,,
|
||||
flood_unreal,,,,,,,,,,,,f
|
||||
freetarget,,,,,,,,F,F,F,,
|
||||
had_delayjoin,,,,,d,d,d,,,,,
|
||||
halfop,,h,"h (m_customprefix, m_halfop)",h,,,,,h (when enabled),h (when enabled),,h
|
||||
hiddenbans,,,,,,,,,,u,,
|
||||
hidequits,,,,,,Q,u,,,,,
|
||||
history,,,H (m_chanhistory),,,,,,,,,
|
||||
invex,,I,I (m_inviteexception),I,,,,I,I,I,I,I
|
||||
inviteonly,i,i,i,i,i,i,i,i,i,i,i,i
|
||||
issecure,,,,,,,,,,,,Z
|
||||
joinflood,,,j (m_joinflood),,,,,j,j,j,,
|
||||
key,k,k,k,k,k,k,k,k,k,k,k,k
|
||||
kicknorejoin,,,,,,,,,,J,,
|
||||
kicknorejoin_insp,,,J (m_kicknorejoin),,,,,,,,,
|
||||
largebanlist,,,,,,,,L,L,L,,
|
||||
limit,l,l,l,l,l,l,l,l,l,l,l,l
|
||||
moderated,m,m,m,m,m,m,m,m,m,m,m,m
|
||||
netadminonly,,,,,,,,,N (ext/chm_netadminonly),,,
|
||||
nickflood,,,F (m_nickflood),,,,,,,,,
|
||||
noamsg,,,,,,T,T,,,,,
|
||||
noctcp,,C,C (m_noctcp),,C,C,C,C,C,C,,C
|
||||
noextmsg,n,n,n,n,,n,,n,n,n,n,n
|
||||
noforwards,,,,,,,,Q,Q,Q,,
|
||||
noinvite,,,,V,,,,,,,,V
|
||||
nokick,,,Q (m_nokicks),Q,,,,,,E,,Q
|
||||
noknock,,p*,K (m_knock),,,,,p*,p*,p*,p*,K
|
||||
nonick,,,N (m_nonicks),N,,,,,,d,,N
|
||||
nonotice,,,T (m_nonotice),,,N,N,T (ext/chm_nonotice),T (ext/chm_nonotice),T,,T
|
||||
official-join,,,Y (m_ojoin),,,,,,,,,
|
||||
op,o,o,o,o,o,o,o,o,o,o,o,o
|
||||
operonly,,O,O (m_operchans),O,,O,,O (ext/chm_operonly),O (ext/chm_operonly),O (ext/chm_operonly.so),,O
|
||||
oplevel_apass,,,,,A,A,A,,,,,
|
||||
oplevel_upass,,,,,U,U,U,,,,,
|
||||
opmoderated,,,U (extras/m_opmoderated),,,,,z,z,z,,
|
||||
owner,,,"q (m_customprefix, m_chanprotect)",q,,,,,y (when enabled),y (when enabled),,q
|
||||
paranoia,,p*,,,,,,,,,,
|
||||
permanent,,,P (m_permchannels),P,,z,,P,P,P,,P
|
||||
private,p,p*,p,p,p,p,p,p*,p*,p*,p*,p
|
||||
quiet,,,(via extban m:),,,(via extban ~q:),,q,q,q,,(via extban ~q:)
|
||||
redirect,,,L (m_redirect),,,L,,f,f,f,,L
|
||||
registered,,r,r (m_services_account),r,R,R,R,,,,,r
|
||||
regmoderated,,M,M (m_services_account),M,,M,M,,,,,M
|
||||
regonly,,R,R (m_services_account),R,r,r,r,r,r,r,r,R
|
||||
repeat,,,,,,,,,,K (ext/chm_norepeat.c),,
|
||||
repeat_insp,,,E (m_repeat),,,,,,,,,
|
||||
secret,s,s,s,s,s,s,s,s,s,s,s,s
|
||||
sslonly,,S,z (m_sslmodes),z,,,,S (ext/chm_sslonly),S (ext/chm_sslonly),S (ext/chm_sslonly.c),S,z
|
||||
stripcolor,,,S (m_stripcolor),,,S,,c,c,c,,S
|
||||
topiclock,t,t,t,t,t,t,t,t,t,t,t,t
|
||||
voice,v,v,v,v,v,v,v,v,v,v,v,v
|
||||
,,,,,,,,,,,,
|
||||
----,,,,,,,,,,,,
|
||||
"* Mode +p corresponds to both “noknock” and “private” on TS6 IRCds, as well as “paranoia” on hybrid.",,,,,,,,,,,,
|
|
285
docs/modelists/channel-modes.html
Normal file
285
docs/modelists/channel-modes.html
Normal file
@ -0,0 +1,285 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
|
||||
<head>
|
||||
<title>Supported Channel Modes for PyLink</title>
|
||||
<style>
|
||||
|
||||
html {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
/* (╮°-°)╮┳━┳ */
|
||||
table, th, td {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
td, th {
|
||||
text-align: center;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
td:first-child, th[scope="row"] {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Table cells */
|
||||
.tablecell-yes {
|
||||
background-color: #A7F2A5
|
||||
}
|
||||
|
||||
.tablecell-no {
|
||||
background-color: #F08496
|
||||
}
|
||||
|
||||
.tablecell-na {
|
||||
background-color: #F0F0F0
|
||||
}
|
||||
|
||||
.tablecell-planned, .tablecell-yes2 {
|
||||
background-color: #92E8DF
|
||||
}
|
||||
|
||||
.tablecell-partial {
|
||||
background-color: #EDE8A4
|
||||
}
|
||||
|
||||
.tablecell-special {
|
||||
background-color: #DCB1FC
|
||||
}
|
||||
|
||||
.tablecell-caveats {
|
||||
background-color: #F0C884
|
||||
}
|
||||
|
||||
.tablecell-caveats2 {
|
||||
background-color: #ED9A80
|
||||
}
|
||||
|
||||
.tablecell-no-padding {
|
||||
padding: initial;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<table><tr>
|
||||
<th scope="col">Channel Mode / IRCd</th>
|
||||
<th scope="col">rfc1459</th>
|
||||
<th scope="col">hybrid</th>
|
||||
<th scope="col">inspircd</th>
|
||||
<th scope="col">ngircd</th>
|
||||
<th scope="col">p10/ircu</th>
|
||||
<th scope="col">p10/nefarious</th>
|
||||
<th scope="col">p10/snircd</th>
|
||||
<th scope="col">ts6/charybdis</th>
|
||||
<th scope="col">ts6/chatircd</th>
|
||||
<th scope="col">ts6/elemental</th>
|
||||
<th scope="col">ts6/ratbox</th>
|
||||
<th scope="col">unreal</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">admin</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+a<br><span class="note">(m_customprefix, m_chanprotect)</span></td><td class="tablecell-yes">+a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+a<br><span class="note">(when enabled)</span></td><td class="tablecell-special">+a<br><span class="note">(when enabled)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">adminonly</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+A<br><span class="note">(ext/chm_adminonly)</span></td><td class="tablecell-special">+A<br><span class="note">(ext/chm_adminonly)</span></td><td class="tablecell-special">+A<br><span class="note">(ext/chm_adminonly.so)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">allowinvite</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+A<br><span class="note">(m_allowinvite)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">auditorium</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+u<br><span class="note">(m_auditorium)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">autoop</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+w<br><span class="note">(m_autoop)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban</th>
|
||||
<td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td></tr>
|
||||
<tr>
|
||||
<th scope="row">banexception</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+e</td><td class="tablecell-special">+e<br><span class="note">(m_banexception)</span></td><td class="tablecell-yes">+e</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+e</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+e</td><td class="tablecell-yes">+e</td><td class="tablecell-yes">+e</td><td class="tablecell-yes">+e</td><td class="tablecell-yes">+e</td></tr>
|
||||
<tr>
|
||||
<th scope="row">blockcaps</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+B<br><span class="note">(m_blockcaps)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+G<br><span class="note">(ext/chm_nocaps.so)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">blockcolor</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-special">+c<br><span class="note">(m_blockcolor)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-yes">+c</td><td class="tablecell-yes">+c</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td></tr>
|
||||
<tr>
|
||||
<th scope="row">blockhighlight</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+V<br><span class="note">(extras/m_blockhighlight)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">delayjoin</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+D<br><span class="note">(m_delayjoin)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+D</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+D</td></tr>
|
||||
<tr>
|
||||
<th scope="row">exemptchanops</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+X<br><span class="note">(m_exemptchanops)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">filter</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+g<br><span class="note">(m_filter)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">flood</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+f<br><span class="note">(m_messageflood)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">flood_unreal</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+f</td></tr>
|
||||
<tr>
|
||||
<th scope="row">freetarget</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+F</td><td class="tablecell-yes">+F</td><td class="tablecell-yes">+F</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">had_delayjoin</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+d</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">halfop</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+h</td><td class="tablecell-special">+h<br><span class="note">(m_customprefix, m_halfop)</span></td><td class="tablecell-yes">+h</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+h<br><span class="note">(when enabled)</span></td><td class="tablecell-special">+h<br><span class="note">(when enabled)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+h</td></tr>
|
||||
<tr>
|
||||
<th scope="row">hiddenbans</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+u</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">hidequits</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+u</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">history</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+H<br><span class="note">(m_chanhistory)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">invex</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td><td class="tablecell-special">+I<br><span class="note">(m_inviteexception)</span></td><td class="tablecell-yes">+I</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td></tr>
|
||||
<tr>
|
||||
<th scope="row">inviteonly</th>
|
||||
<td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td></tr>
|
||||
<tr>
|
||||
<th scope="row">issecure</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+Z</td></tr>
|
||||
<tr>
|
||||
<th scope="row">joinflood</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+j<br><span class="note">(m_joinflood)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+j</td><td class="tablecell-yes">+j</td><td class="tablecell-yes">+j</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">key</th>
|
||||
<td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td></tr>
|
||||
<tr>
|
||||
<th scope="row">kicknorejoin</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+J</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">kicknorejoin_insp</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+J<br><span class="note">(m_kicknorejoin)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">largebanlist</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+L</td><td class="tablecell-yes">+L</td><td class="tablecell-yes">+L</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">limit</th>
|
||||
<td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td></tr>
|
||||
<tr>
|
||||
<th scope="row">moderated</th>
|
||||
<td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td></tr>
|
||||
<tr>
|
||||
<th scope="row">netadminonly</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+N<br><span class="note">(ext/chm_netadminonly)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">nickflood</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+F<br><span class="note">(m_nickflood)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noamsg</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+T</td><td class="tablecell-yes">+T</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noctcp</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-special">+C<br><span class="note">(m_noctcp)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-yes">+C</td><td class="tablecell-yes">+C</td><td class="tablecell-yes">+C</td><td class="tablecell-yes">+C</td><td class="tablecell-yes">+C</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noextmsg</th>
|
||||
<td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+n</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noforwards</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+Q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noinvite</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+V</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+V</td></tr>
|
||||
<tr>
|
||||
<th scope="row">nokick</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+Q<br><span class="note">(m_nokicks)</span></td><td class="tablecell-yes">+Q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+E</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+Q</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noknock</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+p*</td><td class="tablecell-special">+K<br><span class="note">(m_knock)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes">+K</td></tr>
|
||||
<tr>
|
||||
<th scope="row">nonick</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+N<br><span class="note">(m_nonicks)</span></td><td class="tablecell-yes">+N</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+d</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+N</td></tr>
|
||||
<tr>
|
||||
<th scope="row">nonotice</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+T<br><span class="note">(m_nonotice)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+N</td><td class="tablecell-yes">+N</td><td class="tablecell-special">+T<br><span class="note">(ext/chm_nonotice)</span></td><td class="tablecell-special">+T<br><span class="note">(ext/chm_nonotice)</span></td><td class="tablecell-yes">+T</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+T</td></tr>
|
||||
<tr>
|
||||
<th scope="row">official-join</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+Y<br><span class="note">(m_ojoin)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">op</th>
|
||||
<td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td></tr>
|
||||
<tr>
|
||||
<th scope="row">operonly</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+O</td><td class="tablecell-special">+O<br><span class="note">(m_operchans)</span></td><td class="tablecell-yes">+O</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+O</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+O<br><span class="note">(ext/chm_operonly)</span></td><td class="tablecell-special">+O<br><span class="note">(ext/chm_operonly)</span></td><td class="tablecell-special">+O<br><span class="note">(ext/chm_operonly.so)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+O</td></tr>
|
||||
<tr>
|
||||
<th scope="row">oplevel_apass</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+A</td><td class="tablecell-yes">+A</td><td class="tablecell-yes">+A</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">oplevel_upass</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+U</td><td class="tablecell-yes">+U</td><td class="tablecell-yes">+U</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">opmoderated</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+U<br><span class="note">(extras/m_opmoderated)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+z</td><td class="tablecell-yes">+z</td><td class="tablecell-yes">+z</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">owner</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+q<br><span class="note">(m_customprefix, m_chanprotect)</span></td><td class="tablecell-yes">+q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+y<br><span class="note">(when enabled)</span></td><td class="tablecell-special">+y<br><span class="note">(when enabled)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+q</td></tr>
|
||||
<tr>
|
||||
<th scope="row">paranoia</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+p*</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">permanent</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+P<br><span class="note">(m_permchannels)</span></td><td class="tablecell-yes">+P</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+z</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+P</td><td class="tablecell-yes">+P</td><td class="tablecell-yes">+P</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+P</td></tr>
|
||||
<tr>
|
||||
<th scope="row">private</th>
|
||||
<td class="tablecell-yes">+p</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes">+p</td></tr>
|
||||
<tr>
|
||||
<th scope="row">quiet</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-partial">(via extban m:)</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-partial">(via extban ~q:)</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+q</td><td class="tablecell-yes">+q</td><td class="tablecell-yes">+q</td><td class="tablecell-na note">n/a</td><td class="tablecell-partial">(via extban ~q:)</td></tr>
|
||||
<tr>
|
||||
<th scope="row">redirect</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+L<br><span class="note">(m_redirect)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+L</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+f</td><td class="tablecell-yes">+f</td><td class="tablecell-yes">+f</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+L</td></tr>
|
||||
<tr>
|
||||
<th scope="row">registered</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td><td class="tablecell-special">+r<br><span class="note">(m_services_account)</span></td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td></tr>
|
||||
<tr>
|
||||
<th scope="row">regmoderated</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+M</td><td class="tablecell-special">+M<br><span class="note">(m_services_account)</span></td><td class="tablecell-yes">+M</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+M</td><td class="tablecell-yes">+M</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+M</td></tr>
|
||||
<tr>
|
||||
<th scope="row">regonly</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+R</td><td class="tablecell-special">+R<br><span class="note">(m_services_account)</span></td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+R</td></tr>
|
||||
<tr>
|
||||
<th scope="row">repeat</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+K<br><span class="note">(ext/chm_norepeat.c)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">repeat_insp</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+E<br><span class="note">(m_repeat)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">secret</th>
|
||||
<td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sslonly</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+S</td><td class="tablecell-special">+z<br><span class="note">(m_sslmodes)</span></td><td class="tablecell-yes">+z</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+S<br><span class="note">(ext/chm_sslonly)</span></td><td class="tablecell-special">+S<br><span class="note">(ext/chm_sslonly)</span></td><td class="tablecell-special">+S<br><span class="note">(ext/chm_sslonly.c)</span></td><td class="tablecell-yes">+S</td><td class="tablecell-yes">+z</td></tr>
|
||||
<tr>
|
||||
<th scope="row">stripcolor</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+S<br><span class="note">(m_stripcolor)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+S</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-yes">+c</td><td class="tablecell-yes">+c</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+S</td></tr>
|
||||
<tr>
|
||||
<th scope="row">topiclock</th>
|
||||
<td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td></tr>
|
||||
<tr>
|
||||
<th scope="row">voice</th>
|
||||
<td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td></tr>
|
||||
<p>* Mode +p corresponds to both “noknock” and “private” on TS6 IRCds, as well as “paranoia” on hybrid.</p>
|
||||
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
35
docs/modelists/extbans.csv
Normal file
35
docs/modelists/extbans.csv
Normal file
@ -0,0 +1,35 @@
|
||||
Extban / IRCd,inspircd,p10/nefarious,ts6/charybdis,unreal
|
||||
ban_account,R:,~a:,$a:,~a:
|
||||
ban_account_legacy,,,,~R:
|
||||
ban_all_opers,,,$o,
|
||||
ban_all_registered,,,$a,
|
||||
ban_all_ssl,,,$z,
|
||||
ban_banshare,,~j:,$j:,
|
||||
ban_blockcaps,B:,,,
|
||||
ban_blockcolor,c:,,,
|
||||
ban_certfp,z:,,,~S:
|
||||
ban_extgecos,,,$x:,
|
||||
ban_inchannel,j:,~c:,$c:,~c:
|
||||
ban_invites,A:,,,
|
||||
ban_mark,,~m:,,
|
||||
ban_noctcp,C:,,,
|
||||
ban_nojoins,,,,~j:
|
||||
ban_nokicks,Q:,,,
|
||||
ban_nonick,N:,~n:,,~n:
|
||||
ban_nonotice,T:,,,
|
||||
ban_not_account,,,$~a:,
|
||||
ban_not_banshare,,,$~j:,
|
||||
ban_not_extgecos,,,$~x:,
|
||||
ban_not_inchannel,,,$~c:,
|
||||
ban_not_opers,,,$~o,
|
||||
ban_not_realname,,,$~r:,
|
||||
ban_not_server,,,$~s:,
|
||||
ban_not_ssl,,,$~z,
|
||||
ban_opertype,O:,,,~O:
|
||||
ban_partmsgs,p:,,,
|
||||
ban_realname,r:,~r:,$r:,~r:
|
||||
ban_server,s:,,$s:,
|
||||
ban_stripcolor,S:,,,
|
||||
ban_unregistered_mark,,~M:,,
|
||||
ban_unregistered_matching,U:,,,
|
||||
quiet,m:,~q:,(via cmode +q),~q:
|
|
187
docs/modelists/extbans.html
Normal file
187
docs/modelists/extbans.html
Normal file
@ -0,0 +1,187 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
|
||||
<head>
|
||||
<title>Supported Extbans for PyLink</title>
|
||||
<style>
|
||||
|
||||
html {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
/* (╮°-°)╮┳━┳ */
|
||||
table, th, td {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
td, th {
|
||||
text-align: center;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
td:first-child, th[scope="row"] {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Table cells */
|
||||
.tablecell-yes {
|
||||
background-color: #A7F2A5
|
||||
}
|
||||
|
||||
.tablecell-no {
|
||||
background-color: #F08496
|
||||
}
|
||||
|
||||
.tablecell-na {
|
||||
background-color: #F0F0F0
|
||||
}
|
||||
|
||||
.tablecell-planned, .tablecell-yes2 {
|
||||
background-color: #92E8DF
|
||||
}
|
||||
|
||||
.tablecell-partial {
|
||||
background-color: #EDE8A4
|
||||
}
|
||||
|
||||
.tablecell-special {
|
||||
background-color: #DCB1FC
|
||||
}
|
||||
|
||||
.tablecell-caveats {
|
||||
background-color: #F0C884
|
||||
}
|
||||
|
||||
.tablecell-caveats2 {
|
||||
background-color: #ED9A80
|
||||
}
|
||||
|
||||
.tablecell-no-padding {
|
||||
padding: initial;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<table><tr>
|
||||
<th scope="col">Extban / IRCd</th>
|
||||
<th scope="col">inspircd</th>
|
||||
<th scope="col">p10/nefarious</th>
|
||||
<th scope="col">ts6/charybdis</th>
|
||||
<th scope="col">unreal</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">ban_account</th>
|
||||
<td class="tablecell-yes">R:</td><td class="tablecell-yes">~a:</td><td class="tablecell-yes">$a:</td><td class="tablecell-yes">~a:</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_account_legacy</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">~R:</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_all_opers</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$o</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_all_registered</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_all_ssl</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$z</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_banshare</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">~j:</td><td class="tablecell-yes">$j:</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_blockcaps</th>
|
||||
<td class="tablecell-yes">B:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_blockcolor</th>
|
||||
<td class="tablecell-yes">c:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_certfp</th>
|
||||
<td class="tablecell-yes">z:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">~S:</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_extgecos</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$x:</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_inchannel</th>
|
||||
<td class="tablecell-yes">j:</td><td class="tablecell-yes">~c:</td><td class="tablecell-yes">$c:</td><td class="tablecell-yes">~c:</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_invites</th>
|
||||
<td class="tablecell-yes">A:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_mark</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">~m:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_noctcp</th>
|
||||
<td class="tablecell-yes">C:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_nojoins</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">~j:</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_nokicks</th>
|
||||
<td class="tablecell-yes">Q:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_nonick</th>
|
||||
<td class="tablecell-yes">N:</td><td class="tablecell-yes">~n:</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">~n:</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_nonotice</th>
|
||||
<td class="tablecell-yes">T:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_not_account</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~a:</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_not_banshare</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~j:</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_not_extgecos</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~x:</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_not_inchannel</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~c:</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_not_opers</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~o</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_not_realname</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~r:</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_not_server</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~s:</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_not_ssl</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~z</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_opertype</th>
|
||||
<td class="tablecell-yes">O:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">~O:</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_partmsgs</th>
|
||||
<td class="tablecell-yes">p:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_realname</th>
|
||||
<td class="tablecell-yes">r:</td><td class="tablecell-yes">~r:</td><td class="tablecell-yes">$r:</td><td class="tablecell-yes">~r:</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_server</th>
|
||||
<td class="tablecell-yes">s:</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$s:</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_stripcolor</th>
|
||||
<td class="tablecell-yes">S:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_unregistered_mark</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">~M:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_unregistered_matching</th>
|
||||
<td class="tablecell-yes">U:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">quiet</th>
|
||||
<td class="tablecell-yes">m:</td><td class="tablecell-yes">~q:</td><td class="tablecell-partial">(via cmode +q)</td><td class="tablecell-yes">~q:</td></tr>
|
||||
|
||||
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
153
docs/modelists/modelists-genhtml.py
Executable file
153
docs/modelists/modelists-genhtml.py
Executable file
@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generates HTML versions of the mode list .csv definitions.
|
||||
"""
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import csv
|
||||
|
||||
os.chdir(os.path.dirname(__file__))
|
||||
|
||||
FILES = {
|
||||
'user-modes.csv': 'Supported User Modes for PyLink',
|
||||
'channel-modes.csv': 'Supported Channel Modes for PyLink',
|
||||
'extbans.csv': 'Supported Extbans for PyLink',
|
||||
}
|
||||
|
||||
def _write(outf, text):
|
||||
print(text, end='')
|
||||
outf.write(text)
|
||||
|
||||
def _format(articlename, text):
|
||||
# More formatting
|
||||
if text:
|
||||
if text.startswith('('):
|
||||
text = '<td class="tablecell-partial">%s</td>' % text
|
||||
else:
|
||||
if 'modes' in articlename:
|
||||
text = '+' + text
|
||||
try:
|
||||
text, note = text.split(' ', 1)
|
||||
except ValueError:
|
||||
if text.endswith('*'):
|
||||
text = '<td class="tablecell-yes2">%s</td>' % text
|
||||
else:
|
||||
text = '<td class="tablecell-yes">%s</td>' % text
|
||||
else:
|
||||
text = '%s<br><span class="note">%s</span>' % (text, note)
|
||||
text = '<td class="tablecell-special">%s</td>' % text
|
||||
else:
|
||||
text = '<td class="tablecell-na note">n/a</td>'
|
||||
return text
|
||||
|
||||
for fname, title in FILES.items():
|
||||
outfname = os.path.splitext(fname)[0] + '.html'
|
||||
print('Generating HTML for %s to %s:' % (fname, outfname))
|
||||
with open(fname) as csvfile:
|
||||
csvdata = csv.reader(csvfile)
|
||||
|
||||
with open(outfname, 'w') as outf:
|
||||
# CSS in HTML in Python, how lovely...
|
||||
_write(outf, """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
|
||||
<head>
|
||||
<title>%s</title>
|
||||
<style>
|
||||
|
||||
html {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
/* (╮°-°)╮┳━┳ */
|
||||
table, th, td {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
td, th {
|
||||
text-align: center;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
td:first-child, th[scope="row"] {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Table cells */
|
||||
.tablecell-yes {
|
||||
background-color: #A7F2A5
|
||||
}
|
||||
|
||||
.tablecell-no {
|
||||
background-color: #F08496
|
||||
}
|
||||
|
||||
.tablecell-na {
|
||||
background-color: #F0F0F0
|
||||
}
|
||||
|
||||
.tablecell-planned, .tablecell-yes2 {
|
||||
background-color: #92E8DF
|
||||
}
|
||||
|
||||
.tablecell-partial {
|
||||
background-color: #EDE8A4
|
||||
}
|
||||
|
||||
.tablecell-special {
|
||||
background-color: #DCB1FC
|
||||
}
|
||||
|
||||
.tablecell-caveats {
|
||||
background-color: #F0C884
|
||||
}
|
||||
|
||||
.tablecell-caveats2 {
|
||||
background-color: #ED9A80
|
||||
}
|
||||
|
||||
.tablecell-no-padding {
|
||||
padding: initial;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<table>""" % title)
|
||||
notes = False
|
||||
for idx, row in enumerate(csvdata):
|
||||
if not any(row): # Empty row
|
||||
continue
|
||||
elif row[0] == '----':
|
||||
notes = True
|
||||
continue
|
||||
|
||||
if notes:
|
||||
_write(outf, "<p>%s</p>" % row[0])
|
||||
continue
|
||||
|
||||
_write(outf, "<tr>\n")
|
||||
for colidx, coltext in enumerate(row):
|
||||
if idx == 0:
|
||||
text = '<th scope="col">%s</th>\n' % coltext
|
||||
elif colidx == 0:
|
||||
text = '<th scope="row">%s</th>\n' % coltext
|
||||
else:
|
||||
text = _format(fname, coltext)
|
||||
_write(outf, text)
|
||||
|
||||
_write(outf, "</tr>\n")
|
||||
_write(outf, """
|
||||
|
||||
</table>
|
||||
</body>
|
||||
</html>""")
|
54
docs/modelists/user-modes.csv
Normal file
54
docs/modelists/user-modes.csv
Normal file
@ -0,0 +1,54 @@
|
||||
User Mode / IRCd,RFC 1459,hybrid,inspircd,ngircd,p10/ircu,p10/nefarious,p10/snircd,ts6/charybdis,ts6/chatircd,ts6/elemental,ts6/ratbox,unreal
|
||||
admin,,a,,,,a,,a,a,a,a,
|
||||
away,,,,a,,,,,,,,
|
||||
bot,,,B,B,,B,,,B,B,,B
|
||||
callerid,,g,g,,,,,g,g,g,g,
|
||||
callerid_sslonly,,,,,,,,,t,,,
|
||||
cloak,,x,x,x,x,x,x,x,x,x,,x
|
||||
cloak_fakehost,,,,,,f,,,,,,
|
||||
cloak_hashedhost,,,,,,C,,,,,,
|
||||
cloak_hashedip,,,,,,c,,,,,,
|
||||
cloak_sethost,,,,,,h,h,,,,,
|
||||
deaf,,D,d,b,d,d,d,D,D,D,D,d
|
||||
deaf_commonchan,,G,c,C,,q,,,,,,
|
||||
debug,,d,,,,,,,,,,
|
||||
filter,,,,,,,,,,,,G
|
||||
floodexempt,,,,f,,,,,,,,
|
||||
helpop,,,h,,,,,,,,,
|
||||
hidechans,,p,I,I,,n,n,,,I,,p
|
||||
hideidle,,q,,,,I,I,,,,,I
|
||||
hideoper,,H,H,,,H,,,,,,H
|
||||
invisible,i,i,i,i,i,i,i,i,i,i,i,i
|
||||
locops,,l,,,O,O,O,l,l,l,l,
|
||||
netadmin,,,,,,,,,N,,,
|
||||
noctcp,,,,,,,,,,C,,T
|
||||
noforward,,,,,,L,,Q,Q,Q,,
|
||||
noinvite,,,,,,,,,,V,,
|
||||
oper,o,o,o,o,o,o,o,o,o,o,o,o
|
||||
operwall,,,,,,,,z,z,z,z,
|
||||
override,,,,,,X,X,p,p,p,,
|
||||
privdeaf,,,,,,D,,,,,,
|
||||
protected,,,,,,,,,,,,q
|
||||
regdeaf,,R,R,,,R,R,R,R,R,,R
|
||||
registered,,r,r,R,r,r,r,,,,,r
|
||||
restricted,,,,r,,,,,,,,
|
||||
servprotect,,,k,q,k,k,k,S,S,S,S,S
|
||||
showwhois,,,W,,,W,,,,,,W
|
||||
sno_badclientconnections,,u,,,,,,,,,u,
|
||||
sno_botfloods,,b,,,,,,,,,b,
|
||||
sno_clientconnections,,c,,c,,,,,,,c,
|
||||
sno_debug,,,,,g,g,g,,,,d,
|
||||
sno_extclientconnections,,,,,,,,,,,C,
|
||||
sno_fullauthblock,,f,,,,,,,,,f,
|
||||
sno_nickchange,,n,,,,,,,,,,
|
||||
sno_rejectedclients,,j,,,,,,,,,r,
|
||||
sno_remoteclientconnections,,F,,,,,,,,,,
|
||||
sno_serverconnects,,e,,,,,,,,,x,
|
||||
sno_skill,,k,,,,,,,,,k,
|
||||
sno_stats,,y,,,,,,,,,y,
|
||||
snomask,s,s,s,s,,s,,s,s,s,s,s
|
||||
ssl,,S,,,,z,,,,,,z
|
||||
stripcolor,,,S,,,,,,,,,
|
||||
vhost,,,,,,,,,,,,t
|
||||
wallops,w,w,w,w,,w,,w,w,w,w,w
|
||||
webirc,,W,,,,,,,,,,
|
|
252
docs/modelists/user-modes.html
Normal file
252
docs/modelists/user-modes.html
Normal file
@ -0,0 +1,252 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
|
||||
<head>
|
||||
<title>Supported User Modes for PyLink</title>
|
||||
<style>
|
||||
|
||||
html {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
/* (╮°-°)╮┳━┳ */
|
||||
table, th, td {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
td, th {
|
||||
text-align: center;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
td:first-child, th[scope="row"] {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Table cells */
|
||||
.tablecell-yes {
|
||||
background-color: #A7F2A5
|
||||
}
|
||||
|
||||
.tablecell-no {
|
||||
background-color: #F08496
|
||||
}
|
||||
|
||||
.tablecell-na {
|
||||
background-color: #F0F0F0
|
||||
}
|
||||
|
||||
.tablecell-planned, .tablecell-yes2 {
|
||||
background-color: #92E8DF
|
||||
}
|
||||
|
||||
.tablecell-partial {
|
||||
background-color: #EDE8A4
|
||||
}
|
||||
|
||||
.tablecell-special {
|
||||
background-color: #DCB1FC
|
||||
}
|
||||
|
||||
.tablecell-caveats {
|
||||
background-color: #F0C884
|
||||
}
|
||||
|
||||
.tablecell-caveats2 {
|
||||
background-color: #ED9A80
|
||||
}
|
||||
|
||||
.tablecell-no-padding {
|
||||
padding: initial;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<table><tr>
|
||||
<th scope="col">User Mode / IRCd</th>
|
||||
<th scope="col">RFC 1459</th>
|
||||
<th scope="col">hybrid</th>
|
||||
<th scope="col">inspircd</th>
|
||||
<th scope="col">ngircd</th>
|
||||
<th scope="col">p10/ircu</th>
|
||||
<th scope="col">p10/nefarious</th>
|
||||
<th scope="col">p10/snircd</th>
|
||||
<th scope="col">ts6/charybdis</th>
|
||||
<th scope="col">ts6/chatircd</th>
|
||||
<th scope="col">ts6/elemental</th>
|
||||
<th scope="col">ts6/ratbox</th>
|
||||
<th scope="col">unreal</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">admin</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+a</td><td class="tablecell-yes">+a</td><td class="tablecell-yes">+a</td><td class="tablecell-yes">+a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">away</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">bot</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+B</td><td class="tablecell-yes">+B</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+B</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+B</td><td class="tablecell-yes">+B</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+B</td></tr>
|
||||
<tr>
|
||||
<th scope="row">callerid</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">callerid_sslonly</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+t</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">cloak</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+x</td></tr>
|
||||
<tr>
|
||||
<th scope="row">cloak_fakehost</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+f</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">cloak_hashedhost</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">cloak_hashedip</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">cloak_sethost</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+h</td><td class="tablecell-yes">+h</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">deaf</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+d</td></tr>
|
||||
<tr>
|
||||
<th scope="row">deaf_commonchan</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+G</td><td class="tablecell-yes">+c</td><td class="tablecell-yes">+C</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">debug</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+d</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">filter</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+G</td></tr>
|
||||
<tr>
|
||||
<th scope="row">floodexempt</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+f</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">helpop</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+h</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">hidechans</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+p</td></tr>
|
||||
<tr>
|
||||
<th scope="row">hideidle</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td></tr>
|
||||
<tr>
|
||||
<th scope="row">hideoper</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+H</td><td class="tablecell-yes">+H</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+H</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+H</td></tr>
|
||||
<tr>
|
||||
<th scope="row">invisible</th>
|
||||
<td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td></tr>
|
||||
<tr>
|
||||
<th scope="row">locops</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+l</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+O</td><td class="tablecell-yes">+O</td><td class="tablecell-yes">+O</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">netadmin</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+N</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noctcp</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+T</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noforward</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+L</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+Q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noinvite</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+V</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">oper</th>
|
||||
<td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td></tr>
|
||||
<tr>
|
||||
<th scope="row">operwall</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+z</td><td class="tablecell-yes">+z</td><td class="tablecell-yes">+z</td><td class="tablecell-yes">+z</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">override</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+X</td><td class="tablecell-yes">+X</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">privdeaf</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+D</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">protected</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+q</td></tr>
|
||||
<tr>
|
||||
<th scope="row">regdeaf</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+R</td></tr>
|
||||
<tr>
|
||||
<th scope="row">registered</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td></tr>
|
||||
<tr>
|
||||
<th scope="row">restricted</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">servprotect</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+q</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+S</td><td class="tablecell-yes">+S</td><td class="tablecell-yes">+S</td><td class="tablecell-yes">+S</td><td class="tablecell-yes">+S</td></tr>
|
||||
<tr>
|
||||
<th scope="row">showwhois</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+W</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+W</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+W</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sno_badclientconnections</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+u</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+u</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sno_botfloods</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+b</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+b</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sno_clientconnections</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sno_debug</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+d</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sno_extclientconnections</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sno_fullauthblock</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+f</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+f</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sno_nickchange</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+n</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sno_rejectedclients</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+j</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sno_remoteclientconnections</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+F</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sno_serverconnects</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+e</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+x</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sno_skill</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+k</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+k</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sno_stats</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+y</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+y</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">snomask</th>
|
||||
<td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+s</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ssl</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+S</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+z</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+z</td></tr>
|
||||
<tr>
|
||||
<th scope="row">stripcolor</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+S</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">vhost</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+t</td></tr>
|
||||
<tr>
|
||||
<th scope="row">wallops</th>
|
||||
<td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+w</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td></tr>
|
||||
<tr>
|
||||
<th scope="row">webirc</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+W</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
|
||||
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
@ -3,12 +3,12 @@
|
||||
Below is a list of all the permissions defined by PyLink and its official plugins. For instructions on how to fine-tune permissions, see [example-permissions.yml](../example-permissions.yml).
|
||||
|
||||
## PyLink Core
|
||||
- `core.clearqueue` - Allows access to the `clearqueue` command.
|
||||
- `core.shutdown` - Allows access to the `shutdown` command.
|
||||
- `core.load` - Allows access to the `load` command.
|
||||
- `core.unload` - Allows access to the `unload` command.
|
||||
- `core.reload` - Allows access to the `reload`, `load`, and `unload` commands. (This implies access to `load` and `unload` because `reload` is really just those two commands combined.)
|
||||
- `core.rehash` - Allows access to the `rehash` command.
|
||||
- `core.clearqueue` - Grants access to the `clearqueue` command.
|
||||
- `core.load` - Grants access to the `load` command.
|
||||
- `core.rehash` - Grants access to the `rehash` command.
|
||||
- `core.reload` - Grants access to the `reload`, `load`, and `unload` commands. (This implies access to `load` and `unload` because `reload` is really just those two commands combined.)
|
||||
- `core.shutdown` - Grants access to the `shutdown` command.
|
||||
- `core.unload` - Grants access to the `unload` command.
|
||||
|
||||
## Automode
|
||||
|
||||
@ -36,64 +36,84 @@ Remote versions of the `manage`, `list`, `sync`, and `clear` commands also exist
|
||||
|
||||
## Bots
|
||||
|
||||
- `bots.spawnclient` - Allows access to the `spawnclient` command.
|
||||
- `bots.quit` - Allows access to the `quit` command.
|
||||
- `bots.joinclient` - Allows access to the `joinclient` command.
|
||||
- `bots.nick` - Allows access to the `nick` command.
|
||||
- `bots.part` - Allows access to the `part` command.
|
||||
- `bots.msg` - Allows access to the `msg` command.
|
||||
- `bots.join` - Grants access to the `join` command. `bots.joinclient` is a deprecated alias for this, retained for compatibility with PyLink < 2.0-rc1.
|
||||
- `bots.msg` - Grants access to the `msg` command.
|
||||
- `bots.nick` - Grants access to the `nick` command.
|
||||
- `bots.part` - Grants access to the `part` command.
|
||||
- `bots.quit` - Grants access to the `quit` command.
|
||||
- `bots.spawnclient` - Grants access to the `spawnclient` command.
|
||||
|
||||
## Changehost
|
||||
|
||||
- `changehost.applyhosts` - Allows access to the `applyhosts` command.
|
||||
- `changehost.applyhosts` - Grants access to the `applyhosts` command.
|
||||
|
||||
## Commands
|
||||
- `commands.status` - Allows access to the `status` command. **With the default permissions set, this is granted to all users.**
|
||||
- `commands.showuser` - Allows access to the `showuser` command. **With the default permissions set, this is granted to all users.**
|
||||
- `commands.showchan` - Allows access to the `showchan` command. **With the default permissions set, this is granted to all users.**
|
||||
- `commands.echo` - Allows access to the `echo` command.
|
||||
- `commands.echo` - Grants access to the `echo` command.
|
||||
- `commands.loglevel` - Grants access to the `loglevel` command.
|
||||
- `commands.logout.force` - Allows forcing logouts on other users via the `logout` command.
|
||||
- `commands.loglevel` - Allows access to the `loglevel` command.
|
||||
- `commands.showchan` - Grants access to the `showchan` command. **With the default permissions set, this is granted to all users.**
|
||||
- `commands.showuser` - Grants access to the `showuser` command. **With the default permissions set, this is granted to all users.**
|
||||
- `commands.status` - Grants access to the `status` command. **With the default permissions set, this is granted to all users.**
|
||||
|
||||
## Exec
|
||||
- `exec.exec` - Allows access to the `exec` command.
|
||||
- `exec.eval` - Allows access to the `eval` command.
|
||||
- `exec.raw` - Allows access to the `raw` command.
|
||||
- `exec.inject` - Allows access to the `inject` command.
|
||||
- `exec.exec` - Grants access to the `exec` command.
|
||||
- `exec.eval` - Grants access to the `eval` command.
|
||||
- `exec.inject` - Grants access to the `inject` command.
|
||||
- `exec.threadinfo` - Grants access to the `threadinfo` command.
|
||||
|
||||
## Global
|
||||
- `global.global` - Allows access to the `global` command.
|
||||
- `global.global` - Grants access to the `global` command.
|
||||
|
||||
## Networks
|
||||
- `networks.disconnect` - Allows access to the `disconnect` command.
|
||||
- `networks.autoconnect` - Allows access to the `autoconnect` command.
|
||||
- `networks.remote` - Allows access to the `remote` command.
|
||||
- `networks.reloadproto` - Allows access to the `reloadproto` command.
|
||||
- `networks.autoconnect` - Grants access to the `autoconnect` command.
|
||||
- `networks.disconnect` - Grants access to the `disconnect` command.
|
||||
- `networks.reloadproto` - Grants access to the `reloadproto` command.
|
||||
- `networks.remote` - Grants access to the `remote` command.
|
||||
|
||||
## Opercmds
|
||||
- `opercmds.checkban` - Allows access to the `checkban` command.
|
||||
- `opercmds.jupe` - Allows access to the `jupe` command.
|
||||
- `opercmds.kick` - Allows access to the `kick` command.
|
||||
- `opercmds.kill` - Allows access to the `kill` command.
|
||||
- `opercmds.mode` - Allows access to the `mode` command.
|
||||
- `opercmds.topic` - Allows access to the `topic` command.
|
||||
- `opercmds.checkban` - Grants access to the `checkban` command.
|
||||
- `opercmds.checkban.re` - Grants access to the `checkbanre` command **if** the caller also has `opercmds.checkban`.
|
||||
- `opercmds.chghost` - Grants access to the `chghost` command.
|
||||
- `opercmds.chgident` - Grants access to the `chgident` command.
|
||||
- `opercmds.chgname` - Grants access to the `chgname` command.
|
||||
- `opercmds.jupe` - Grants access to the `jupe` command.
|
||||
- `opercmds.kick` - Grants access to the `kick` command.
|
||||
- `opercmds.kill` - Grants access to the `kill` command.
|
||||
- `opercmds.massban` - Grants access to the `massban` command.
|
||||
- `opercmds.massban.re` - Grants access to the `massbanre` command **if** the caller also has `opercmds.massban`.
|
||||
- `opercmds.mode` - Grants access to the `mode` command.
|
||||
- `opercmds.topic` - Grants access to the `topic` command.
|
||||
|
||||
## Raw
|
||||
- `raw.raw` - Grants access to the `raw` command. `exec.raw` is equivalent to this and retained for compatibility with PyLink 1.x.
|
||||
- `raw.raw.unsupported_network` - Allows use of the `raw` command on servers other than Clientbot.
|
||||
|
||||
## Relay
|
||||
- `relay.claim` - Allows access to the `claim` command.
|
||||
- `relay.create` - Allows access to the `create` command. **With the default permissions set, this is granted to all opers.**
|
||||
- `relay.delink` - Allows access to the `delink` command. **With the default permissions set, this is granted to all opers.**
|
||||
- `relay.destroy` - Allows access to the `destroy` command. **With the default permissions set, this is granted to all opers.**
|
||||
- `relay.destroy.remote` - Allows access to the `remote` command.
|
||||
- `relay.linkacl` - Allows access to the `linkacl` command. **With the default permissions set, this is granted to all opers.**
|
||||
- `relay.linkacl.view` - Allows access to the `view` command. **With the default permissions set, this is granted to all opers.**
|
||||
- `relay.link` - Allows access to the `link` command. **With the default permissions set, this is granted to all opers.**
|
||||
- `relay.link.force` - Allows access to the `--force` option in the `link` command (skip TS and target network is connected checks).
|
||||
- `relay.linked` - Allows access to the `link` command. **With the default permissions set, this is granted to all users.**
|
||||
- `relay.purge` - Allows access to the `purge` command.
|
||||
- `relay.savedb` - Allows access to the `savedb` command.
|
||||
These permissions are granted to all opers when the `relay::allow_free_oper_links` option is set (this is the default):
|
||||
|
||||
- `relay.chandesc.remove` - Allows removing channel descriptions via the `chandesc` command.
|
||||
- `relay.chandesc.set` - Allows setting / updating channel descriptions via the `chandesc` command.
|
||||
- `relay.claim` - Grants access to the `claim` command.
|
||||
- `relay.create` - Grants access to the `create` command.
|
||||
- `relay.delink` - Grants access to the `delink` command.
|
||||
- `relay.destroy` - Grants access to the `destroy` command.
|
||||
- `relay.link` - Grants access to the `link` command.
|
||||
|
||||
These permissions are always granted to all opers:
|
||||
- `relay.linkacl` - Allows managing LINKACL entries via the `linkacl` command.
|
||||
- `relay.linkacl.view` - Allows viewing LINKACL entries via the `linkacl` command.
|
||||
|
||||
These permissions are not granted to anyone by default:
|
||||
- `relay.destroy.remote` - Allows destroying remote channels.
|
||||
- `relay.link.force_ts` - Grants access to the `link` command's `--force-ts` option (skip TS and target network is connected checks).
|
||||
- `relay.linked` - Grants access to the `link` command. **With the default permissions set, this is granted to all users.**
|
||||
- `relay.purge` - Grants access to the `purge` command.
|
||||
- `relay.savedb` - Grants access to the `savedb` command.
|
||||
|
||||
## Servermaps
|
||||
- `servermaps.map` - Allows access to the `map` and `localmap` commands.
|
||||
- `servermaps.localmap` - Grants access to the `localmap` command.
|
||||
- `servermaps.map` - Grants access to the `map` command.
|
||||
|
||||
## Stats
|
||||
- `stats.uptime` - Allows access to the `stats` command.
|
||||
- `stats.c`, `stats.o`, `stats.u` - Grants access to remote `/stats` calls with the corresponding letter.
|
||||
- `stats.uptime` - Grants access to the `stats` command.
|
||||
|
@ -1,82 +1 @@
|
||||
# Opering with PyLink Relay
|
||||
|
||||
*This guide was written for the OVERdrive-IRC network, but may be applicable elsewhere.*
|
||||
|
||||
PyLink Relay behaves much like Janus, an extended service used to relay channels together. This guide goes over some of the basic oper commands in Relay, along with the best ways to handle channel emergencies.
|
||||
|
||||
## How nick suffixing work
|
||||
|
||||
When joining a relay channel, every user from another network will have a network tag attached to their name. The purpose of this is to prevent nick collisions from the same nick being used on multiple nets, and ensure that different networks' registered nicks remain separate.
|
||||
|
||||
How is this relevant? Firstly, it means that you **cannot ban users** using banmasks such as `*/net1!*@*`! The nick suffix is something PyLink adds artificially; on `net1`'s IRCd, which is checking the bans locally, the nick suffix simply doesn't exist.
|
||||
|
||||
However, this *does* mean that you can effectively give access to remote users via services, by specifying masks such as `*/net1@someident@someperson.opers.somenet.org`. Just don't make masks too wide, or you risk getting channel takeovers.
|
||||
|
||||
## Relay commands
|
||||
The concept of relay channels in PyLink is greatly inspired by the original Janus implementation, though with a few differences in command syntax.
|
||||
|
||||
To create a channel:
|
||||
- `/msg PyLink create #channelname`
|
||||
|
||||
To link to a channel already created on a different network:
|
||||
- `/msg PyLink link othernet #channelname`
|
||||
|
||||
You can also link remote channels to take a different name on your network. (This is the third argument to the LINK command)
|
||||
- `/msg PyLink link othernet #lobby #othernet-lobby`
|
||||
|
||||
Also, to list the available channels:
|
||||
- `/msg PyLink linked`
|
||||
|
||||
To remove a relay channel that you've created:
|
||||
- `/msg PyLink destroy #channelname`
|
||||
|
||||
To delink a channel linked to another network:
|
||||
- `/msg PyLink delink #channelname`
|
||||
|
||||
### Claiming channels
|
||||
|
||||
PyLink offers channel claiming similarly to Janus, except that it is on by default when you create a channel on any network. Unless the claimed network list of a channel is EMPTY, oper override (MODE, KICK, TOPIC) will only be allowed from networks on that list.
|
||||
|
||||
To set a claim (note: for these commands, you must be on the network which created the channel in question!):
|
||||
- `/msg PyLink claim #channel yournet,net2,net3` (the last parameter is a comma-separated list of networks, case-sensitive)
|
||||
|
||||
To list claims on a channel:
|
||||
- `/msg PyLink claim #channel`
|
||||
|
||||
To remove claims from a channel
|
||||
- `/msg PyLink claim #channel -`
|
||||
|
||||
### Access control for links (LINKACL)
|
||||
LINKACL allows you to block certain networks from linking to your relay channels, based on a blacklist. By default, this blacklist is empty.
|
||||
|
||||
To list blocked networks for a channel:
|
||||
- `/msg PyLink linkacl #channel list`
|
||||
|
||||
To add a network to the blacklist:
|
||||
- `/msg PyLink linkacl #channel allow badnet`
|
||||
|
||||
To remove a network from the blacklist:
|
||||
- `/msg PyLink linkacl #channel deny goodnet`
|
||||
|
||||
Whitelists with LINKACL are not supported at this time.
|
||||
|
||||
## Dealing with channel emergencies
|
||||
|
||||
PyLink is not designed with the ability to forward KILLs, G:Lines, or any network bans. **The best thing to do in the case of emergencies is to delink the problem networks / channels!** Kills are actively blocked by the PyLink daemon (user is just respawned), while X:Lines are simply ignored, as there isn't any code to handle them yet.
|
||||
|
||||
To delink another network from a channel your network owns:
|
||||
|
||||
- `/msg PyLink delink #yourchannel badnetwork`
|
||||
|
||||
To delink your network from a bad network's channel:
|
||||
|
||||
- `/msg PyLink delink #badchannel`
|
||||
|
||||
Basically, only one of the two above commands will work for one specific channel. Almost always, the network that owns a channel should be the one who has it registered via their services. You can see a list of channels by typing `/msg PyLink linked`.
|
||||
|
||||
## When a network starts causing disconnect spam
|
||||
|
||||
Juping an individual `net.relay` server will likely cause PyLink Relay to break or disconnect completely. When a network starts acting up and disconnecting frequently (and causing netsplit/quit floods), you should disable autoconnect for this network:
|
||||
|
||||
- `/msg PyLink autoconnect badnetwork -1` (setting autoconnect to 0 or below will cause it to be disabled)
|
||||
|
||||
Moved to [relay-quickstart.md](relay-quickstart.md).
|
||||
|
121
docs/relay-quickstart.md
Normal file
121
docs/relay-quickstart.md
Normal file
@ -0,0 +1,121 @@
|
||||
# PyLink Relay Quick Start Guide
|
||||
|
||||
PyLink Relay (aka "Relay") provides transparent server-side relaying between channels, letting networks share channels on demand without going through all the fuss of a hard link. Each network retains its own opers and services, with default behaviour being so that oper features (kill, overrides, etc.) are isolated to only work on channels they own. If you're familiar with Janus, you can think of PyLink Relay as being a rewrite of it from scratch (though PyLink can do much more via its other plugins!).
|
||||
|
||||
This guide goes over some of the basic commands in Relay, as well as all the must-know notes.
|
||||
|
||||
## How nick suffixing work
|
||||
|
||||
The default Relay configuration in will automatically tag users from other networks with a suffix such as `/net`. The purpose of this is to prevent confusing nick collisions if the same nick is used on multiple linked networks, and ensure that remote networks' nicks effectively use their own namespace.
|
||||
|
||||
How is this relevant to an operator? Firstly, it means that you **cannot ban users** using banmasks such as `*/net1!*@*`! The nick suffix is something PyLink adds artificially; on `net1`'s IRCd, which check the bans locally, the nick suffix doesn't exist and will therefore *not* match anyone.
|
||||
|
||||
## Services compatibility
|
||||
While PyLink is generally able to run independently of individual networks's services, there are some gotchas. This list briefly details services features that have been known to cause problems with Relay. **Using any of these features in conjunction with Relay is *not* supported.**
|
||||
|
||||
- Anope, Atheme: **Clones prevention should be DISABLED** (or at a minimum, set to use G/KLINE instead of KILL)
|
||||
- Rationale: it is common for a person to want to connect to multiple networks in a Relay instance, because they are still independent entities. You can still use IRCd-side clones prevention, which sanely blocks connections instead of killing / banning everyone involved.
|
||||
- Anope: **SQLINE nicks should NOT be used**
|
||||
- Rationale: Anope falls back to killing target clients matching a SQLINE, which will obviously cause conflicts with other services.
|
||||
- *Any*: **Do NOT register a relayed channel on multiple networks**
|
||||
- Rationale: It is very easy for this to start kick or mode wars. (Bad akick mask? Secure ops enabled?)
|
||||
- *Any*: **Do NOT jupe virtual Relay servers** (e.g. `net.relay`)
|
||||
- Rationale: This will just make PyLink split off - you should instead [delink any problem networks / channels](#dealing-with-disputes-and-emergencies).
|
||||
- Multiple PyLink Relay instances:
|
||||
- **Do NOT connect a network twice to any PyLink instance**.
|
||||
- **Do NOT connect a network to 2+ separate PyLink instances if there is another network already acting as a hub for them**.
|
||||
- Not following these rules means that it's very easy for the Relay instances to go in a loop, whcih will hammer your CPU and seriously spam your channels.
|
||||
|
||||
## Relay commands
|
||||
The concept of relay channels in PyLink is greatly inspired by Janus, though with a few differences in command syntax.
|
||||
|
||||
Then, to list all available channels:
|
||||
- `/msg PyLink linked`
|
||||
|
||||
To create a channel:
|
||||
- `/msg PyLink create #channelname`
|
||||
|
||||
To link to a channel already created on a different network:
|
||||
- `/msg PyLink link othernet #channelname`
|
||||
|
||||
You can also link remote channels to take a different name on your network. (This is the third argument to the LINK command)
|
||||
- `/msg PyLink link othernet #lobby #othernet-lobby`
|
||||
|
||||
To remove a relay channel that you've created:
|
||||
- `/msg PyLink destroy #channelname`
|
||||
|
||||
To delink a channel linked to another network:
|
||||
- `/msg PyLink delink #localchannelname`
|
||||
|
||||
Then, to list all available channels:
|
||||
- `/msg PyLink linked`
|
||||
|
||||
### Claiming channels
|
||||
|
||||
Channel claims are a feature which prevents oper override (MODE, KICK, TOPIC, KILL, OJOIN, ...) from working on channels not owned by or whitelisting a network. By default, CLAIM is enabled for all new channels, though this can be configured in PyLink 2.0+ via the [`relay::enable_default_claim` option](https://github.com/jlu5/PyLink/blob/2.0-beta1/example-conf.yml#L771-L774). Unless the claimed network list of a channel is EMPTY, oper override will only be allowed from networks on that list.
|
||||
|
||||
To set a claim (note: for these commands, you must be on the network which created the channel in question!):
|
||||
- `/msg PyLink claim #channel yournet,net2,net3` (the last parameter is a case-sensitive comma-separated list of networks)
|
||||
|
||||
To list claim networks on a channel:
|
||||
- `/msg PyLink claim #channel`
|
||||
|
||||
To clear the claim list for a channel:
|
||||
- `/msg PyLink claim #channel -`
|
||||
|
||||
### Access control for links (LINKACL)
|
||||
LINKACL allows you to blacklist or whitelist networks from linking to your channel. The default configuration enables blacklist mode by default, though this can be configured via the [`relay::linkacl_use_whitelist` option](https://github.com/jlu5/PyLink/blob/2.0-beta1/example-conf.yml#L766-L769).
|
||||
|
||||
To change between blacklist and whitelist mode:
|
||||
- `/msg PyLink linkacl whitelist #channel true/false`
|
||||
- Note that when you switch between LINKACL modes, the LINKACL entries from the previous mode are stored and stashed away. This means that you will get an empty LINKACL list in the new LINKACL mode if you haven't used it already, and that you can reload the previous LINKACL mode's entries by switching back to it at any point.
|
||||
|
||||
To view the LINKACL networks for a channel:
|
||||
- `/msg PyLink linkacl #channel list`
|
||||
|
||||
To add a network to the whitelist **OR** remove a network from the blacklist:
|
||||
- `/msg PyLink linkacl #channel allow badnet`
|
||||
|
||||
To remove a network from the whitelist **OR** add a network to the blacklist:
|
||||
- `/msg PyLink linkacl #channel deny goodnet`
|
||||
|
||||
### Adding channel descriptions
|
||||
Starting with PyLink 2.0, you can annotate your channels with a description to use in LINKED:
|
||||
|
||||
To view the description for a channel:
|
||||
- `/msg PyLink chandesc #channel`
|
||||
|
||||
To change the description for a channel:
|
||||
- `/msg PyLink chandesc #channel your text goes here`
|
||||
|
||||
To remove the description for a channel:
|
||||
- `/msg PyLink chandesc #channel -`
|
||||
|
||||
## Dealing with disputes and emergencies
|
||||
|
||||
The best thing to do in the event of a dispute is to delink the problem networks / channels. KILLs and network bans (K/G/ZLINE) will most often *not* behave the way you expect it to.
|
||||
|
||||
### Kill handling
|
||||
Special kill handling was introduced in PyLink 2.0, while in previous versions they were always rejected:
|
||||
|
||||
1) If the sender was a server and not a client, reject the kill.
|
||||
2) If the target and source networks are both in a(ny) [kill share pool](https://github.com/jlu5/PyLink/blob/2.0-beta1/example-conf.yml#L725-L735), relay the kill as-is.
|
||||
3) Otherwise, check every channels the kill target is in:
|
||||
- If the killer has claim access in a channel, forward the KILL as a kick to that channel.
|
||||
- Otherwise, bounce the kill silently.
|
||||
|
||||
### Network bans (K/G/ZLINE)
|
||||
|
||||
Network bans are purposely not supported; see https://github.com/jlu5/PyLink/issues/521#issuecomment-352316396.
|
||||
|
||||
### Delinking channels
|
||||
|
||||
To delink another network from a channel your network owns:
|
||||
|
||||
- `/msg PyLink delink #yourchannel badnetwork`
|
||||
|
||||
To delink your network from a bad network's channel:
|
||||
|
||||
- `/msg PyLink delink #badchannel`
|
||||
|
||||
Basically, only one of the two above commands will work for one specific channel. Almost always, the network that owns a channel should be the one who has it registered via their services. You can see a list of channels by typing `/msg PyLink linked`.
|
@ -1,16 +1,14 @@
|
||||
# PyLink Developer Documentation
|
||||
|
||||
Please note that as PyLink is still in its development phase, its APIs are subject to change.
|
||||
Any documentation here is provided for reference only.
|
||||
This documentation is provided for reference only, and may not always be up to date as APIs change.
|
||||
Patches are welcome if something looks wrong or *is* wrong. In such cases, consulting the source code is probably your best bet.
|
||||
|
||||
The docs are also really incomplete (contributors welcome!)
|
||||
The docs are also really incomplete (contributions are appreciated!)
|
||||
|
||||
## Introduction
|
||||
|
||||
PyLink is an a modular, plugin-based IRC services framework. It uses swappable protocol modules and a hooks system for calling plugins, allowing them to function regardless of the IRCd used.
|
||||
|
||||
<img src="core-structure.png" width="50%" height="50%"> <img src="protocol-modules.png" width="50%" height="50%">
|
||||
|
||||
## Contents
|
||||
|
||||
- [Writing plugins for PyLink](writing-plugins.md)
|
||||
@ -22,13 +20,9 @@ PyLink is an a modular, plugin-based IRC services framework. It uses swappable p
|
||||
----
|
||||
|
||||
- [PyLink protocol module specification](pmodule-spec.md)
|
||||
- [Supported named channel modes](channel-modes.csv)
|
||||
- [Supported named user modes](user-modes.csv)
|
||||
|
||||
----
|
||||
|
||||
- [Release Process for PyLink](release-process.md)
|
||||
|
||||
### Future topics (not yet available)
|
||||
- [Writing tests for PyLink modules](writing-tests.md)
|
||||
|
||||
![Graph of protocol module inheritance tree](protocol-modules.svg)
|
||||
|
@ -1,60 +0,0 @@
|
||||
Channel Mode / IRCd,RFC1459,InspIRCd,charybdis,Elemental-IRCd,UnrealIRCd,IRCd-Hybrid,Nefarious IRCu,ircd-ratbox,snircd,IRCu,ngIRCd
|
||||
admin,,a (m_customprefix/m_chanprotect),,a (when enabled),a,,,,,,
|
||||
adminonly,,,A (extensions/chm_adminonly),A (extensions/chm_adminonly.c),,,a,,,,
|
||||
allowinvite,,A (m_allowinvite),g,g,,,,,,,
|
||||
autoop,,w (m_autoop),,,,,,,,,
|
||||
ban,b,b,b,b,b,b,b,b,b,b,b
|
||||
banexception,,e (m_banexception),e,e,e,e,e,e,,,e
|
||||
blockcaps,,B (m_blockcaps),,G (extensions/chm_nocaps.c),,,,,,,
|
||||
blockcolor,,c (m_blockcolor),,,c,c,c,,c,c,
|
||||
delayjoin,,,,,D,,D,,D,D,
|
||||
exemptchanops,,X (m_exemptchanops),,,,,,,,,
|
||||
filter,,g (m_filter),,,,,,,,,
|
||||
flood,,f (m_messageflood),,,,,,,,,
|
||||
flood_unreal,,,,,f,,,,,,
|
||||
freetarget,,,F,F,,,,,,,
|
||||
had_delayjoin,,,,,,,d,,d,d,
|
||||
halfop,,h (m_customprefix/m_halfop),,h (when enabled),h,h,,,,,
|
||||
hiddenbans,,,,u,,,,,,,
|
||||
hidequits,,,,,,,Q,,u,,
|
||||
history,,H (m_chanhistory),,,,,,,,,
|
||||
invex,,I (m_inviteexception),I,I,I,I,,I,,,I
|
||||
inviteonly,i,i,i,i,i,i,i,i,i,i,i
|
||||
issecure,,,,,Z,,,,,,
|
||||
joinflood,,j (m_joinflood),j,j,,,,,,,
|
||||
key,k,k,k,k,k,k,k,k,k,k,k
|
||||
kicknorejoin,,J (m_kicknorejoin),,J,,,,,,,
|
||||
largebanlist,,,L,L,,,,,,,
|
||||
limit,l,l,l,l,l,l,l,l,l,l,l
|
||||
moderated,m,m,m,m,m,m,m,m,m,m,m
|
||||
nickflood,,F (m_nickflood),,,,,,,,,
|
||||
noamsg,,,,,,,T,,T,,
|
||||
noctcp,,C (m_noctcp),C,C,C,C,C,,C,C,
|
||||
noextmsg,n,n,n,n,n,n,n,n,,,n
|
||||
noforwards,,,Q,Q,,,,,,,
|
||||
noinvite,,,,,V,,,,,,V
|
||||
nokick,,Q (m_nokicks),,E,Q,,,,,,Q
|
||||
noknock,,K (m_knock),p,,K,p,,,,,
|
||||
nonick,,N (m_nonicks),,d,N,,,,,,N
|
||||
nonotice,,T (m_nonotice),T (extensions/chm_nonotice),T,T,,N,,N,,
|
||||
official-join,,Y (m_ojoin),,,,,,,,,
|
||||
op,o,o,o,o,o,o,o,o,o,o,o
|
||||
operonly,,O (m_operchans),O (extensions/chm_operonly),O (extensions/chm_operonly.c),O,O,O,,,,O
|
||||
oplevel_apass,,,,,,,A,,A,A,
|
||||
oplevel_upass,,,,,,,U,,U,U,
|
||||
opmoderated,,U (extras/m_opmoderated),z,z,,,,,,,
|
||||
owner,,q (m_customprefix/m_chanprotect),,y (when enabled),q,,,,,,
|
||||
paranoia,,,,,,p,,,,,
|
||||
permanent,,P (m_permchannels),P,P,P,,z,,,,P
|
||||
private,p,p,,,p,,p,,p,p,p
|
||||
quiet,,,q,q,,,,,,,
|
||||
redirect,,L (m_redirect),f,f,L,,L,,,,
|
||||
registered,,r (m_services_account),,,r,r,R,,R,R,r
|
||||
regmoderated,,M (m_services_account),,,M,M,M,,M,,M
|
||||
regonly,,R (m_services_account),r,r,R,R,r,r,r,r,R
|
||||
repeat,,E (m_repeat),,K (extensions/chm_norepeat.c),,,,,,,
|
||||
secret,s,s,s,s,s,s,s,s,s,s,s
|
||||
sslonly,,z (m_sslmodes),S (extensions/chm_sslonly),S (extensions/chm_sslonly.c),z,S,,S,,,z
|
||||
stripcolor,,S (m_stripcolor),c,c,S,,S,,,,
|
||||
topiclock,t,t,t,t,t,t,t,t,t,t,t
|
||||
voice,v,v,v,v,v,v,v,v,v,v,v
|
|
@ -1,27 +0,0 @@
|
||||
/* Graph for the PyLink Application Structure:
|
||||
* Update using: dot -Tpng core-structure.dot > core-structure.png
|
||||
*/
|
||||
|
||||
digraph G {
|
||||
ratio = 0.8; /* make the graph wider than tall */
|
||||
subgraph cluster_core {
|
||||
label="PyLink Application Structure";
|
||||
style="filled";
|
||||
node [style="filled",color="white"];
|
||||
color="lightblue";
|
||||
|
||||
"IRC objects" -> "Protocol modules" [label="Data relayed"]
|
||||
"Protocol modules" -> "PyLink hooks" -> Plugins;
|
||||
"IRC objects" -> "PyLink hooks";
|
||||
"Main program" -> "IRC objects" [color=indigo] [label="One per network\nspawned"] [fontcolor=indigo];
|
||||
"Main program" -> "IRC objects" [color=indigo];
|
||||
"Main program" -> "IRC objects" [color=indigo];
|
||||
"Protocol modules" -> "IRC objects" [label="States updated"] [color=darkgreen] [fontcolor=darkgreen];
|
||||
"Main program" -> Plugins [label="Plugin loaders"];
|
||||
}
|
||||
|
||||
"Protocol modules" -> "IRCds" -> "Protocol modules";
|
||||
Plugins -> "Protocol modules" [label="Communication via\nIRC command\nsenders"] [color=navyblue] [fontcolor=navyblue];
|
||||
Plugins -> "Main program" [label="Registers commands\n& hook handlers"] [color=brown] [fontcolor=brown];
|
||||
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 56 KiB |
@ -3,7 +3,7 @@ This version of the document targets the current stable branch of PyLink, and ma
|
||||
|
||||
# PyLink hooks reference
|
||||
|
||||
***Last updated for 1.2-dev (2017-02-24).***
|
||||
***Last updated for 2.0.0 (2018-07-11).***
|
||||
|
||||
In PyLink, protocol modules communicate with plugins through a system of hooks. This has the benefit of being IRCd-independent, allowing most plugins to function regardless of the IRCd being used.
|
||||
Each hook payload is formatted as a Python `list`, with three arguments: `numeric`, `command`, and `args`.
|
||||
@ -32,16 +32,7 @@ Some hooks, like MODE, are more complex and can include the entire state of a ch
|
||||
['001ZJZW01',
|
||||
'MODE',
|
||||
{'modes': [('+o', '38QAAAAAA')],
|
||||
'channeldata': IrcChannel({'modes': set(),
|
||||
'prefixmodes': {'admin': set(),
|
||||
'halfop': set(),
|
||||
'op': set(),
|
||||
'owner': set(),
|
||||
'voice': set()},
|
||||
'topic': '',
|
||||
'topicset': False,
|
||||
'ts': 1451169448,
|
||||
'users': {'38QAAAAAA', '001ZJZW01'}}),
|
||||
'channeldata': Channel(...),
|
||||
'target': '#chat',
|
||||
'ts': 1451174702}]
|
||||
```
|
||||
@ -71,51 +62,51 @@ The following hooks represent regular IRC commands sent between servers.
|
||||
- `modes` returns a list of parsed modes: `(mode character, mode argument)` tuples, where the mode argument is either `None` (for modes without arguments), or a string.
|
||||
- The sender of this hook payload is IRCd-dependent, and is determined by whether the command was originally a SJOIN or regular JOIN - SJOIN is only sent by servers, and JOIN is only sent by users.
|
||||
- For IRCds that support joining multiple channels in one command (`/join #channel1,#channel2`), consecutive JOIN hook payloads of this format will be sent (one per channel).
|
||||
- For SJOIN, the `channeldata` key may also be sent, with a copy of the `IrcChannel` object BEFORE any mode changes from this burst command were processed.
|
||||
- For SJOIN, the `channeldata` key may also be sent, with a copy of the `classes.Channel` object *before* any mode changes from this burst command were processed.
|
||||
|
||||
- **KICK**: `{'channel': '#channel', 'target': 'UID1', 'text': 'some reason'}`
|
||||
- `text` refers to the kick reason. The `target` and `channel` fields send the target's UID and the channel they were kicked from, and the sender of the hook payload is the kicker.
|
||||
|
||||
- **KILL**: `{'target': killed, 'text': args[1], 'userdata': data}`
|
||||
- **KILL**: `{'target': killed, 'text': 'Killed (james (absolutely not))', 'userdata': data}`
|
||||
- `text` refers to the kill reason. `target` is the target's UID.
|
||||
- The `userdata` key may include an `IrcUser` instance, depending on the IRCd. On IRCds where QUITs are explicitly sent (InspIRCd), `userdata` will be `None`. Other IRCds do not explicitly send QUIT messages for KILLed clients, so the daemon must assume that they've quit, and deliver their last state to plugins that require this info.
|
||||
- The `userdata` key may include an `classes.User` instance, depending on the IRCd. On IRCds where QUITs are explicitly sent (e.g InspIRCd), `userdata` will be `None`. Other IRCds do not explicitly send QUIT messages for killed clients, so the daemon must assume that they've quit, and deliver their last state to plugins that require this info.
|
||||
|
||||
- **MODE**: `{'target': '#channel', 'modes': [('+m', None), ('+i', None), ('+t', None), ('+l', '3'), ('-o', 'person')], 'channeldata': IrcChannel(...)}`
|
||||
- `target` is the target the mode is being set on: it may be either a channel (for channel modes) OR a UID (for user modes).
|
||||
- **MODE**: `{'target': '#channel', 'modes': [('+m', None), ('+i', None), ('+t', None), ('+l', '3'), ('-o', 'person')], 'channeldata': Channel(...)}`
|
||||
- `target` is the target the mode is being set on: it may be either a channel (for channel modes) *or* a UID (for user modes).
|
||||
- `modes` is a list of prefixed parsed modes: `(mode character, mode argument)` tuples, but with `+/-` prefixes to denote whether each mode is being set or unset.
|
||||
- For channels, the `channeldata` key is also sent, with a copy of the `IrcChannel` BEFORE this MODE hook was processed.
|
||||
- For channels, the `channeldata` key is also sent, with a copy of the `classes.Channel` object *before* this MODE hook was processed.
|
||||
- One use for this is to prevent oper-override hacks: checks for whether a sender is opped have to be done before the MODE is processed; otherwise, someone can simply op themselves and circumvent this detection.
|
||||
|
||||
- **NICK**: `{'newnick': 'Alakazam', 'oldnick': 'Abracadabra', 'ts': 1234567890}`
|
||||
|
||||
- **NOTICE**: `{'target': 'UID3', 'text': 'hi there!'}`
|
||||
- *Note:* `target` can not only be a channel or a UID, but also a channel with a prefix attached (e.g. `@#lounge`). These cases should not be overlooked!
|
||||
- STATUSMSG targets (e.g. `@#lounge`) are also allowed here.
|
||||
|
||||
- **PART**: `{'channels': ['#channel1', '#channel2'], 'text': 'some reason'}`
|
||||
- `text` can also be an empty string, as part messages are *optional* on IRC.
|
||||
- Unlike the JOIN hook, multiple channels can be specified in a list for PART. This means that a user PARTing one channel will cause a payload to be sent with `channels` as a one-length *list* with the channel name.
|
||||
- Unlike the JOIN hook, multiple channels can be specified in a list for PART. This means that a user parting one channel will cause a payload to be sent with `channels` as a one-length *list* with the channel name.
|
||||
|
||||
- **PRIVMSG**: `{'target': 'UID3', 'text': 'hi there!'}`
|
||||
- Ditto with NOTICE: `target` can be a channel or a UID, or a channel with a prefix attached (e.g. `@#lounge`).
|
||||
- Ditto with NOTICE: STATUSMSG targets (e.g. `@#lounge`) are also allowed here.
|
||||
|
||||
- **QUIT**: `{'text': 'Quit: Bye everyone!'}`
|
||||
- `text` corresponds to the quit reason.
|
||||
|
||||
- **SQUIT**: `{'target': '800', 'users': ['UID1', 'UID2', 'UID6'], 'name': 'some.server', 'uplink': '24X', 'nicks': {'#channel1: ['tester1', 'tester2'], '#channel3': ['somebot']}, 'serverdata': IrcServer(...)`
|
||||
- **SQUIT**: `{'target': '800', 'users': ['UID1', 'UID2', 'UID6'], 'name': 'some.server', 'uplink': '24X', 'nicks': {'#channel1: ['tester1', 'tester2'], '#channel3': ['somebot']}, 'serverdata': Server(...)`
|
||||
- `target` is the SID of the server being split, while `name` is the server's name.
|
||||
- `users` is a list of all UIDs affected by the netsplit. `nicks` maps channels to lists of nicks affected.
|
||||
- `serverdata` provides the `IrcServer` object of the server that was split.
|
||||
- `serverdata` provides the `classes.Server` object of the server that split off.
|
||||
- `channeldata` provides the channel index of the network before the netsplit was processed, allowing plugins to track who was affected by a netsplit in a channel specific way.
|
||||
|
||||
- **TOPIC**: `{'channel': channel, 'setter': numeric, 'text': 'Welcome to #Lounge!, 'oldtopic': 'Welcome to#Lounge!'}`
|
||||
- `oldtopic` denotes the original topic, and `text` indicates the new one being set.
|
||||
- `setter` is the raw sender field given to us by the IRCd; it may be a `nick!user@host`, a UID, a SID, a server name, or a nick. This is not processed any further.
|
||||
- `setter` is the raw sender field given to us by the IRCd; it may be a `nick!user@host`, a UID, a SID, a server name, or a nick. This is not processed at the protocol level.
|
||||
|
||||
- **UID**: `{'uid': 'UID1', 'ts': 1234567891, 'nick': 'supercoder', 'realhost': 'localhost', 'host': 'admin.testnet.local', 'ident': ident, 'ip': '127.0.0.1'}`
|
||||
- This command is used to introduce users; the sender of the message should be the server bursting or announcing the connection.
|
||||
- `ts` refers to the user's signon time.
|
||||
|
||||
### Extra commands (where supported by the IRCd)
|
||||
### Extra commands (where supported)
|
||||
|
||||
- **AWAY**: `{'text': text}`
|
||||
- `text` denotes the away reason. It is an empty string (`''`) when a user is unsetting their away status.
|
||||
@ -129,9 +120,9 @@ The following hooks represent regular IRC commands sent between servers.
|
||||
- **CHGNAME**: `{'target': 'UID2', 'newgecos': "I ain't telling you!"}`
|
||||
- SETNAME and CHGNAME commands, where available, both share this hook name.
|
||||
|
||||
- **INVITE**: `{'target': 'UID3', 'channel': '#myroom'}`
|
||||
- **INVITE**: `{'target': 'UID3', 'channel': '#hello'}`
|
||||
|
||||
- **KNOCK**: `{'text': 'let me in please!', 'channel': '#myroom'}`
|
||||
- **KNOCK**: `{'text': 'let me in please!', 'channel': '#hello'}`
|
||||
- This is not actually implemented by any protocol module as of writing.
|
||||
|
||||
- **SAVE**: `{'target': 'UID8', 'ts': 1234567892, 'oldnick': 'Abracadabra'}`
|
||||
@ -144,7 +135,7 @@ The following hooks represent regular IRC commands sent between servers.
|
||||
|
||||
- **VERSION**: `{}`
|
||||
- This is used for protocols that send VERSION requests between servers when a client requests it (e.g. `/raw version pylink.local`).
|
||||
- `coremods/handlers.py` automatically handles this by responding with a 351 numeric, with the data being the output of `irc.fullVersion()`.
|
||||
- `coremods/handlers.py` automatically handles this by responding with a 351 numeric, with the data being the output of `irc.version()`.
|
||||
|
||||
- **WHOIS**: `{'target': 'UID1'}`
|
||||
- On protocols supporting it (everything except InspIRCd), the WHOIS command is sent between servers for remote WHOIS requests.
|
||||
@ -166,12 +157,18 @@ Some hooks do not map directly to IRC commands, but to events that protocol modu
|
||||
|
||||
- **PYLINK_CUSTOM_WHOIS**: `{'target': UID1, 'server': SID1}`
|
||||
- This hook is called by `coremods/handlers.py` during its WHOIS handling process, to allow plugins to provide custom WHOIS information. The `target` field represents the target UID, while the `server` field represents the SID that should be replying to the WHOIS request. The source of the payload is the user using `/whois`.
|
||||
- Plugins wishing to implement this should use the standard WHOIS numerics, using `irc.proto.numeric()` to reply to the source from the given server.
|
||||
- This hook replaces the pre-0.8.x fashion of defining custom WHOIS handlers, which was non-standard and poorly documented.
|
||||
- Plugins wishing to implement this should use the standard WHOIS numerics, using `irc.numeric()` to reply to the source from the given server.
|
||||
- This hook replaces the pre-0.8.x fashion of defining custom WHOIS handlers, which was never standardized and poorly documented.
|
||||
|
||||
## Commands handled WITHOUT hooks
|
||||
At this time, commands that are handled by protocol modules without returning any hook data include PING, PONG, and various commands sent during the initial server linking phase.
|
||||
|
||||
## Changes
|
||||
* 2018-07-11 (2.0.0)
|
||||
- Version bump for 2.0 stable release; no meaningful content changes.
|
||||
* 2018-01-13 (2.0-alpha2)
|
||||
- Replace `IrcChannel`, `IrcUser`, and `IrcServer` with their new class names (`classes.Channel`, `classes.User`, and `classes.Server`)
|
||||
- Replace `irc.fullVersion()` with `irc.version()`
|
||||
- Various minor wording tweaks.
|
||||
* 2017-02-24 (1.2-dev)
|
||||
- The `was_successful` key was added to PYLINK_DISCONNECT.
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Permissions were introduced in PyLink 1.0 as a way for plugins to manage command access, replacing the old `irc.checkAuthenticated()`. The permissions system in PyLink is fairly simple, globally assigning a list of permissions to each hostmask/exttarget.
|
||||
|
||||
Permissions take the format `pluginname.commandname.optional_extra_portion(s)`, and support wildcards in matching. Permission nodes are case-insensitive and casemapping aware, but are conventionally defined as being all lowercase.
|
||||
Permissions conventionally take the format `pluginname.commandname.optional_extra_portion(s)`, and support globs† in matching. Permission nodes are case-insensitive and casemapping aware, but are defined as being all lowercase for consistency.
|
||||
|
||||
The permissions module is available as `pylinkirc.coremods.permissions`. Usually, plugins import it this format:
|
||||
|
||||
@ -10,17 +10,19 @@ The permissions module is available as `pylinkirc.coremods.permissions`. Usually
|
||||
from pylinkirc.coremods import permissions
|
||||
```
|
||||
|
||||
† The globbing used by the permissions module is just generic IRC-style globbing. For example, anyone with `*`, `perm.*`, `perm.?`, `*.1`, etc. in their permissions list will be allowed to use a command checking for a permission named `perm.1`.
|
||||
|
||||
## Checking for permissions
|
||||
|
||||
Individual functions check for permissions using the `permissions.checkPermissions(irc, source, ['perm.1', 'perm.2'])` function, where the last argument is an OR'ed list of permissions (i.e. users only need one out of all of them). This function returns `True` when a permission check passes, and raises `utils.NotAuthorizedError` when a check fails, automatically aborting the execution of the command function.
|
||||
Individual functions check for permissions using the `permissions.checkPermissions(irc, source, ['perm.1', 'perm.2'])` function, where the last argument is an OR'ed list of permissions matched against a list of permission string globs that a user may have. If the user has any of the permissions in the permission list, they will be allowed to call the command. This function returns `True` when a permission check passes, and raises `utils.NotAuthorizedError` when a check fails, automatically aborting the execution of the command function.
|
||||
|
||||
`utils.NotAuthorizedError` can be treated like any other exception, so it's possible to wrap it around `try:` / `except:` for more complex access checking ([example in the Automode plugin](https://github.com/GLolol/PyLink/blob/1.1.1/plugins/automode.py#L64-L68)).
|
||||
`utils.NotAuthorizedError` can be treated like any other exception, so it's possible to wrap it around `try:` / `except:` for more complex access checking ([example in the Automode plugin](https://github.com/jlu5/PyLink/blob/1.1.1/plugins/automode.py#L64-L68)).
|
||||
|
||||
## Assigning default permissions
|
||||
|
||||
Plugins are also allowed to assign default permissions to their commands, though this should be used sparingly to ensure maximum configurability (explicitly removing permissions isn't supported yet). Default permissions are specified as a `dict` mapping targets to permission lists.
|
||||
|
||||
Example of this in [Automode](https://github.com/GLolol/PyLink/blob/1.1-alpha1/plugins/automode.py#L38-L39):
|
||||
Example of this in [Automode](https://github.com/jlu5/PyLink/blob/1.1-alpha1/plugins/automode.py#L38-L39):
|
||||
|
||||
```python
|
||||
# The default set of Automode permissions.
|
||||
|
@ -3,53 +3,69 @@ This version of the document targets the current stable branch of PyLink, and ma
|
||||
|
||||
# PyLink Protocol Module Specification
|
||||
|
||||
***Last updated for 1.2-dev (2017-03-15).***
|
||||
***Last updated for 2.0.0 (2018-07-11).***
|
||||
|
||||
In PyLink, each protocol module is a file consisting of a protocol class (e.g. `InspIRCdProtocol`), and a global `Class` attribute set equal to it (e.g. `Class = InspIRCdProtocol`). These classes are usually based off boilerplate classes such as `classes.Protocol`, `protocols.ircs2s_common.IRCS2SProtocol`, or other protocol module classes that share functionality with it.
|
||||
Starting with PyLink 2.x, a *protocol module* is any module containing a class derived from `PyLinkNetworkCore` (e.g. `InspIRCdProtocol`), along with a global `Class` attribute set equal to it (e.g. `Class = InspIRCdProtocol`). These modules do everything from managing connections to providing plugins with an API to send and receive data. New protocol modules may be implemented based off any of the classes in the following inheritance tree, with each containing a different amount of abstraction.
|
||||
|
||||
![[Protocol module inheritence graph]](protocol-modules.png)
|
||||
![[Protocol module inheritence graph]](protocol-modules.svg)
|
||||
|
||||
IRC objects load protocol modules by creating an instance of this `Class` attribute, and then proceeding to call its commands.
|
||||
## Starting Steps
|
||||
|
||||
## Tasks
|
||||
**Before you proceed, we highly recommend protocol module coders to get in touch with us** (e.g. via IRC at `#PyLink @ irc.overdrivenetworks.com`). Letting us know what you are working on can help coordinate coding efforts and better prepare for potential API breaks.
|
||||
|
||||
Protocol modules have some very important jobs. If any of these aren't done correctly, you will be left with a broken, desynced services server:
|
||||
Note: The following notes in this section assume that you are working on some IRCd's server protocol, such that PyLink can spawn subservers and its own pseudoclients. If this is not the case, *virtual* clients and servers have to be spawned instead to emulate the correct state - the `clientbot` protocol module is a functional (though not very elegant) example of this.
|
||||
|
||||
1) Handle incoming commands from the uplink IRCd.
|
||||
When writing new protocol modules, it is recommended to subclass from one of the following classes:
|
||||
|
||||
2) Return [hook data](hooks-reference.md) for relevant commands, so that plugins can receive data from IRC.
|
||||
### `classes.IRCNetwork`
|
||||
|
||||
3) Make sure channel/user states are kept correctly. Joins, quits, parts, kicks, mode changes, nick changes, etc. should all be handled accurately.
|
||||
`IRCNetwork` is the base IRC class which includes the state checking utilities from `PyLinkNetworkCore`, the generic IRC utilities from `PyLinkNetworkCoreWithUtils`, along with abstraction for establishing IRC connections and pinging the uplink at a set interval.
|
||||
|
||||
4) Respond to both pings *and* pongs - the `irc.lastping` attribute **must** be set to the current time whenever a `PONG` is received from the uplink, so PyLink's doesn't [lag out the uplink](https://github.com/GLolol/PyLink/blob/1.0-beta1/classes.py#L383-L386) thinking that it isn't responding to our pings.
|
||||
To use `classes.IRCNetwork`, the following functions must be defined:
|
||||
|
||||
5) Implement a series of outgoing command functions (see below), used by plugins to send commands to IRC.
|
||||
- `handle_events(self, data)`: given a line of text containing an IRC command, parse it and return a hook payload as specified in the [PyLink hooks reference](hooks-reference.md).
|
||||
- In all of the official PyLink modules so far, handling for specific commands is delegated into submethods via [`getattr()`](https://github.com/jlu5/PyLink/blob/3922d44173593e4bcceae1218bbc6f267caa9fc1/protocols/ircs2s_common.py#L409-L412), and unknown commands are ignored.
|
||||
- `post_connect(self)`: This method sends the server introduction commands to the uplink IRC server. This method replaces the `connect()` function defined by protocol modules prior to PyLink 2.x.
|
||||
- `_ping_uplink(self)`: Sends a ping command to the uplink. No return value is expected / used.
|
||||
|
||||
6) Set the threading.Event object `irc.connected` (via `irc.connected.set()`) when the protocol negotiation with the uplink is complete. This is important for plugins like Relay which must check that links are ready before spawning clients, and they will fail to work if this is not set.
|
||||
This class offers the most flexibility because the protocol module can choose how it wants to handle any command. However, because most IRC server protocols use the same RFC 1459-style message format, rewriting the entire event handler is often not worth doing. Instead, it may be better to use `IRCS2SProtocol`, as documented below, which includes a `handle_events` method which handles most cases (TS5/6, P10, and TS-less protocols such as ngIRCd).
|
||||
|
||||
7) Check that `recvpass` is correct (when applicable), and raise `ProtocolError` with a relevant error message if not.
|
||||
- An exception to this general statement is `clientbot`, whose event handler also checks for unknown message senders and enumerates them when such a message is received.
|
||||
|
||||
## Core functions
|
||||
### `protocols.ircs2s_common.IRCCommonProtocol`
|
||||
|
||||
The following functions *must* be implemented by any protocol module within its main class, since they are used by the IRC object internals.
|
||||
`IRCCommonProtocol` (based off `IRCNetwork`) includes more IRC-specific methods such as parsers for ISUPPORT, as well as helper methods to parse arguments and recursively handle SQUIT. It also defines a default `_ping_uplink()` and incoming command handlers for commands that are the same across known protocols (AWAY, PONG, ERROR).
|
||||
|
||||
- **`connect`**`(self)` - Initializes a connection to a server.
|
||||
`IRCCommonProtocol` does *not*, however, define an `handle_events` method.
|
||||
|
||||
- **`handle_events`**`(self, line)` - Handles inbound data (lines of text) from the uplink IRC server. Normally, this will pass commands to other command handlers within the protocol module, while dropping commands that are unrecognized (wildcard handling). This is type of handling is only a guideline, as it's technically possible to structure event listeners any way you want.
|
||||
### `protocols.ircs2s_common.IRCS2SProtocol`
|
||||
`IRCS2SProtocol` is the most complete base server class, including a generic `handle_events()` supporting most IRC S2S message styles (i.e. prefix-less messages, protocols with and without UIDs). It also defines some incoming and outgoing command functions that hardly vary between protocols: `invite()`, `kick()`, `message()`, `notice()`, `numeric()`, `part()`, `quit()`, `squit()`, and `topic()` as of PyLink 2.0. This list is subject to change in future releases.
|
||||
|
||||
- **`ping`**`(self, source=None, target=None)` - Sends a PING to a target server. Periodic PINGs are sent to our uplink automatically by the [`Irc()`
|
||||
internals](https://github.com/GLolol/PyLink/blob/1.0-beta1/classes.py#L474-L483); plugins shouldn't have to use this.
|
||||
### For non-IRC protocols: `classes.PyLinkNetworkCoreWithUtils`
|
||||
Although no such transports have been implemented yet, PyLink leaves some level of abstraction for non-IRC protocols (e.g. Discord, Telegram, Slack, ...) by providing generic classes that only include state checking and utility functions.
|
||||
|
||||
### Outgoing command functions
|
||||
Subclassing one of the `PyLinkNetworkCore*` classes means that a protocol module only needs to define one method of entry: `connect()`, and must set up its own message handling stack. Protocol configuration validation checks and autoconnect must also be reimplemented. IRC-style utility functions (i.e. `PyLinkNetworkCoreWithUtils` methods) should also be reimplemented / overridden when applicable.
|
||||
|
||||
- **`spawnClient`**`(self, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None, manipulatable=False)` - Spawns a client on the PyLink server. No nick collision / valid nickname checks are done by protocol modules, as it is up to plugins to make sure they don't introduce anything invalid.
|
||||
(Unfortunately, this work is complicated, so please get in touch with us if you're stuck or want tips!)
|
||||
|
||||
### Other
|
||||
|
||||
For protocols that are closely related to existing ones, it may be wise to subclass off of an existing protocol class. For example, the `hybrid` and `ratbox` modules are based off of `ts6`. However, these protocol modules *do not guarantee API stability*, so we recommend letting us know of your intentions beforehand.
|
||||
|
||||
## Outgoing command functions
|
||||
|
||||
The methods defined below are integral to any protocol module, as they are needed by plugins to communicate with the rest of the world.
|
||||
|
||||
Unless otherwise noted, the camel-case variants of command functions (e.g. "`spawnClient`) are supported but deprecated. Protocol modules do *not* need to implement these aliases themselves; attempts to missing camel case functions are automatically coersed into their snake case variants via the [`structures.CamelCaseToSnakeCase`](https://github.com/jlu5/PyLink/blob/3922d44173593e4bcceae1218bbc6f267caa9fc1/structures.py#L172-L197) wrapper.
|
||||
|
||||
- **`spawn_client`**`(self, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None, manipulatable=False)` - Spawns a client on the PyLink server. No nick collision / valid nickname checks are done by protocol modules, as it is up to plugins to make sure they don't introduce anything invalid.
|
||||
- `modes` is a list or set of `(mode char, mode arg)` tuples in the [PyLink mode format](#mode-formats).
|
||||
- `ident` and `host` default to "null", while `realhost` defaults to the same things as `host` if not defined.
|
||||
- `realname` defaults to the real name specified in the PyLink config, if not given.
|
||||
- `ts` defaults to the current time if not given.
|
||||
- `opertype` (the oper type name, if applicable) defaults to the simple text of `IRC Operator`.
|
||||
- `ident` and `host` should default to "null", while `realhost` should default to the same things as `host` if not defined.
|
||||
- `realname` should default to the real name specified in the PyLink config, if not given.
|
||||
- `ts` should default to the current time if not given.
|
||||
- `opertype` (the oper type name, if applicable) should default to the simple text of `IRC Operator`.
|
||||
- The `manipulatable` option toggles whether the client spawned should be considered protected. Currently, all this does is prevent commands from plugins like `bots` from modifying these clients, but future client protections (anti-kill flood, etc.) may also depend on this.
|
||||
- The `server` option optionally takes a SID of any PyLink server, and spawns the client on the one given. It will default to the root PyLink server.
|
||||
- The `server` option optionally takes a SID of any PyLink server, and spawns the client on the one given. It should default to the root PyLink server if not specified.
|
||||
|
||||
- **`join`**`(self, client, channel)` - Joins the given client UID given to a channel.
|
||||
|
||||
@ -57,11 +73,11 @@ internals](https://github.com/GLolol/PyLink/blob/1.0-beta1/classes.py#L474-L483)
|
||||
|
||||
- **`invite`**`(self, source, target, channel)` - Sends an INVITE from a PyLink client.
|
||||
|
||||
- **`kick`**`(self, source, channel, target, reason=None)` - Sends a kick from a PyLink client/server.
|
||||
- **`kick`**`(self, source, channel, target, reason=None)` - Sends a kick from a PyLink client/server. This should raise `NotImplementedError` if not supported by a protocol.
|
||||
|
||||
- **`kill`**`(self, source, target, reason)` - Sends a kill from a PyLink client/server.
|
||||
- **`kill`**`(self, source, target, reason)` - Sends a kill from a PyLink client/server. This should raise `NotImplementedError` if not supported by a protocol.
|
||||
|
||||
- **`knock`**`(self, source, target, text)` - Sends a KNOCK from a PyLink client.
|
||||
- **`knock`**`(self, source, target, text)` - Sends a KNOCK from a PyLink client. This should raise `NotImplementedError` if not supported by a protocol.
|
||||
|
||||
- **`message`**`(self, source, target, text)` - Sends a PRIVMSG from a PyLink client.
|
||||
|
||||
@ -71,7 +87,7 @@ internals](https://github.com/GLolol/PyLink/blob/1.0-beta1/classes.py#L474-L483)
|
||||
|
||||
- **`notice`**`(self, source, target, text)` - Sends a NOTICE from a PyLink client or server.
|
||||
|
||||
- **`numeric`**`(self, source, numeric, target, text)` - Sends a raw numeric `numeric` with `text` from the `source` server to `target`.
|
||||
- **`numeric`**`(self, source, numeric, target, text)` - Sends a raw numeric `numeric` with `text` from the `source` server to `target`. This should raise `NotImplementedError` if not supported on a protocol.
|
||||
|
||||
- **`part`**`(self, client, channel, reason=None)` - Sends a part from a PyLink client.
|
||||
|
||||
@ -80,71 +96,122 @@ internals](https://github.com/GLolol/PyLink/blob/1.0-beta1/classes.py#L474-L483)
|
||||
- **`sjoin`**`(self, server, channel, users, ts=None, modes=set())` - Sends an SJOIN for a group of users to a channel. The sender should always be a Server ID (SID). TS is
|
||||
optional, and defaults to the one we've stored in the channel state if not given. `users` is a list of `(prefix mode, UID)` pairs. Example uses:
|
||||
- `sjoin('100', '#test', [('', '100AAABBC'), ('qo', 100AAABBB'), ('h', '100AAADDD')])`
|
||||
- `sjoin(self.irc.sid, '#test', [('o', self.irc.pseudoclient.uid)])`
|
||||
- `sjoin(self.sid, '#test', [('o', self.pseudoclient.uid)])`
|
||||
|
||||
- **`spawnServer`**`(self, name, sid=None, uplink=None, desc=None)` - Spawns a server off another PyLink server. `desc` (server description) defaults to the one in the config. `uplink` defaults to the main PyLink server, and `sid` (the server ID) is automatically generated if not given. Sanity checks for server name and SID validity ARE done by the protocol module here.
|
||||
- **`spawn_server`**`(self, name, sid=None, uplink=None, desc=None)` - Spawns a server off another PyLink server. `desc` (server description) defaults to the one in the config. `uplink` defaults to the main PyLink server, and `sid` (the server ID) is automatically generated if not given. Sanity checks for server name and SID validity ARE done by the protocol module here.
|
||||
|
||||
- **`squit`**`(self, source, target, text='No reason given')` - SQUITs a PyLink server.
|
||||
|
||||
- **`topic`**`(self, source, target, text)` - Sends a topic change from a PyLink client.
|
||||
- **`topic`**`(self, source, target, text)` - Sends a topic change from a PyLink *client.
|
||||
|
||||
- **`topicBurst`**`(self, source, target, text)` - Sends a topic change from a PyLink server. This is usually used on burst.
|
||||
- **`topic_burst`**`(self, source, target, text)` - Sends a topic change from a PyLink server. This is usually used on burst.
|
||||
|
||||
- **`updateClient`**`(self, source, field, text)` - Updates the ident, host, or realname of a PyLink client. `field` should be either "IDENT", "HOST", "GECOS", or
|
||||
"REALNAME". If changing the field given on the IRCd isn't supported, `NotImplementedError` should be raised.
|
||||
- **`update_client`**`(self, source, field, text)` - Updates the ident, host, or realname of a PyLink client. `field` should be either "IDENT", "HOST", "GECOS", or "REALNAME". If changing the field given on the IRCd isn't supported, `NotImplementedError` should be raised.
|
||||
|
||||
## Things to note
|
||||
## Special variables
|
||||
|
||||
### Special variables
|
||||
|
||||
A protocol module should also set the following variables in their protocol class:
|
||||
A protocol module should also set the following variables in each instance:
|
||||
|
||||
- `self.casemapping`: a string (`'rfc1459'` or `'ascii'`) to determine which case mapping the IRCd uses.
|
||||
- `self.hook_map`: this is a `dict`, which maps non-standard command names sent by the IRCd to those used by [PyLink hooks](hooks-reference.md).
|
||||
- Examples exist in the [UnrealIRCd](https://github.com/GLolol/PyLink/blob/1.0-beta1/protocols/unreal.py#L24-L27) and [InspIRCd](https://github.com/GLolol/PyLink/blob/1.0-beta1/protocols/inspircd.py#L25-L28) modules.
|
||||
- Examples exist in the [UnrealIRCd](https://github.com/jlu5/PyLink/blob/1.0-beta1/protocols/unreal.py#L24-L27) and [InspIRCd](https://github.com/jlu5/PyLink/blob/1.0-beta1/protocols/inspircd.py#L25-L28) modules.
|
||||
- `self.conf_keys`: a set of strings determining which server configuration options a protocol module needs to function; see the [Configuration key validation](#configuration-key-validation) section below.
|
||||
|
||||
#### IRC object variables
|
||||
|
||||
A protocol module manipulates the following attributes in the IRC object it is attached to:
|
||||
|
||||
- `self.irc.cmodes` / `self.irc.umodes`: These are mappings of named IRC modes (e.g. `inviteonly` or `moderated`) to a string list of mode letters, that should be either set during link negotiation or hardcoded into the protocol module. There are also special keys: `*A`, `*B`, `*C`, and `*D`, which **must** be set properly with a list of mode characters for that type of mode.
|
||||
- `self.cmodes` / `self.umodes`: These are mappings of named IRC modes (e.g. `inviteonly` or `moderated`) to a string list of mode letters, that should be either set during link negotiation or hardcoded into the protocol module. There are also special keys: `*A`, `*B`, `*C`, and `*D`, which **must** be set properly with a list of mode characters for that type of mode.
|
||||
- Types of modes are defined as follows (from http://www.irc.org/tech_docs/005.html):
|
||||
- A = Mode that adds or removes a nick or address to a list. Always has a parameter.
|
||||
- B = Mode that changes a setting and always has a parameter.
|
||||
- C = Mode that changes a setting and only has a parameter when set.
|
||||
- D = Mode that changes a setting and never has a parameter.
|
||||
- If not defined, these will default to modes defined by RFC 1459: https://github.com/GLolol/PyLink/blob/1.0-beta1/classes.py#L127-L152
|
||||
- An example of mode mapping hardcoding can be found here: https://github.com/GLolol/PyLink/blob/1.0-beta1/protocols/ts6.py#L259-L311
|
||||
- If not defined, these will default to modes defined by RFC 1459: https://github.com/jlu5/PyLink/blob/1.0-beta1/classes.py#L127-L152
|
||||
- An example of mode mapping hardcoding can be found here: https://github.com/jlu5/PyLink/blob/1.0-beta1/protocols/ts6.py#L259-L311
|
||||
- You can find a list of supported (named) channel modes [here](channel-modes.csv), and a list of user modes [here](user-modes.csv).
|
||||
- `self.irc.prefixmodes`: This defines a mapping of prefix modes (+o, +v, etc.) to their respective mode prefix. This will default to `{'o': '@', 'v': '+'}` (the standard op and voice) if not defined.
|
||||
- Example: `self.irc.prefixmodes = {'o': '@', 'h': '%', 'v': '+'}`
|
||||
- `self.prefixmodes`: This defines a mapping of prefix modes (+o, +v, etc.) to their respective mode prefix. This will default to `{'o': '@', 'v': '+'}` (the standard op and voice) if not defined.
|
||||
- Example: `self.prefixmodes = {'o': '@', 'h': '%', 'v': '+'}`
|
||||
- `self.connected`: this is a `threading.Event` object that plugins use to determine if the network has finished bursting. Protocol modules should set this to True via `self.connected.set()` when ready.
|
||||
|
||||
### Topics
|
||||
## PyLink Protocol capabilities
|
||||
PyLink 1.2 introduced the concept of protocol-defined capabilities, so that plugins wishing to use IRCd-specific features don't have to hard code protocol modules by name. Protocol capabilities are defined in `self.protocol_caps` (a set of strings) and may be changed freely before `self.connected` is set. Individual capabilities are then checked by plugins via `irc.has_cap(capability_name)`.
|
||||
|
||||
When receiving or sending topics, there is a `topicset` attribute in the IRC channel (IrcChannel) object that should be set **True**. It simply denotes that a topic has been set in the channel at least once. Relay uses this so it doesn't overwrite topics with empty ones during burst, when a relay channel initialize before the uplink has sent the topic for it.
|
||||
As of writing, the following protocol capabilities (case-sensitive) are implemented:
|
||||
|
||||
*Caveat:* Topic handling is not yet subject to TS rules (which vary by IRCds) and are currently blindly accepted. https://github.com/GLolol/PyLink/issues/277
|
||||
### Supported protocol capabilities
|
||||
- `can-host-relay` - whether servers using this protocol can host a relay channel (for sanity reasons, this should be off for anything that's not IRC S2S)
|
||||
- `can-spawn-clients` - determines whether any spawned clients are real or virtual (mainly for `services_support`).
|
||||
- `can-track-servers` - determines whether servers are accurately tracked (for `servermaps` and other statistics)
|
||||
- `has-statusmsg` - whether STATUSMSG messages (e.g. `@#channel`) are supported
|
||||
- `has-ts` - determines whether channel and user timestamps are trackable (and not just spoofed)
|
||||
- `slash-in-hosts` - determines whether `/` is allowed in hostnames
|
||||
- `slash-in-nicks` - determines whether `/` is allowed in nicks
|
||||
- `ssl-should-verify` - determines whether TLS certificates should be checked for validity by default - this should be enabled for any protocol modules needing to verify a remote server (e.g. Clientbot or a non-IRC API endpoint), and disabled for most IRC S2S links (where self-signed certs are widespread)
|
||||
- `underscore-in-hosts` - determines whether `_` is allowed in client hostnames (yes, support for this actually varies by IRCd)
|
||||
- `visible-state-only` - determines whether channels should be autocleared when the PyLink client leaves (for clientbot, etc.)
|
||||
- Note: enabling this in a protocol module lets `coremods/handlers` automatically clean up old channels for you!
|
||||
|
||||
New protocol capabilities are generally added when needed - see https://github.com/jlu5/PyLink/issues/620
|
||||
|
||||
### Abstraction defaults
|
||||
|
||||
For reference, the `IRCS2SProtocol` class defines the following by default:
|
||||
- `can-host-relay`
|
||||
- `can-spawn-clients`
|
||||
- `can-track-servers`
|
||||
- `has-ts`
|
||||
|
||||
Whereas `PyLinkNetworkCore` defines no capabilities (i.e. the empty set) by default.
|
||||
|
||||
## PyLink structures
|
||||
In this section, `self` refers to the network object/protocol module instance itself (i.e. from its own perspective).
|
||||
|
||||
### Server, User, Channel classes
|
||||
PyLink defines classes named `Server`, `User`, and `Channel` in the `classes` module, and stores dictionaries of these in the `servers`, `users`, and `channels` attributes of a protocol object respectively.
|
||||
|
||||
- `self.servers` is a dictionary mapping server IDs (SIDs) to `Server` objects. If a protocol module does not use SIDs, servers are stored by server name instead.
|
||||
|
||||
- `self.users` is a dictionary mapping user IDs (UIDs) to `User` objects. If a protocol module does not use UIDs, a pseudo UID (PUID) generator such as [`classes.PUIDGenerator`](https://github.com/jlu5/PyLink/blob/3922d44173593e4bcceae1218bbc6f267caa9fc1/classes.py#L1710-L1726) *must* be used instead.
|
||||
- The rationale behind this is because plugins tracking user lists are not designed to remove and re-add users when they change their nicks.
|
||||
- When sending text back to the protocol module, it may be helpful to use the [`_expandPUID()`](https://github.com/jlu5/PyLink/blob/4a363aee509c5a0488a38b9e60f93ec59a274c3c/classes.py#L1213-L1231) function in `PyLinkNetworkCoreWithUtils` to expand these pseudo-UIDs back to regular nicks.
|
||||
|
||||
- `self._channels` and `self.channels` are [IRC case-insensitive dictionaries](https://github.com/jlu5/PyLink/blob/4a363aee509c5a0488a38b9e60f93ec59a274c3c/structures.py#L114-L116) mapping channel names to Channel objects.
|
||||
- The key difference between these two dictionaries is that `_channels` is powered by `classes.ChannelState` and creates new channels *automatically* when they are accessed by index. This makes writing protocol modules easier, as they can assume that the channels they wish to modify always exist (no chance of `KeyError`!).
|
||||
- `self.channels`, on the other hand, does *not* implicitly create channels and is thus better suited for plugins.
|
||||
|
||||
The `Channel`, `User`, and `Server` classes are initiated as follows:
|
||||
|
||||
- `Channel(self, name)` - First arg is the protocol object, second is the channel name.
|
||||
- `User(self, nick, ts, uid, server, ident='null', host='null', realname='PyLink dummy client', realhost='null', ip='0.0.0.0', manipulatable=False, opertype='IRC Operator')` - These arguments are essentially the same as `spawn_client()`'s.
|
||||
- `Server(self, uplink, name, internal=False, desc="(None given)")`
|
||||
- The `uplink` (type `str`) option sets the SID of the uplink server, or *None* for both the main PyLink server and its uplink.
|
||||
- The `name` option sets the server name.
|
||||
- The `internal` boolean sets whether the server is an internal PyLink server.
|
||||
- The `desc` option sets the server description, when applicable.
|
||||
|
||||
#### Statekeeping specifics
|
||||
- When a user is introduced, their UID must be added to both `self.users` and to the `users` set in the `Server` object hosting the user (`self.servers[SID].users`). The latter list is used internally to track SQUITs.
|
||||
- When a user joins a channel, the channel name is added to the User object's `channels` set (`self.users[UID].channels`), as well as the Channel object's user list (`self.channels[CHANNELNAME].users`)
|
||||
- When a user disconnects, the `_remove_client` helper method can be called on their UID to automatically remove them from the relevant Server object, as well as all channels they were in.
|
||||
- When a user leaves a channel, the `Channel.remove_user()` method can be used to easily remove them from the channel state, and vice versa.
|
||||
|
||||
### Mode formats
|
||||
|
||||
Modes are stored a special format in PyLink, different from raw mode strings in order to make them easier to parse. Mode strings can be turned into mode *lists*, which are used to represent mode changes in hooks, and when storing modes internally.
|
||||
Modes are stored not stored as strings, but lists of mode pairs in order to ease parsing. These lists of mode pairs are used both to represent mode changes in hooks and store modes internally.
|
||||
|
||||
`irc.parseModes(target, modestring)` is used to convert mode strings to mode lists. `target` is the channel name/UID the mode is being set on, while `modestring` takes either a string or string split by spaces (really a list).
|
||||
`self.parse_modes(target, modestring)` is used to convert mode strings to mode lists. `target` is the channel name/UID the mode is being set on, while `modestring` takes either a string or string split by spaces (really a list).
|
||||
|
||||
- `irc.parseModes('#chat', ['+tHIs', '*!*@is.sparta'])` would give:
|
||||
- `self.parse_modes('#chat', ['+tHIs', '*!*@is.sparta'])` would give:
|
||||
- `[('+t', None), ('+H', None), ('+I', '*!*@is.sparta'), ('+s', None)]`
|
||||
|
||||
`parseModes` will also automatically convert prefix mode targets from nicks to UIDs, and drop any duplicate (already set) or invalid (e.g. missing argument) modes.
|
||||
`parse_modes()` will also automatically convert prefix mode targets from nicks to UIDs, and drop any duplicate (already set) or invalid (e.g. missing argument) modes.
|
||||
|
||||
- `irc.parseModes('#chat', ['+ol invalidnick'])`:
|
||||
- `self.parse_modes('#chat', ['+ol invalidnick'])`:
|
||||
- `[]`
|
||||
- `irc.parseModes('#chat', ['+o GLolol'])`:
|
||||
- `self.parse_modes('#chat', ['+o GLolol'])`:
|
||||
- `[('+o', '001ZJZW01')]`
|
||||
|
||||
Then, a parsed mode list can be applied to channel name or UID using `irc.applyModes(target, parsed_modelist)`. **Note**: for protocols that accept or reject mode changes based on TS (i.e. practically every IRCd), you may want to use [`Protocol.updateTS(...)`](https://github.com/GLolol/PyLink/blob/1.0-beta1/classes.py#L1252-L1261) to handle TS changes more efficiently.
|
||||
Afterwords, a parsed mode list can be applied to channel name or UID using `self.apply_modes(target, parsed_modelist)`.
|
||||
|
||||
Internally, modes are stored in `IrcChannel` and `IrcUser` objects as sets, with the `+` prefixing each mode character omitted. This set is accessed via the `modes` attribute:
|
||||
**Note**: for protocols that accept or reject mode changes based on TS (i.e. practically every IRCd), you will want to use [`updateTS(...)`](https://github.com/jlu5/PyLink/blob/master/classes.py#L1484-L1487) instead to only apply the modes if the source TS is lower.
|
||||
|
||||
Internally, modes are stored in `Channel` and `User` objects as sets, **with the `+` prefixing each mode character omitted**. These sets are accessed via the `modes` attribute:
|
||||
|
||||
```
|
||||
<+GLolol> PyLink-devel, eval irc.users[source].modes
|
||||
@ -153,7 +220,7 @@ Internally, modes are stored in `IrcChannel` and `IrcUser` objects as sets, with
|
||||
<@PyLink-devel> {('n', None), ('t', None)}
|
||||
```
|
||||
|
||||
**Exception**: the owner, admin, op, halfop, and voice channel prefix modes are stored separately as a dict of sets in `IrcChannel.prefixmodes`:
|
||||
**Exception**: the owner, admin, op, halfop, and voice channel prefix modes are stored separately as a dict of sets in `Channel.prefixmodes`:
|
||||
|
||||
```
|
||||
<@GLolol> PyLink-devel, eval irc.channels['#chat'].prefixmodes
|
||||
@ -162,15 +229,56 @@ Internally, modes are stored in `IrcChannel` and `IrcUser` objects as sets, with
|
||||
|
||||
When a certain mode (e.g. owner) isn't supported on a network, the key still exists in `prefixmodes` but is simply unused.
|
||||
|
||||
### Configuration key validation
|
||||
### Topics
|
||||
|
||||
Starting with PyLink 0.10.x, protocol modules can specify which config values within a server block they need in order to work. This is done by adjusting the `self.conf_keys` attribute, usually in the protocol module's `__init__()` method. The default set, defined in [`Classes.Protocol`](https://github.com/GLolol/PyLink/blob/1.0-beta1/classes.py#L1202-L1204), includes `{'ip', 'port', 'hostname', 'sid', 'sidrange', 'protocol', 'sendpass', 'recvpass'}`. Should any of these keys be missing from a server block, PyLink will bail with a configuration error.
|
||||
When receiving or sending topics, there is a `topicset` attribute in the `Channel` object that should be set to **True**. This boolean denotes that a topic has been set in the channel at least once; Relay uses it to know not to overwrite topics with empty ones during startup, when topics have not been received from all networks yet.
|
||||
|
||||
As an example, one protocol module that tweaks this is [`Clientbot`](https://github.com/GLolol/PyLink/blob/1.0-beta1/protocols/clientbot.py#L17-L18), which removes all options except `ip`, `protocol`, and `port`.
|
||||
*Caveat:* Topic handlers on the current protocol modules do not follow TS rules (which vary by IRCd), and blindly accept data. See issue https://github.com/jlu5/PyLink/issues/277
|
||||
|
||||
## Changes
|
||||
## Configuration key validation
|
||||
|
||||
Starting with PyLink 1.x, protocol modules can specify which config values within a server block they need in order to work. This is done by adjusting the `self.conf_keys` attribute, usually in the protocol module's `__init__()` method. The default set, defined in [`Classes.Protocol`](https://github.com/jlu5/PyLink/blob/1.0-beta1/classes.py#L1202-L1204), includes `{'ip', 'port', 'hostname', 'sid', 'sidrange', 'protocol', 'sendpass', 'recvpass'}`. Should any of these keys be missing from a server block, PyLink will bail with a configuration error.
|
||||
|
||||
As an example, one protocol module that tweaks this is [`Clientbot`](https://github.com/jlu5/PyLink/blob/1.0-beta1/protocols/clientbot.py#L17-L18), which removes all options except `ip`, `protocol`, and `port`.
|
||||
|
||||
## The final checklist
|
||||
|
||||
In short, protocol modules have some very important jobs. If any of these aren't done correctly, you will be left with a very broken, desynced services server:
|
||||
|
||||
1) Handle incoming commands from the uplink.
|
||||
|
||||
2) Return [hook data](hooks-reference.md) for relevant commands, so that plugins can receive data from the uplink.
|
||||
|
||||
3) Make sure channel/user states are kept correctly. Joins, quits, parts, kicks, mode changes, nick changes, etc. should all be handled accurately where relevant.
|
||||
|
||||
4) Implement the specified outgoing command functions, which are used by plugins to send commands to the uplink.
|
||||
|
||||
5) Set the `threading.Event` instance `self.connected` to True (via `self.connected.set()`) when the connection with the uplink is fully established. This is important for Relay and the services API, which will refuse to initialize if the connection is not marked ready.
|
||||
|
||||
6) Check that `recvpass` is correct when applicable, and raise `ProtocolError` with a relevant error message if not.
|
||||
|
||||
7) Declare the correct set of protocol module capabilities to prevent confusing PyLink's plugins.
|
||||
|
||||
## Changes to this document
|
||||
* 2018-07-11 (2.0.0)
|
||||
- Version bump for 2.0 stable release; no meaningful content changes.
|
||||
* 2018-06-26 (2.0-beta1)
|
||||
- Added documentation for PyLink protocol capabilities
|
||||
- Wording tweaks, restructured headings
|
||||
- Consistently refer to protocol module attributes as `self.<whatever>` instead of `irc.<whatever>`
|
||||
* 2018-05-09 (2.0-alpha3)
|
||||
- `kill` and `kick` implementations should raise `NotImplementedError` if not supported (anti-desync measure).
|
||||
- Future PyLink versions will further standardize which functions should be stubbed (no-op) when not available and which should raise an error.
|
||||
* 2017-10-05 (2.0-alpha1)
|
||||
- Added notes on user statekeeping and the tracking/helper functions used.
|
||||
- Mention the `post_connect()` function that must be defined by protocols inheriting from IRCNetwork.
|
||||
* 2017-08-30 (2.0-dev)
|
||||
- Rewritten specification for the IRC-protocol class convergence in PyLink 2.0.
|
||||
- Updated the spec for 2.0 method renames and class restructures.
|
||||
- Added a proper "Starting Steps" section detailing which new classes inherit from and when.
|
||||
- Explicitly document the Server, User, and Channel classes.
|
||||
* 2017-03-15 (1.2-dev)
|
||||
- Corrected the location of `self.irc.cmodes/umodes/prefixmodes` attributes
|
||||
- Corrected the location of `self.cmodes/umodes/prefixmodes` attributes
|
||||
- Mention `self.conf_keys` as a special variable for completeness
|
||||
* 2017-01-29 (1.2-dev)
|
||||
- NOTICE can now be sent from servers.
|
||||
|
@ -1,29 +1,47 @@
|
||||
/* Graph showing inheritance with the current PyLink protocol protocols:
|
||||
* Update using: dot -Tpng protocol-modules.dot > protocol-modules.png
|
||||
/* Graph showing inheritance with the current PyLink protocol modules:
|
||||
* Update using: dot -Tsvg protocol-modules.dot > protocol-modules.svg
|
||||
*/
|
||||
|
||||
digraph G {
|
||||
ratio = 0.8; /* make the graph wider than tall */
|
||||
|
||||
edge [ penwidth=0.75, color="#111111CC" ];
|
||||
subgraph cluster_core {
|
||||
label="Core classes (pylinkirc.classes)";
|
||||
style="filled";
|
||||
node [style="filled",color="white"];
|
||||
color="#90EE90";
|
||||
|
||||
"PyLinkNetworkCore" -> "PyLinkNetworkCoreWithUtils" -> "IRCNetwork";
|
||||
}
|
||||
|
||||
subgraph cluster_helper {
|
||||
label="Protocol module helpers";
|
||||
label="Protocol module helpers\n(pylinkirc.protocols.ircs2s_common)";
|
||||
style="filled";
|
||||
node [style="filled",color="white"];
|
||||
color="lightblue";
|
||||
|
||||
"ircs2s_common.py" -> "ts6_common.py";
|
||||
"IRCNetwork" -> "IRCCommonProtocol" -> "IRCS2SProtocol" -> "TS6BaseProtocol";
|
||||
|
||||
subgraph cluster_helper {
|
||||
label="pylinkirc.protocols.ts6_common";
|
||||
style="filled";
|
||||
color="lightcyan";
|
||||
|
||||
"TS6BaseProtocol";
|
||||
}
|
||||
}
|
||||
|
||||
subgraph cluster_pluggable {
|
||||
label="Pluggable (full) protocol modules";
|
||||
label="Complete protocol modules (pylinkirc.protocols.*)";
|
||||
style="filled";
|
||||
node [style="filled",color="white"];
|
||||
color="khaki";
|
||||
|
||||
"ircs2s_common.py" -> "p10.py";
|
||||
"ts6_common.py" -> "ts6.py" -> "hybrid.py";
|
||||
"ts6.py" -> "ratbox.py";
|
||||
"ts6_common.py" -> "inspircd.py";
|
||||
"ts6_common.py" -> "unreal.py";
|
||||
"clientbot.py";
|
||||
"IRCS2SProtocol" -> "p10";
|
||||
"IRCS2SProtocol" -> "ngircd";
|
||||
"TS6BaseProtocol" -> "ts6" -> "hybrid";
|
||||
"TS6BaseProtocol" -> "inspircd";
|
||||
"TS6BaseProtocol" -> "unreal";
|
||||
"IRCCommonProtocol" -> "clientbot";
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 40 KiB |
155
docs/technical/protocol-modules.svg
Normal file
155
docs/technical/protocol-modules.svg
Normal file
@ -0,0 +1,155 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="530pt" height="651pt"
|
||||
viewBox="0.00 0.00 530.00 651.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 647)">
|
||||
<title>G</title>
|
||||
<polygon fill="white" stroke="none" points="-4,4 -4,-647 526,-647 526,4 -4,4"/>
|
||||
<g id="clust1" class="cluster"><title>cluster_core</title>
|
||||
<polygon fill="#90ee90" stroke="#90ee90" points="77,-416 77,-635 333,-635 333,-416 77,-416"/>
|
||||
<text text-anchor="middle" x="205" y="-619.8" font-family="Times,serif" font-size="14.00">Core classes (pylinkirc.classes)</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster"><title>cluster_helper</title>
|
||||
<polygon fill="lightblue" stroke="lightblue" points="83,-163 83,-408 302,-408 302,-163 83,-163"/>
|
||||
<text text-anchor="middle" x="192.5" y="-392.8" font-family="Times,serif" font-size="14.00">Protocol module helpers</text>
|
||||
<text text-anchor="middle" x="192.5" y="-377.8" font-family="Times,serif" font-size="14.00">(pylinkirc.protocols.ircs2s_common)</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster"><title>cluster_helper</title>
|
||||
<polygon fill="lightcyan" stroke="lightcyan" points="91,-171 91,-246 285,-246 285,-171 91,-171"/>
|
||||
<text text-anchor="middle" x="188" y="-230.8" font-family="Times,serif" font-size="14.00">pylinkirc.protocols.ts6_common</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster"><title>cluster_pluggable</title>
|
||||
<polygon fill="khaki" stroke="khaki" points="8,-8 8,-155 514,-155 514,-8 8,-8"/>
|
||||
<text text-anchor="middle" x="261" y="-139.8" font-family="Times,serif" font-size="14.00">Complete protocol modules (pylinkirc.protocols.*)</text>
|
||||
</g>
|
||||
<!-- PyLinkNetworkCore -->
|
||||
<g id="node1" class="node"><title>PyLinkNetworkCore</title>
|
||||
<ellipse fill="white" stroke="white" cx="205" cy="-586" rx="84.485" ry="18"/>
|
||||
<text text-anchor="middle" x="205" y="-582.3" font-family="Times,serif" font-size="14.00">PyLinkNetworkCore</text>
|
||||
</g>
|
||||
<!-- PyLinkNetworkCoreWithUtils -->
|
||||
<g id="node2" class="node"><title>PyLinkNetworkCoreWithUtils</title>
|
||||
<ellipse fill="white" stroke="white" cx="205" cy="-514" rx="119.679" ry="18"/>
|
||||
<text text-anchor="middle" x="205" y="-510.3" font-family="Times,serif" font-size="14.00">PyLinkNetworkCoreWithUtils</text>
|
||||
</g>
|
||||
<!-- PyLinkNetworkCore->PyLinkNetworkCoreWithUtils -->
|
||||
<g id="edge1" class="edge"><title>PyLinkNetworkCore->PyLinkNetworkCoreWithUtils</title>
|
||||
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M205,-567.697C205,-559.983 205,-550.712 205,-542.112"/>
|
||||
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="208.5,-542.104 205,-532.104 201.5,-542.104 208.5,-542.104"/>
|
||||
</g>
|
||||
<!-- IRCNetwork -->
|
||||
<g id="node3" class="node"><title>IRCNetwork</title>
|
||||
<ellipse fill="white" stroke="white" cx="205" cy="-442" rx="55.7903" ry="18"/>
|
||||
<text text-anchor="middle" x="205" y="-438.3" font-family="Times,serif" font-size="14.00">IRCNetwork</text>
|
||||
</g>
|
||||
<!-- PyLinkNetworkCoreWithUtils->IRCNetwork -->
|
||||
<g id="edge2" class="edge"><title>PyLinkNetworkCoreWithUtils->IRCNetwork</title>
|
||||
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M205,-495.697C205,-487.983 205,-478.712 205,-470.112"/>
|
||||
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="208.5,-470.104 205,-460.104 201.5,-470.104 208.5,-470.104"/>
|
||||
</g>
|
||||
<!-- IRCCommonProtocol -->
|
||||
<g id="node4" class="node"><title>IRCCommonProtocol</title>
|
||||
<ellipse fill="white" stroke="white" cx="205" cy="-344" rx="89.0842" ry="18"/>
|
||||
<text text-anchor="middle" x="205" y="-340.3" font-family="Times,serif" font-size="14.00">IRCCommonProtocol</text>
|
||||
</g>
|
||||
<!-- IRCNetwork->IRCCommonProtocol -->
|
||||
<g id="edge3" class="edge"><title>IRCNetwork->IRCCommonProtocol</title>
|
||||
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M205,-423.837C205,-409.503 205,-388.807 205,-372.216"/>
|
||||
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="208.5,-372.014 205,-362.014 201.5,-372.014 208.5,-372.014"/>
|
||||
</g>
|
||||
<!-- IRCS2SProtocol -->
|
||||
<g id="node5" class="node"><title>IRCS2SProtocol</title>
|
||||
<ellipse fill="white" stroke="white" cx="214" cy="-272" rx="69.5877" ry="18"/>
|
||||
<text text-anchor="middle" x="214" y="-268.3" font-family="Times,serif" font-size="14.00">IRCS2SProtocol</text>
|
||||
</g>
|
||||
<!-- IRCCommonProtocol->IRCS2SProtocol -->
|
||||
<g id="edge4" class="edge"><title>IRCCommonProtocol->IRCS2SProtocol</title>
|
||||
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M207.225,-325.697C208.217,-317.983 209.408,-308.712 210.514,-300.112"/>
|
||||
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="213.997,-300.469 211.801,-290.104 207.054,-299.576 213.997,-300.469"/>
|
||||
</g>
|
||||
<!-- clientbot -->
|
||||
<g id="node13" class="node"><title>clientbot</title>
|
||||
<ellipse fill="white" stroke="white" cx="464" cy="-106" rx="41.6928" ry="18"/>
|
||||
<text text-anchor="middle" x="464" y="-102.3" font-family="Times,serif" font-size="14.00">clientbot</text>
|
||||
</g>
|
||||
<!-- IRCCommonProtocol->clientbot -->
|
||||
<g id="edge12" class="edge"><title>IRCCommonProtocol->clientbot</title>
|
||||
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M233.699,-326.923C251.178,-316.837 273.771,-303.27 293,-290 319.052,-272.023 326.338,-268.098 349,-246 386.799,-209.142 424.679,-160.559 446.146,-131.67"/>
|
||||
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="449.147,-133.498 452.263,-123.372 443.512,-129.345 449.147,-133.498"/>
|
||||
</g>
|
||||
<!-- TS6BaseProtocol -->
|
||||
<g id="node6" class="node"><title>TS6BaseProtocol</title>
|
||||
<ellipse fill="white" stroke="white" cx="187" cy="-197" rx="72.2875" ry="18"/>
|
||||
<text text-anchor="middle" x="187" y="-193.3" font-family="Times,serif" font-size="14.00">TS6BaseProtocol</text>
|
||||
</g>
|
||||
<!-- IRCS2SProtocol->TS6BaseProtocol -->
|
||||
<g id="edge5" class="edge"><title>IRCS2SProtocol->TS6BaseProtocol</title>
|
||||
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M207.738,-254.069C204.49,-245.287 200.448,-234.36 196.798,-224.49"/>
|
||||
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="200.073,-223.257 193.322,-215.092 193.508,-225.685 200.073,-223.257"/>
|
||||
</g>
|
||||
<!-- p10 -->
|
||||
<g id="node7" class="node"><title>p10</title>
|
||||
<ellipse fill="white" stroke="white" cx="293" cy="-106" rx="27" ry="18"/>
|
||||
<text text-anchor="middle" x="293" y="-102.3" font-family="Times,serif" font-size="14.00">p10</text>
|
||||
</g>
|
||||
<!-- IRCS2SProtocol->p10 -->
|
||||
<g id="edge6" class="edge"><title>IRCS2SProtocol->p10</title>
|
||||
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M269.936,-261.101C277.361,-257.496 284.089,-252.611 289,-246 313.082,-213.582 307.4,-164.181 300.569,-133.833"/>
|
||||
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="303.928,-132.834 298.152,-123.952 297.129,-134.497 303.928,-132.834"/>
|
||||
</g>
|
||||
<!-- ngircd -->
|
||||
<g id="node8" class="node"><title>ngircd</title>
|
||||
<ellipse fill="white" stroke="white" cx="371" cy="-106" rx="33.2948" ry="18"/>
|
||||
<text text-anchor="middle" x="371" y="-102.3" font-family="Times,serif" font-size="14.00">ngircd</text>
|
||||
</g>
|
||||
<!-- IRCS2SProtocol->ngircd -->
|
||||
<g id="edge7" class="edge"><title>IRCS2SProtocol->ngircd</title>
|
||||
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M276.712,-264.047C289.461,-260.278 302.009,-254.58 312,-246 345.593,-217.151 360.541,-165.861 366.824,-134.301"/>
|
||||
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="370.276,-134.882 368.637,-124.415 363.391,-133.62 370.276,-134.882"/>
|
||||
</g>
|
||||
<!-- ts6 -->
|
||||
<g id="node9" class="node"><title>ts6</title>
|
||||
<ellipse fill="white" stroke="white" cx="43" cy="-106" rx="27" ry="18"/>
|
||||
<text text-anchor="middle" x="43" y="-102.3" font-family="Times,serif" font-size="14.00">ts6</text>
|
||||
</g>
|
||||
<!-- TS6BaseProtocol->ts6 -->
|
||||
<g id="edge8" class="edge"><title>TS6BaseProtocol->ts6</title>
|
||||
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M137.801,-183.792C118.234,-177.355 96.3078,-168.023 79,-155 70.5984,-148.678 63.2254,-139.88 57.3761,-131.54"/>
|
||||
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="60.2708,-129.572 51.8527,-123.138 54.4215,-133.417 60.2708,-129.572"/>
|
||||
</g>
|
||||
<!-- inspircd -->
|
||||
<g id="node11" class="node"><title>inspircd</title>
|
||||
<ellipse fill="white" stroke="white" cx="209" cy="-106" rx="38.9931" ry="18"/>
|
||||
<text text-anchor="middle" x="209" y="-102.3" font-family="Times,serif" font-size="14.00">inspircd</text>
|
||||
</g>
|
||||
<!-- TS6BaseProtocol->inspircd -->
|
||||
<g id="edge10" class="edge"><title>TS6BaseProtocol->inspircd</title>
|
||||
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M191.242,-178.84C194.377,-166.158 198.707,-148.64 202.307,-134.077"/>
|
||||
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="205.769,-134.655 204.771,-124.107 198.974,-132.975 205.769,-134.655"/>
|
||||
</g>
|
||||
<!-- unreal -->
|
||||
<g id="node12" class="node"><title>unreal</title>
|
||||
<ellipse fill="white" stroke="white" cx="120" cy="-106" rx="32.4942" ry="18"/>
|
||||
<text text-anchor="middle" x="120" y="-102.3" font-family="Times,serif" font-size="14.00">unreal</text>
|
||||
</g>
|
||||
<!-- TS6BaseProtocol->unreal -->
|
||||
<g id="edge11" class="edge"><title>TS6BaseProtocol->unreal</title>
|
||||
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M174.398,-179.26C164.099,-165.579 149.413,-146.071 137.895,-130.772"/>
|
||||
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="140.682,-128.653 131.871,-122.769 135.089,-132.863 140.682,-128.653"/>
|
||||
</g>
|
||||
<!-- hybrid -->
|
||||
<g id="node10" class="node"><title>hybrid</title>
|
||||
<ellipse fill="white" stroke="white" cx="50" cy="-34" rx="33.5952" ry="18"/>
|
||||
<text text-anchor="middle" x="50" y="-30.3" font-family="Times,serif" font-size="14.00">hybrid</text>
|
||||
</g>
|
||||
<!-- ts6->hybrid -->
|
||||
<g id="edge9" class="edge"><title>ts6->hybrid</title>
|
||||
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M44.7303,-87.6966C45.5017,-79.9827 46.4288,-70.7125 47.2888,-62.1124"/>
|
||||
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="50.7771,-62.403 48.2896,-52.1043 43.8118,-61.7064 50.7771,-62.403"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 11 KiB |
@ -2,16 +2,12 @@
|
||||
|
||||
This document documents the steps that I (James) use to release updates to PyLink.
|
||||
|
||||
1) Draft the next release & changelog at https://github.com/GLolol/PyLink/releases
|
||||
1) Draft the next release's changelog in `RELNOTES.md`
|
||||
|
||||
2) Copy/export the changelog draft to [RELNOTES.md](../../RELNOTES.md), using a new section.
|
||||
2) Bump the version in the [`VERSION`](VERSION) file.
|
||||
|
||||
- [`export_github_relnotes.py`](https://github.com/GLolol/codescraps/blob/master/utils/export_github_relnotes.py) allows automating this process, using the GitHub API and an optional login to read unpublished drafts.
|
||||
3) Commit the changes to `VERSION` and `RELNOTES.md`, and tag+sign this commit as the new release. Do not prefix version numbers with "v".
|
||||
|
||||
3) Bump the version in the [`VERSION`](VERSION) file.
|
||||
4) Publish the release via the GitHub release page, using the same changelog content as `RELNOTES.md`.
|
||||
|
||||
4) Commit the changes to `VERSION` and `RELNOTES.md`, and tag+sign this commit as the new release. Do not prefix version numbers with "v".
|
||||
|
||||
5) Publish the release via the GitHub release page.
|
||||
|
||||
6) For stable releases, also upload to PyPI: `python3 setup.py sdist upload`
|
||||
5) For stable releases, ~~also upload to PyPI: `python3 setup.py sdist upload`~~ PyPI uploads are handled automatically via Travis-CI.
|
||||
|
@ -1,10 +1,10 @@
|
||||
# PyLink Services Bot API
|
||||
|
||||
Starting with PyLink 0.9.x, a services bot API was introduced to make writing custom services slightly easier. PyLink's Services API automatically connects service bots, and handles rejoin on kick/kill all by itself, meaning less code is needed per plugin to have functional service bots.
|
||||
The goal of PyLink's Services API is to make writing custom services easier. It is able to automatically spawn service bots on connect, handle rejoins on kill and kick, and expose a way for plugins to bind commands to various services bots. It also handles U-line servprotect modes when enabled and supported on a particular network (i.e. the `protect_services` option).
|
||||
|
||||
## Creating new service bots
|
||||
## Basic service creation
|
||||
|
||||
Services can be created (registered) using code similar to the following in a plugin:
|
||||
Services can be registered and created using code similar to the following in a plugin:
|
||||
|
||||
```python
|
||||
|
||||
@ -14,41 +14,67 @@ from pylinkirc import utils, world
|
||||
desc = "Optional description of servicenick, in sentence form."
|
||||
|
||||
# First argument is the internal service name.
|
||||
# utils.registerService() returns a utils.ServiceBot instance, which can also be found
|
||||
# by calling world["myservice"].
|
||||
myservice = utils.registerService("myservice", desc=desc)
|
||||
# utils.register_service() returns a utils.ServiceBot instance, which is also stored
|
||||
# as world.services['myservice'].
|
||||
myservice = utils.register_service('myservice', desc=desc)
|
||||
```
|
||||
|
||||
`utils.registerService()` passes its arguments directly to the `utils.ServiceBot` class constructor, which in turn supports the following options:
|
||||
`utils.register_service()` passes its arguments directly to the `utils.ServiceBot` class constructor, which in turn supports the following options:
|
||||
|
||||
- **`name`** - defines the service name (mandatory)
|
||||
- `default_help` - Determines whether the default HELP command should be used for the service. Defaults to True.
|
||||
- `default_list` - Determines whether the default LIST command should be used for the service. Defaults to True.
|
||||
- `nick`, `ident` - Sets the default nick and ident for the service bot. If not given, these simply default to the service name.
|
||||
- `default_nick` - Sets the default nick this service should use if the user doesn't provide it. Defaults to the same as the service name.
|
||||
- `manipulatable` - Determines whether the bot is marked manipulatable. Only manipulatable clients can be force joined, etc. using PyLink commands. Defaults to False.
|
||||
- `desc` - Sets the command description of the service. This is shown in the default HELP command if enabled.
|
||||
|
||||
**NOTE**: It is a good practice for the SERVICE name in `utils.registerService("SERVICE")` to match your plugin name, as the service bot API implicitly loads [configuration options](../advanced-services-config.md) from config blocks named `SERVICE:`.
|
||||
**NOTE**: It is convention for the service name in `utils.register_service('SERVICE')` to match your plugin name, as the services API implicitly loads [configuration options](../advanced-services-config.md) from config blocks named `SERVICE:` (which you may want to put plugin options in as well).
|
||||
|
||||
Implementation note: if the `spawn_service` option is disabled (either globally or for your service bot), `register_service` will return the main PyLink `ServiceBot` instance (i.e. `world.services['pylink']`), which you can modify as usual. `unregister_service` calls to your service name will be silently ignored as no `ServiceBot` instance is actually registered for that name. Altogether, this allows service-spawning plugins to function normally regardless of the `spawn_service` value.
|
||||
|
||||
### Getting the UID of a bot
|
||||
|
||||
Should you want to get the UID of a service bot on a specific server, use `myservice.uids.get('irc.name')`
|
||||
To obtain the UID of a service bot on a specific network, use `myservice.uids.get(irc.name)` (where `irc` is the network object).
|
||||
|
||||
### Setting channels to join
|
||||
### Removing services on unload
|
||||
|
||||
All services bots will automatically join the autojoin channels configured for a specific network, if any.
|
||||
|
||||
However, plugins can persistently join services bots to specific channels by calling `myservice.join(irc, channels)`. To manually add/remove channels from the service's autojoin list, modify the `myservice.extra_channels` set.
|
||||
|
||||
## Removing services on unload
|
||||
|
||||
All plugins using the services API **MUST** have a `die()` function that unregisters all services that they've created. A simple example would be in the `games` plugin:
|
||||
All plugins using the services API should have a `die()` function that unregisters all service bots that they've created. A simple example would be in the `games` plugin:
|
||||
|
||||
```python
|
||||
def die(irc):
|
||||
utils.unregisterService('games')
|
||||
def die(irc=None):
|
||||
utils.unregister_service('games')
|
||||
```
|
||||
|
||||
Should your service bot define any persistent channels, you will also want to clear them on unload via `myservice.clear_persistent_channels(None, 'your-namespace', ...)`
|
||||
|
||||
## Persistent channel joining
|
||||
|
||||
Since PyLink 2.0-alpha3, persistent channels are handled in a plugin specific manner. For any service bot on any network, a plugin can register a list of channels that the bot should join persistently (i.e. through kicks and kills). Instead of removing channels from service bots directly, plugins then "request" parts through the services API, which succeed only if no other plugins still mark the channel as persistent. This rework fixes [edge-case desyncs](https://github.com/jlu5/PyLink/issues/265) in earlier versions when multiple plugins change a service bot's channel list, and replaces the `ServiceBot.extra_channels` attribute (which is no longer supported).
|
||||
|
||||
Note: Autojoin channels defined in a network's server block are always treated as persistent on that network.
|
||||
|
||||
### Channel management methods
|
||||
|
||||
Channels, persistent and otherwise are managed through the following functions implemented by `ServiceBot`. While namespaces for channel registrations can technically be any string, it is preferable to keep them close (or equal) to your plugin name.
|
||||
|
||||
- `myservice.add_persistent_channel(irc, namespace, channel, try_join=True)`: Adds a persistent channel to the service bot on the given network and namespace.
|
||||
- `try_join` determines whether the service bot should try to join the channel immediately; you can disable this if you prefer to manage joins by yourself.
|
||||
- `myservice.remove_persistent_channel(irc, namespace, channel, try_part=True, part_reason='')`: Removes a persistent channel from the service bot on the given network and namespace.
|
||||
- `try_part` determines whether a part should be requested from the channel immediately. (`part_reason` is ignored if this is False)
|
||||
- `myservice.get_persistent_channels(irc, namespace=None)`: Returns a set of persistent channels for the IRC network, optionally filtering by namespace is one is given. The channels defined in the network's server block are also included because they are always treated as persistent.
|
||||
- `myservice.clear_persistent_channels(irc, namespace, try_part=True, part_reason='')`: Clears all the persistent channels defined by a namespace. `irc` can also be `None` to clear persistent channels for all networks in this namespace.
|
||||
- `myservice.join(irc, channels, ignore_empty=True)`: Joins the given service bot to the given channel(s). `channels` can be an iterable of channel names or the name of a single channel (type `str`).
|
||||
- The `ignore_empty` option sets whether we should skip joining empty channels and join them later when we see someone else join (if it is marked persistent). This option is automatically *disabled* on networks where we cannot monitor channels we're not in (e.g. on Clientbot).
|
||||
- Before 2.0-alpha3, this function implicitly marks channels it receives to be persistent - this is no longer the case!
|
||||
- `myservice.part(irc, channels, reason='')`: Requests a part from the given channel(s) - that is, leave only if no other plugins still register it as a persistent channel.
|
||||
- `channels` can be an iterable of channel names or the name of a single channel (type `str`).
|
||||
|
||||
### A note on dynamicness
|
||||
|
||||
As of PyLink 2.0-alpha3, persistent channels are also "dynamic" in the sense that PyLink service bots will part channels marked persistent when they become empty, and rejoin when they are recreated. This feature will hopefully be more fine-tunable in future releases.
|
||||
|
||||
Dynamic channels are disabled on networks with the [`visible-state-only` protocol capability](pmodule-spec.md#pylink-protocol-capabilities) (e.g. Clientbot), where it is impossible to monitor the state of channels the bot is not in.
|
||||
|
||||
## Service bots and commands
|
||||
|
||||
Commands for service bots and commands for the main PyLink bot have two main differences.
|
||||
@ -59,4 +85,17 @@ Commands for service bots and commands for the main PyLink bot have two main dif
|
||||
|
||||
### Featured commands
|
||||
|
||||
Commands for service bots can also be marked as *featured*, which shows it with its command arguments in the default `LIST` command. To mark a command as featured, use `myservice.add_cmd(cmdfunc, 'cmdname', featured=True)`.
|
||||
Commands for service bots can also be marked as *featured*, which shows it with its command arguments in the default `LIST` command. To mark a command as featured, enable the `featured` option when binding it: e.g. `myservice.add_cmd(cmdfunc, featured=True)`.
|
||||
|
||||
### Command aliases
|
||||
|
||||
Since PyLink 2.0-alpha1, `ServiceBot.add_cmd(...)` and `utils.add_cmd(...)` support assigning aliases to a command by defining the `aliases` argument. Command aliases do not show in `LIST`, allowing command listings to be much cleaner. Instead, they are only mentioned when `HELP` is called on an alias command name or its parent.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
myservice.add_cmd(functwo, aliases=('abc',))
|
||||
myservice.add_cmd(somefunc, aliases=('command1', 'command2'))
|
||||
```
|
||||
|
||||
Note: use `(variable,)` when defining one length tuples to [prevent them from being parsed as a single string](https://wiki.python.org/moin/TupleSyntax).
|
||||
|
@ -1,52 +0,0 @@
|
||||
User Mode / IRCd,RFC1459,InspIRCd,charybdis,Elemental-IRCd,UnrealIRCd,IRCd-Hybrid,Nefarious IRCu,ircd-ratbox,snircd,IRCu,ngIRCd
|
||||
admin,,,a,a,,a,a,a,,,
|
||||
away,,,,,,,,,,,a
|
||||
bot,,B,,B,B,,B,,,,B
|
||||
callerid,,g,g,g,,g,,g,,,
|
||||
cloak,,x,x,x,x,x,x,,x,x,x
|
||||
cloak_fakehost,,,,,,,f,,,,
|
||||
cloak_hashedhost,,,,,,,C,,,,
|
||||
cloak_hashedip,,,,,,,c,,,,
|
||||
cloak_sethost,,,,,,,h,,h,,
|
||||
deaf,,d,D,D,d,D,d,D,d,d,b
|
||||
deaf_commonchan,,c,,,,G,q,,,,C
|
||||
debug,,,,,,d,,,,,
|
||||
filter,,,,,G,,,,,,
|
||||
floodexempt,,,,,,,,,,,f
|
||||
helpop,,h,,,,,,,,,
|
||||
hidechans,,I,,I,p,p,n,,n,,I
|
||||
hideidle,,,,,I,q,I,,I,,
|
||||
hideoper,,H,,,H,H,H,,,,
|
||||
invisible,i,i,i,i,i,i,i,i,i,i,i
|
||||
locops,,,l,l,,l,O,l,O,O,
|
||||
noctcp,,,,C,T,,,,,,
|
||||
noforward,,,Q,Q,,,L,,,,
|
||||
noinvite,,,,V,,,,,,,
|
||||
oper,o,o,o,o,o,o,o,o,o,o,o
|
||||
operwall,,,z,z,,,,z,,,
|
||||
override,,,p,p,,,X,,X,,
|
||||
privdeaf,,,,,,,D,,,,
|
||||
protected,,,,,q,,,,,,
|
||||
regdeaf,,R,R,R,R,R,R,,R,,
|
||||
registered,,r,,,r,r,r,,r,r,R
|
||||
restricted,,,,,,,,,,,r
|
||||
servprotect,,k,S,S,S,,k,S,k,k,q
|
||||
showwhois,,W,,,W,,W,,,,
|
||||
sno_admin_requests,,,,,,y,,y,,,
|
||||
sno_badclientconnections,,,,,,u,,u,,,
|
||||
sno_botfloods,,,,,,b,,b,,,
|
||||
sno_clientconnections,,,,,,c,,c,,,c
|
||||
sno_debug,,,,,,,g,d,g,g,
|
||||
sno_extclientconnections,,,,,,,,C,,,
|
||||
sno_fullauthblock,,,,,,f,,f,,,
|
||||
sno_nickchange,,,,,,n,,,,,
|
||||
sno_rejectedclients,,,,,,j,,r,,,
|
||||
sno_remoteclientconnections,,,,,,F,,,,,
|
||||
sno_server_connects,,,,,,e,,x,,,
|
||||
sno_skill,,,,,,k,,k,,,
|
||||
snomask,s,s,s,s,s,s,s,s,,,s
|
||||
ssl,,,,,z,S,z,,,,
|
||||
stripcolor,,S,,,,,,,,,
|
||||
vhost,,,,,t,,,,,,
|
||||
wallops,w,w,w,w,w,w,w,w,,,w
|
||||
webirc,,,,,,W,,,,,
|
|
@ -3,45 +3,65 @@ This version of the document targets the current stable branch of PyLink, and ma
|
||||
|
||||
# Writing plugins for PyLink
|
||||
|
||||
PyLink plugins are modules that extend its functionality by giving it something to do. Without any plugins loaded, PyLink can only sit on a server and do absolutely nothing.
|
||||
Most features in PyLink (Relay, Automode, etc.) are implemented as plugins, which can be mix-and-matched on any particular instance. Without any plugins loaded, PyLink can connect to servers but won't accomplish anything useful.
|
||||
|
||||
This guide, along with the sample plugins [`plugins/example.py`](../../plugins/example.py), and [`plugins/example_service.py`](../../plugins/example_service.py) aim to show the basics of writing plugins for PyLink.
|
||||
This guide, along with the sample plugin [`example.py`](../../plugins/example.py) aim to show the basics of writing plugins for PyLink.
|
||||
|
||||
## Receiving data from IRC
|
||||
|
||||
Plugins have two ways of communicating with IRC: hooks, and commands sent in PM to the main PyLink client. A simple plugin can use one, or any mixture of these.
|
||||
Plugins have two ways of communicating with IRC: hooks, and commands directed towards service clients. Any plugin can use one or a combination of these.
|
||||
|
||||
### Hooks
|
||||
### Hook events
|
||||
|
||||
Hooks are probably the most versatile form of communication. The data in each hook payload is formatted as a Python `dict`, with different data keys depending on the command.
|
||||
For example, a `PRIVMSG` payload would give you the fields `target` and `text`, while a `PART` payload would only give you `channels` and `reason` fields.
|
||||
PyLink's hooks system is designed as a protocol-independent method for protocol modules to communicate with plugins (and to a lesser extend, for plugins to communicate with each other). Hook events are the most versatile form of communication available, with each individual event generally corresponding to a specific chat or server event (e.g. `PRIVMSG`, `JOIN`, `KICK`). Each hook payload includes 4 parts:
|
||||
|
||||
There are many hook types available (one for each supported IRC command), and you can read more about them in the [PyLink hooks reference](hooks-reference.md).
|
||||
1) The corresponding network object (IRC object) where the event took place (**type**: a subclass of `pylinkirc.classes.PyLinkNetworkCore`)
|
||||
2) The numeric ID† of the sender (**type**: `str`)
|
||||
3) An identifier for the command name, which may or may not be the same as the name of the hook depending on context (**type**: `str`)
|
||||
4) A freeform `dict` of arguments, where data keys vary by command - see the [PyLink hooks reference](hooks-reference.md) for what's available where.
|
||||
|
||||
Plugins can bind to hooks using the `utils.add_hook()` function like so: `utils.add_hook(function_name, 'PRIVMSG')`, where `function_name` is your function definition, and `PRIVMSG` is whatever hook name you want to bind to. Once set up, `function_name` will be called whenever the protocol module receives a `PRIVMSG` command.
|
||||
Functions intended to be hook handlers therefore take in 4 arguments corresponding to the ones listed above: `irc`, `source`, `command`, and `args`.
|
||||
|
||||
Each hook-bound function takes 4 arguments: `irc, source, command, args`.
|
||||
- **irc**: The IRC object where the hook was called. Plugins are globally loaded, so there will be one of these per network.
|
||||
- **source**: The numeric of the sender. This will usually be a UID (for users) or a SID (for server).
|
||||
- **command**: The true command name where the hook originates. This may or may not be the same as the name of the hook, depending on context.
|
||||
- **args**: The hook data (a `dict`) associated with the command. Again, the available data keys differ by hook name
|
||||
(see the [hooks reference](hooks-reference.md) for a list of which can be used).
|
||||
#### Return codes for hook handlers
|
||||
|
||||
Hook functions do not return anything, and can raise exceptions to be caught by the core.
|
||||
As of PyLink 2.0-alpha3, the return value of hook handlers are used to determine how the original event will be passed on to further handlers (that is, those created by plugins loaded later, or hook handlers registered with a lower priority).
|
||||
|
||||
The following return values are supported so far:
|
||||
|
||||
- `None` or `True`: passthrough the event unchanged to further handlers (the default behavior)
|
||||
- `False`: block the event from reaching other handlers
|
||||
|
||||
Hook handlers may raise exceptions without blocking the event from reaching further handlers; these are caught by PyLink and logged appropriately.
|
||||
|
||||
### Hook priorities
|
||||
The `priority` option in `utils.add_hook()` allows setting a hook handler's priority when binding it. When multiple modules bind to one hook command, handlers are called in order of decreasing priority (i.e. highest first).
|
||||
|
||||
There is no standard for hook priorities as of 2.0; instead they are declared as necessary. Some priority values used in 2.0 are shown here for reference:
|
||||
|
||||
| Module | Commands | Priority | Description |
|
||||
|-------------------|-----------------|----------|-------------|
|
||||
| `service_support` | ENDBURST | 500 | This sets up services bots before plugins run so that they can assume their presence when initializing. |
|
||||
| `antispam` | PRIVMSG, NOTICE | 990-1000 | This allows `antispam` to filter away spam before it can reach other handlers. |
|
||||
| `relay` | PRIVMSG, NOTICE | 200 | Fixes https://github.com/jlu5/PyLink/issues/123. Essentially, this lets Relay forward messages calling commands before letting the command handler work (and then relaying its responses). |
|
||||
| `ctcp` | PRIVMSG | 200 | The `ctcp` plugin processes CTCPs and blocks them from reaching the services command handler, preventing extraneous "unknown command" errors. |
|
||||
|
||||
### Bot commands
|
||||
|
||||
For plugins that interact with regular users, you can also write commands for the PyLink bot, or [create service bots with their own command set](services-api.md). This section only details the former:
|
||||
Plugins can also define service bot commands, either for the main PyLink service bot or for one created by the plugin itself. This section only details the former - see the [Services API Guide](services-api.md) for details on the latter.
|
||||
|
||||
Plugins can add commands by including something like `utils.add_cmd(testcommand, "hello")`. Here, `testcommand` is the name of your function, and `hello` is the (optional) name of the command. If no command name is specified, it will use the same name as the function.
|
||||
Now, your command function will be called whenever someone PMs the PyLink client with the command (e.g. `/msg PyLink hello`, case-insensitive).
|
||||
Commands are registered by calling `utils.add_cmd()` with one or two arguments. Ex)
|
||||
- `utils.add_cmd(testcommand, "hello")` registers a function named `testcommand` as the command handler for `hello` (i.e. `/msg PyLink hello`)
|
||||
- `utils.add_cmd(testcommand)` registers a function named `testcommand` as the command handler for `testcommand`.
|
||||
|
||||
Each command function takes 3 arguments: `irc, source, args`.
|
||||
- **irc**: The IRC object where the command was called.
|
||||
- **source**: The numeric of the sender. This will usually be a UID (for users) or a SID (for server).
|
||||
- **args**: A `list` of space-separated command arguments (excluding the command name) that the command was called with. For example, `/msg PyLink hello world 1234` would give an `args` list of `['world', '1234']`
|
||||
`utils.add_cmd(...)` also takes some keyword arguments, described in the [services API guide](services-api.md#service-bots-and-commands) (replace `myservice.add_cmd` with `utils.add_cmd`). Decorator syntax (`@utils.add_cmd`) can also be used for the second example above.
|
||||
|
||||
As of PyLink 1.2, there are two ways for a plugin to parse arguments: as a raw list of strings, or with `utils.IRCParser` (an [argparse](https://docs.python.org/3/library/argparse.html) wrapper). More information on using `utils.IRCParser()` can be found in the page ["using IRCParser"](using-ircparser.md).
|
||||
|
||||
Each command handler function takes 3 arguments: `irc, source, args`.
|
||||
- **irc**: The network object where the command was called.
|
||||
- **source**: The numeric ID (or pseudo-ID) of the sender.
|
||||
- **args**: A `list` of command arguments (not including the command name) that the command was called with. For example, `/msg PyLink hello world 1234` would give an `args` list of `['world', '1234']`
|
||||
|
||||
As of PyLink 1.2, there are two ways for a plugin to parse arguments: as a raw list of strings, or with `utils.IRCParser` (an [argparse](https://docs.python.org/3/library/argparse.html) wrapper). `IRCParser()` is documented in the ["using IRCParser"](using-ircparser.md) page.
|
||||
|
||||
Command handlers do not return anything and can raise exceptions, which are caught by the core and automatically return an error message.
|
||||
|
||||
@ -49,18 +69,19 @@ Command handlers do not return anything and can raise exceptions, which are caug
|
||||
|
||||
Plugins receive data from the underlying protocol module, and communicate back using outgoing [command functions](pmodule-spec.md) implemented by the protocol module. They should *never* send raw data directly back to IRC, because that wouldn't be portable across different IRCds.
|
||||
|
||||
These functions are usually called in this fashion: `irc.proto.command(arg1, arg2, ...)`. For example, the command `irc.proto.join('10XAAAAAB', '#bots')` would join a PyLink client with UID `10XAAAAAB` to channel `#bots`.
|
||||
These functions are called in the form: `irc.command(arg1, arg2, ...)`. For example, the command `irc.join('10XAAAAAB', '#bots')` would join a PyLink client with UID `10XAAAAAB` to the channel `#bots`.
|
||||
|
||||
For sending messages (e.g. replies to commands), simpler forms of:
|
||||
|
||||
- `irc.reply(text, notice=False, source=None)`
|
||||
- `irc.error(text, notice=False, source=None)`
|
||||
- and `irc.msg(targetUID, text, notice=False, source=None)`
|
||||
|
||||
are preferred.
|
||||
|
||||
`irc.reply()` is a special form of `irc.msg` in that it automatically finds the target to reply to. If the command was called in a channel using fantasy, it will send the reply in that channel. Otherwise, the reply will be sent in a PM to the caller.
|
||||
`irc.reply()` is a frontend to `irc.msg()` which automatically finds the right target to reply to: that is, the channel for fantasy commands and the caller for PMs. `irc.error()` is in turn a wrapper around `irc.reply()` which prefixes the given text with `Error: `.
|
||||
|
||||
The sender UID for both can be set using the `source` argument, and defaults to the main PyLink client.
|
||||
The sender UID for all of these can be set using the `source` argument, and defaults to the main PyLink client.
|
||||
|
||||
## Access checking for commands
|
||||
|
||||
@ -70,5 +91,23 @@ See the [Permissions API documentation](permissions-api.md) on how to restrict c
|
||||
|
||||
The following functions can also be defined in the body of a plugin to hook onto plugin loading / unloading.
|
||||
|
||||
`main(irc=None)`: Called on plugin load. `irc` is only defined when the plugin is being reloaded from a network: otherwise, it means that PyLink has just been started.
|
||||
`die(irc=None)`: Called on plugin unload or daemon shutdown. `irc` is only defined when the shutdown or unload was called from an IRC network.
|
||||
- `main(irc=None)`: Called on plugin load. `irc` is only defined when the plugin is being reloaded from a network: otherwise, it means that PyLink has just been started.
|
||||
- `die(irc=None)`: Called on plugin unload or daemon shutdown. `irc` is only defined when the shutdown or unload was called from an IRC network.
|
||||
|
||||
## Other tips
|
||||
|
||||
### Logging
|
||||
|
||||
Use PyLink's [global logger](https://docs.python.org/3/library/logging.html) (`from pylinkirc.log import log`) instead of print statements.
|
||||
|
||||
### Some useful attributes
|
||||
|
||||
- **`world.networkobjects`** provides a dict mapping network names (case sensitive) to their corresponding network objects/protocol module instances.
|
||||
- **`irc.connected`** is a [`threading.Event()`](https://docs.python.org/3/library/threading.html#event-objects) object that is set when a network finishes bursting.
|
||||
- `world.started` is a [`threading.Event()`](https://docs.python.org/3/library/threading.html#event-objects) object that is set when all networks have been initialized.
|
||||
- `world.plugins` provides a dict mapping loaded plugins' names (case sensitive) to their module objects. This is the preferred way to call another plugins's methods if need be (while of course, forcing you to check whether the other plugin is already loaded).
|
||||
- `world.services` provides a dict mapping service bot names to their `utils.ServiceBot` instances.
|
||||
|
||||
### Useful modules
|
||||
|
||||
`classes.py`, `utils.py` and `structures.py` all provide a ton of public methods which aren't documented here for conciseness. In `classes.py`, `PyLinkNetworkCore` and `PyLinkNetworkCoreUtils` (which all protocol modules inherit from) are where many utility and state-checking functions sit.
|
||||
|
421
example-conf.yml
421
example-conf.yml
@ -42,7 +42,8 @@ pylink:
|
||||
|
||||
# Determines whether spawning additional services for bots (e.g. Automode, Games) should be
|
||||
# enabled. This defaults to True, unless a network's protocol module doesn't support spawning
|
||||
# extra service bots.
|
||||
# extra service bots. If this is set to false, the other plugins' commands and channels will
|
||||
# be merged into that of the main PyLink service bot.
|
||||
#spawn_services: true
|
||||
|
||||
# Defines extra directories to look up plugins and protocol modules in.
|
||||
@ -108,7 +109,7 @@ permissions:
|
||||
|
||||
# Replace ABC123 with your PyLink account name (configured above)
|
||||
# in order to give yourself admin access.
|
||||
"$pylinkacc:ABC123":
|
||||
"ABC123":
|
||||
- "*"
|
||||
|
||||
servers:
|
||||
@ -151,18 +152,47 @@ servers:
|
||||
# There must be at least one # in this entry.
|
||||
sidrange: "8##"
|
||||
|
||||
# Autojoin channels. The "channels" option affects all service bots, but you can also
|
||||
# configure channels per service using keys in the name of "<servicename>_channels"
|
||||
# Comment out or remove these keys if you don't want service bots# to join any channels by
|
||||
# default.
|
||||
#channels: ["#pylink"]
|
||||
#pylink_channels: ["#services"]
|
||||
#automode_channels: ["#chat"]
|
||||
|
||||
# Sets the protocol module to use for this network - see the README for a
|
||||
# list of supported IRCds.
|
||||
protocol: "inspircd"
|
||||
|
||||
# Sets the max nick length for the network. It is important that this is
|
||||
# set correctly, or PyLink might introduce a nick that is too long and
|
||||
# cause netsplits! This defaults to 30 if not set.
|
||||
maxnicklen: 30
|
||||
|
||||
# Toggles SSL for this network - you should seriously consider using TLS in all your links
|
||||
# for optimal security. Defaults to False if not specified.
|
||||
ssl: true
|
||||
|
||||
# Optional SSL cert/key to pass to the uplink server.
|
||||
#ssl_certfile: pylink-cert.pem
|
||||
#ssl_keyfile: pylink-key.pem
|
||||
|
||||
# New in 2.0: Determines whether the target server's TLS certificate hostnames should be
|
||||
# checked against the hostname we're set to connect to. This defaults to true for Clientbot
|
||||
# networks and others linked to via a hostname. It depends on ssl_accept_invalid_certs being
|
||||
# *disabled* to take effect.
|
||||
#ssl_validate_hostname: true
|
||||
|
||||
# New in 2.0: When enabled, this disables TLS certificate validation on the target network.
|
||||
# This defaults to false (bad certs are rejected) on Clientbot and true for server protocols
|
||||
# (where bad certs are accepted). This disables the ssl_validate_hostname option,
|
||||
# effectively forcing it to be false.
|
||||
#ssl_accept_invalid_certs: false
|
||||
|
||||
# Optionally, you can set this option to verify the SSL certificate fingerprint of your
|
||||
# uplink. This check works regardless of whether ssl_validate_hostname and
|
||||
# ssl_accept_invalid_certs are enabled.
|
||||
#ssl_fingerprint: "e0fee1adf795c84eec4735f039503eb18d9c35cc"
|
||||
|
||||
# This sets the hash type for the fingerprint (md5, sha1, sha256, etc.)
|
||||
# Valid values include md5 and sha1-sha512, though others may be
|
||||
# supported depending on your system: see
|
||||
# https://docs.python.org/3/library/hashlib.html
|
||||
# This setting defaults to sha256.
|
||||
#ssl_fingerprint_type: sha256
|
||||
|
||||
# Sets autoconnect delay - comment this out or set the value below 1 to
|
||||
# disable autoconnect entirely.
|
||||
autoconnect: 10
|
||||
@ -174,37 +204,13 @@ servers:
|
||||
# Defines what the maximum autoconnect time will be (defaults to 1800 secs).
|
||||
#autoconnect_max: 1800
|
||||
|
||||
# Sets the ping frequency in seconds (i.e. how long we should wait between
|
||||
# sending pings to our uplink). When more than two consecutive pings are missed,
|
||||
# PyLink will disconnect with a ping timeout. This defaults to 90 if not set.
|
||||
#pingfreq: 90
|
||||
|
||||
# Sets the max nick length for the network. It is important that this is
|
||||
# set correctly, or PyLink might introduce a nick that is too long and
|
||||
# cause netsplits! This defaults to 30 if not set.
|
||||
maxnicklen: 30
|
||||
|
||||
# Determines the maximum size of the network's outgoing data queue (sendq), in message lines.
|
||||
# This defaults to 4096 if not set.
|
||||
#maxsendq: 4096
|
||||
|
||||
# Toggles SSL for this network. Defaults to False if not specified.
|
||||
#ssl: true
|
||||
|
||||
# Optional SSL cert/key to pass to the uplink server.
|
||||
#ssl_certfile: pylink-cert.pem
|
||||
#ssl_keyfile: pylink-key.pem
|
||||
|
||||
# Optionally, you can set this option to verify the SSL certificate
|
||||
# fingerprint of your uplink.
|
||||
#ssl_fingerprint: "e0fee1adf795c84eec4735f039503eb18d9c35cc"
|
||||
|
||||
# This sets the hash type for the fingerprint (md5, sha1, sha256, etc.)
|
||||
# Valid values include md5 and sha1-sha512, though others may be
|
||||
# supported depending on your system: see
|
||||
# https://docs.python.org/3/library/hashlib.html
|
||||
# This setting defaults to sha256.
|
||||
#ssl_fingerprint_type: sha256
|
||||
# Autojoin channels. The "channels" option affects all service bots, but you can also
|
||||
# configure channels per service using keys in the name of "<servicename>_channels"
|
||||
# Comment out or remove these keys if you don't want service bots# to join any channels by
|
||||
# default.
|
||||
#channels: ["#pylink"]
|
||||
#pylink_channels: ["#services"]
|
||||
#automode_channels: ["#chat"]
|
||||
|
||||
# Encoding: allows you to override the network's encoding. This can be useful for networks
|
||||
# using m_nationalchars or something similar. Encoding defaults to utf-8 if not set, and
|
||||
@ -215,39 +221,54 @@ servers:
|
||||
# This setting is EXPERIMENTAL as of PyLink 1.2.x.
|
||||
#encoding: utf-8
|
||||
|
||||
# If enabled, this opts this network out of relay IP sharing: this network
|
||||
# will not have its users' IPs sent across the relay, and it will not see any
|
||||
# IPs of other networks' users.
|
||||
#relay_no_ips: true
|
||||
# Sets the ping frequency in seconds (i.e. how long we should wait between
|
||||
# sending pings to our uplink). When more than two consecutive pings are missed,
|
||||
# PyLink will disconnect with a ping timeout. This defaults to 90 if not set.
|
||||
#pingfreq: 90
|
||||
|
||||
# If relay nick tagging is disabled, this option specifies a list of nick globs to always
|
||||
# tag when introducing remote users *onto* this network.
|
||||
#relay_forcetag_nicks: ["someuser", "Guest*"]
|
||||
|
||||
# Determines whether relay will tag nicks on this network. This overrides the relay::tag_nicks
|
||||
# option on a per network-basis.
|
||||
#relay_tag_nicks: true
|
||||
|
||||
# Sets the suffix that relay subservers on this network should use.
|
||||
# If not specified per network, this falls back to the value at
|
||||
# relay::server_suffix, or "relay" if that is also not set.
|
||||
#relay_server_suffix: "relay.yournet.net"
|
||||
|
||||
# Determines whether relay will tag nicks on this network. This overrides the relay::tag_nicks
|
||||
# option on a per network-basis.
|
||||
#relay_tag_nicks: true
|
||||
|
||||
# Determines the maximum size of the network's outgoing data queue (sendq), in message lines.
|
||||
# This defaults to 4096 if not set.
|
||||
#maxsendq: 4096
|
||||
|
||||
# Defines a list of "U-lined" servers that should be given special treatment when overriding
|
||||
# modes. Relay uses this as a list of servers to IGNORE some mode changes from on a claimed
|
||||
# channel (versus bouncing the mode back, which may be floody).
|
||||
#ulines: ["services.example.conf"]
|
||||
|
||||
# InspIRCd specific option: determines whether we should display WHOIS extensions by overriding
|
||||
# InspIRCd's default WHOIS formatting. This defaults to true for consistency with PyLink 1.x.
|
||||
#force_whois_extensions: true
|
||||
|
||||
unrealnet:
|
||||
ip: ::1
|
||||
port: 8067
|
||||
|
||||
# Determines whether IPv6 should be used for this connection. Should the ip:
|
||||
# above be a hostname instead of an IP, this will also affect whether A records
|
||||
# (no IPv6) or AAAA records (IPv6) will be used in resolving it.
|
||||
ipv6: yes
|
||||
# (IPv4) or AAAA records (IPv6) will be used in resolving it.
|
||||
# As of PyLink 2.0-beta1, you can leave this unset for direct connections to IP addresses;
|
||||
# the address type will be automatically detected.
|
||||
#ipv6: yes
|
||||
|
||||
# Received and sent passwords. For passwordless links using SSL fingerprints, simply set
|
||||
# these two fields to "*" and enable SSL with a cert and key file.
|
||||
recvpass: "coffee"
|
||||
sendpass: "tea"
|
||||
|
||||
#ssl: true
|
||||
ssl: true
|
||||
#ssl_certfile: mycert.pem
|
||||
#ssl_keyfile: mycert.pem
|
||||
hostname: "pylink.example.com"
|
||||
@ -274,9 +295,18 @@ servers:
|
||||
ip: somenet.ddns.local
|
||||
port: 45454
|
||||
|
||||
# When the IP field is set to a hostname, this option determines whether IPv4 or IPv6 should
|
||||
# be used for the connection.
|
||||
# When the IP field is set to a hostname, the "ipv6" option determines whether IPv4 or IPv6
|
||||
# addresses should be used when resolving it. You can leave this field blank and use an
|
||||
# explicit bindhost instead, which will let the the address type be automatically detected.
|
||||
#ipv6: false
|
||||
#bindhost: 1111:2222:3333:4444
|
||||
|
||||
# Note: if you are actually using dynamic DNS for an IRC link, consider enabling
|
||||
# TLS/SSL certificate checking (new in 2.0). These checks are disabled by default for
|
||||
# server links because self-signed certificates are still quite common.
|
||||
ssl: true
|
||||
#ssl_accept_invalid_certs: false
|
||||
#ssl_validate_hostname: true
|
||||
|
||||
recvpass: "PASS123"
|
||||
sendpass: "PASS321"
|
||||
@ -317,7 +347,10 @@ servers:
|
||||
|
||||
# With this key set to 'generic', neither of these host changing features are enabled
|
||||
# and a baseline RFC1459 mode set is used. This configuration is not officially supported.
|
||||
p10_ircd: nefarious
|
||||
|
||||
# (This option was previously named "p10_ircd" in PyLink 1.2, and that option name is
|
||||
# deprecated as of 2.0).
|
||||
ircd: nefarious
|
||||
|
||||
# Determines whether account-based cloaks should be used (someone.users.yournet.org
|
||||
# format). This setting MUST match your IRCd configuration.
|
||||
@ -351,6 +384,7 @@ servers:
|
||||
ts6net:
|
||||
ip: 1.2.3.4
|
||||
port: 7000
|
||||
ssl: true
|
||||
recvpass: "abcd"
|
||||
sendpass: "abcd"
|
||||
hostname: "pylink.example.com"
|
||||
@ -361,29 +395,23 @@ servers:
|
||||
protocol: "ts6"
|
||||
autoconnect: 10
|
||||
|
||||
# Note: /'s in nicks are automatically converted to |'s for TS6
|
||||
# networks (charybdis, etc.), since they don't allow "/" in nicks.
|
||||
#separator: "|"
|
||||
|
||||
### The following options are specific to TS6 servers:
|
||||
# Toggles owner (+y), admin (+a), and halfop (+h) support for
|
||||
# shadowircd/elemental-ircd.
|
||||
# These default to off for the best compatibility.
|
||||
# shadowircd/elemental-ircd/chatircd. These default to off for the best compatibility.
|
||||
#use_owner: false
|
||||
#use_admin: false
|
||||
#use_halfop: false
|
||||
|
||||
# Toggles support of shadowircd/elemental-ircd specific channel modes:
|
||||
# +T (no notice), +u (hidden ban list), +E (no kicks), +J (blocks kickrejoin),
|
||||
# +K (no repeat messages), +d (no nick changes), and user modes:
|
||||
# +B (bot), +C (blocks CTCP), +D (deaf), +V (no invites), +I (hides WHOIS channel list)
|
||||
# This defaults to false.
|
||||
#use_elemental_modes: false
|
||||
# Sets the IRCd (channel/user mode set) to target - currently supported values include
|
||||
# 'chatircd', 'charybdis', and 'elemental' (elemental-ircd). This option defaults to
|
||||
# 'charybdis' if not set, and replaces the "use_elemental_modes" option from PyLink 1.2
|
||||
# and earlier.
|
||||
#ircd: charybdis
|
||||
|
||||
# Sample Clientbot configuration, if you want to connect PyLink as a bot to relay somewhere
|
||||
# (or do other bot things).
|
||||
magicnet:
|
||||
ip: 1.2.3.4
|
||||
ip: irc.somenet.local
|
||||
port: 6697
|
||||
|
||||
# Optional server password.
|
||||
@ -397,14 +425,33 @@ servers:
|
||||
#pylink_nick: pybot
|
||||
#pylink_ident: pybot
|
||||
|
||||
# SSL options. Certfile and keyfile are optional, but can be used for CertFP/SASL external
|
||||
# if supported.
|
||||
# You can also define alternate fallback nicks on Clientbot. These will be used in order
|
||||
# if successive nicks are unavailable, falling back to the default nick plus an increasing
|
||||
# number of underscores.
|
||||
#pylink_altnicks: ["pybot`", "pybot-"]
|
||||
|
||||
# TLS/SSL options. Certfile and keyfile are optional, but can be used for CertFP/SASL external
|
||||
# where supported.
|
||||
ssl: true
|
||||
#ssl_certfile: mycert.pem
|
||||
#ssl_keyfile: mycert.pem
|
||||
|
||||
# Autoconnect works as usual.
|
||||
# New in 2.0: Determines whether the target server's TLS certificate hostnames should be
|
||||
# checked against the hostname we're set to connect to. This defaults to true for Clientbot
|
||||
# networks and others linked to via a hostname. It depends on ssl_accept_invalid_certs being
|
||||
# *disabled* to take effect.
|
||||
#ssl_validate_hostname: true
|
||||
|
||||
# New in 2.0: When enabled, this disables TLS certificate validation on the target network.
|
||||
# This defaults to false (bad certs are rejected) on Clientbot and true for server protocols
|
||||
# (where bad certs are accepted). This disables the ssl_validate_hostname option,
|
||||
# effectively forcing it to be false.
|
||||
#ssl_accept_invalid_certs: false
|
||||
|
||||
# Autoconnect options work the same as on a regular network.
|
||||
autoconnect: 30
|
||||
#autoconnect_multiplier: 1.5
|
||||
#autoconnect_max: 1800
|
||||
|
||||
# Message throttling: when set to a non-zero value, only one message will be sent every X
|
||||
# seconds. If your bot is constantly running into Excess Flood errors, raising this to
|
||||
@ -436,8 +483,11 @@ servers:
|
||||
# This defaults to false.
|
||||
#sasl_reauth: true
|
||||
|
||||
# Clientbot also supports auto perform, using raw IRC messages.
|
||||
# Raw IRC messages to send on connect. As of 2.0-alpha2, expansions such as $nick, $ident,
|
||||
# and $host are also supported via Python template strings:
|
||||
# (https://docs.python.org/3/library/string.html#template-strings)
|
||||
#autoperform:
|
||||
# - "MODE $nick +B"
|
||||
# - "NOTICE somebody :hello, i've connected"
|
||||
|
||||
# Determines whether oper statuses should be tracked on this Clientbot network. This
|
||||
@ -446,6 +496,13 @@ servers:
|
||||
# This defaults to false if not specified.
|
||||
#track_oper_statuses: false
|
||||
|
||||
# Determines whether the bot should enumerate ban/banexception/invex modes when joining channels.
|
||||
# This is required for relay mode sync to work properly, because the bot will otherwise refuse to
|
||||
# relay unbans (Clientbot only removes modes that it knows are set).
|
||||
# This defaults to False because it causes extra MODE messages to be sent on connect, which can
|
||||
# drastically slow down startup if the bot joins a lot of channels.
|
||||
#fetch_ban_lists: true
|
||||
|
||||
# Plugins to load (omit the .py extension)
|
||||
plugins:
|
||||
# Commands plugin: Provides simple commands to check login status, show info on users and
|
||||
@ -484,6 +541,11 @@ plugins:
|
||||
# configured correctly below.
|
||||
#- changehost
|
||||
|
||||
# Antispam plugin: catches and punishes spammers locally and across relays as necessary.
|
||||
# You *will need* to configure it via the "antispam:" configuration block below.
|
||||
# Note: Antispam is in ALPHA stages as of 2.0-alpha3 and some options may not work yet.
|
||||
#- antispam
|
||||
|
||||
# Servprotect plugin: disconnects from networks if too many kills or nick collisions to
|
||||
# PyLink clients are received.
|
||||
#- servprotect
|
||||
@ -498,6 +560,10 @@ plugins:
|
||||
# Servermaps plugin: displays network /map's from the PyLink server's perspective.
|
||||
#- servermaps
|
||||
|
||||
# Raw plugin: Provides a 'raw' command for sending raw text to IRC.
|
||||
# Not supported outside Clientbot networks!
|
||||
#- raw
|
||||
|
||||
logging:
|
||||
# This configuration block defines targets that PyLink should log commands,
|
||||
# errors, etc., to.
|
||||
@ -615,62 +681,65 @@ relay:
|
||||
# This block defines various options for the Relay plugin. You don't need this
|
||||
# if you aren't using it.
|
||||
|
||||
# Determines whether remote opers will have user mode +H (hideoper) set on
|
||||
# them. This has the benefit of lowering the oper count in /lusers and
|
||||
# /stats (P|p), but only on IRCds that support it. This defaults to true
|
||||
# if not set.
|
||||
hideoper: true
|
||||
# Determines whether all opers should be able to create, link, delink, destroy, and adjust
|
||||
# claim settings & channel descriptions on relay channels. You can disable this if you want to
|
||||
# configure Relay permissions in a more fine grained way, e.g. by granting each network admin
|
||||
# their own PyLink account.
|
||||
# This defaults to True if not set, for consistency with older (< 2.0) PyLink versions.
|
||||
# Changing this setting requires a rehash and reload of the Relay plugin to apply.
|
||||
allow_free_oper_links: true
|
||||
|
||||
# Determines whether real IPs should be sent across the relay. You should
|
||||
# generally have a consensus with your linked networks on whether this should
|
||||
# be turned on. You will see other networks' user IP addresses, and they
|
||||
# will see yours. Individual networks can also opt out of IP sharing
|
||||
# both ways by defining "relay_no_ips: true" in their server block.
|
||||
show_ips: false
|
||||
# Determines whether all relay users' nicks will be tagged with their network, instead of only
|
||||
# when a nick collision occurs. It is recommended that you either leave this on or maintain a
|
||||
# list of nicks in "forcetag_nicks", so that your users don't complain about "nick in use"
|
||||
# errors. This option defaults to True if not specified.
|
||||
tag_nicks: true
|
||||
|
||||
# Determines whether NickServ login info should be shown in the /whois output for
|
||||
# relay users.
|
||||
# Valid options include "all" (show this to everyone), "opers" (show only to
|
||||
# opers), and "none" (disabled). Defaults to none if not specified.
|
||||
whois_show_accounts: all
|
||||
# If tag_nicks is False, this specifies a list of NICK globs that network tags should be added
|
||||
# for anyways (e.g. network services).
|
||||
# There is also a per-network version of this option: see 'relay_forcetag_nicks'.
|
||||
forcetag_nicks:
|
||||
- "*Serv"
|
||||
|
||||
# Determines whether the origin server should be shown in the /whois output for
|
||||
# relay users.
|
||||
# Valid options include "all" (show this to everyone), "opers" (show only to
|
||||
# opers), and "none" (disabled). Defaults to none if not specified.
|
||||
whois_show_server: opers
|
||||
|
||||
# Determines whether the servers disconnecting in a netsplit should be shown when
|
||||
# relaying quits due to a netsplit. Defaults to False.
|
||||
show_netsplits: false
|
||||
# Sets the suffix that relay subservers should use. Defaults to "relay" (as in net1.relay,
|
||||
# net2.relay, etc.) if not specified. This can also be specified per network via the
|
||||
# 'relay_server_suffix' option in a server block.
|
||||
#server_suffix: "relay.yournet.net"
|
||||
|
||||
# Sets the default Relay separator. Defaults to / if not specified. The "separator"
|
||||
# option in server blocks override this if specified.
|
||||
separator: "/"
|
||||
|
||||
# Determines whether all nicks will be tagged by default, instead of only when a
|
||||
# nick collision happens. It is HIGHLY RECOMMENDED that you enable this, unless you're
|
||||
# absolutely sure NO ONE will be using the same nick on 2 or more networks in your
|
||||
# relay.
|
||||
# This defaults to True if not specified. Disabling this option is currently
|
||||
# experimental.
|
||||
tag_nicks: true
|
||||
# This option defines lists of networks to relay user IPs between (instead of
|
||||
# masking them as 0.0.0.0). If a network is in a pool (case-SENSITIVE), their
|
||||
# opers will see the IPs from users in the rest of the pool, and the rest of
|
||||
# the pool will receive IPs from that network too.
|
||||
# You should generally have a consensus among your linked networks as to which
|
||||
# network should be in which pool. A network can be part of one pool, multiple
|
||||
# pools, or none at all.
|
||||
# This option replaces the "relay::show_ips" and network-specific "relay_no_ips"
|
||||
# options, which are DEPRECATED as of 2.0-beta1.
|
||||
#ip_share_pools:
|
||||
# - ["net1", "net2", "net3"]
|
||||
# - ["net1", "meganet"]
|
||||
|
||||
# If tag_nicks is False, this specifies a list of NICK globs that network
|
||||
# tags should be added for anyways (e.g. network services).
|
||||
forcetag_nicks:
|
||||
- "*Serv"
|
||||
# This option defines lists of networks that kills should be relayed between.
|
||||
# If a network is in a pool (case-SENSITIVE), they will be able to kill users
|
||||
# on other networks in the pool, and will also receive kills from other networks
|
||||
# in the pool.
|
||||
# You should generally have a consensus among your linked networks as to which
|
||||
# network should be in which pool.
|
||||
# When a kill is sent to a target not sharing any kill pool with the sender's network,
|
||||
# the kill is relayed as a kick on the target to all shared channels where the sender
|
||||
# has CLAIM access.
|
||||
#kill_share_pools:
|
||||
# - ["net1", "net2", "net3"]
|
||||
|
||||
# This determines whether private messages & notices will be forwarded over Clientbot relay,
|
||||
# and whether the 'rpm' command will be allowed from Clientbot networks. This defaults to
|
||||
# False.
|
||||
allow_clientbot_pms: false
|
||||
|
||||
# Sets the suffix that relay subservers should use. Defaults to "relay" (as in net1.relay,
|
||||
# net2.relay, etc.) if not specified. This can also be set per-network via
|
||||
# servers::<netname>::relay_server_suffix.
|
||||
#server_suffix: "relay.yournet.net"
|
||||
|
||||
# Sets whether Clientbot mode sync will be enabled. Valid options:
|
||||
# "full" - Sync bans, ban/invite exceptions, prefix modes, and all RFC1459-standard modes. The
|
||||
# bot will need op in the Clientbot channel for this to work both ways.
|
||||
@ -685,10 +754,25 @@ relay:
|
||||
#
|
||||
#clientbot_modesync: none
|
||||
|
||||
# Determines whether messages from unknown clients (servers, clients not sharing in a -n channel,
|
||||
# etc.) should be forwarded via the PyLink server. If this is disabled, these messages will be
|
||||
# silently dropped. This defaults to True for consistency with older releases.
|
||||
#accept_weird_senders: false
|
||||
# Determines whether remote opers will have user mode +H (hideoper) set on
|
||||
# them. This has the benefit of lowering the oper count in /lusers and
|
||||
# /stats (P|p), but only on IRCds that support it. This defaults to true
|
||||
# if not set.
|
||||
hideoper: true
|
||||
|
||||
# Determines whether the servers disconnecting in a netsplit should be shown when
|
||||
# relaying quits due to a netsplit. Defaults to False.
|
||||
show_netsplits: false
|
||||
|
||||
# Determines whether LINKACL should use whitelist or blacklist mode by default for newly
|
||||
# created channels. This defaults to false, and can also be specified per network via the
|
||||
# 'relay_linkacl_use_whitelist' option in a server block.
|
||||
#linkacl_use_whitelist: false
|
||||
|
||||
# Determines whether CLAIM should be enabled by default for newly created channels.
|
||||
# This defaults to true, and can also be specified per network via the 'relay_enable_default_claim'
|
||||
# option in a server block.
|
||||
#enable_default_claim: true
|
||||
|
||||
# Optionally defines a message that should be sent to all leaf channels that a network owns, when
|
||||
# it disconnects. This uses a template string as documented at
|
||||
@ -702,6 +786,23 @@ relay:
|
||||
# Network $homenetwork has disconnected: $channel will remain open as the link is
|
||||
# re-established, but new links will be disabled.
|
||||
|
||||
# Determines whether messages from unknown clients (servers, clients not sharing in a -n channel,
|
||||
# etc.) should be forwarded via the PyLink server. If this is disabled, these messages will be
|
||||
# silently dropped. This defaults to True for consistency with older releases.
|
||||
#accept_weird_senders: false
|
||||
|
||||
# Determines whether NickServ login info should be shown in the /whois output for
|
||||
# relay users.
|
||||
# Valid options include "all" (show this to everyone), "opers" (show only to
|
||||
# opers), and "none" (disabled). Defaults to none if not specified.
|
||||
whois_show_accounts: all
|
||||
|
||||
# Determines whether the origin server should be shown in the /whois output for
|
||||
# relay users.
|
||||
# Valid options include "all" (show this to everyone), "opers" (show only to
|
||||
# opers), and "none" (disabled). Defaults to none if not specified.
|
||||
whois_show_server: opers
|
||||
|
||||
#servprotect:
|
||||
# This block configures the servprotect plugin; you don't need this if you aren't using it.
|
||||
|
||||
@ -730,6 +831,8 @@ automode:
|
||||
# Determines whether a separate service bot should be spawned for this plugin. This defaults to
|
||||
# True, unless a network's protocol module doesn't support spawning extra service bots.
|
||||
# This option overrides the global "spawn_services" option defined in "pylink:".
|
||||
# If this is set to False, this plugin's commands and channels will be merged into that of the
|
||||
# main PyLink service bot.
|
||||
#spawn_service: true
|
||||
|
||||
# Defines a fantasy prefix for the Automode bot (requires spawn_services to be set and the
|
||||
@ -775,3 +878,87 @@ stats:
|
||||
# $current_fullnetwork: the full name of the network we're currently broadcasting on
|
||||
# $text: the global text
|
||||
#format: "[$sender@$fullnetwork] $text"
|
||||
|
||||
# Configures a set of channel globs to exempt from announcements. You can also add to
|
||||
# this per network via: servers::<server name>::global_exempt_channels,
|
||||
# the contents of which will be merged into this list.
|
||||
#exempt_channels:
|
||||
# - "#*staff*"
|
||||
|
||||
#antispam:
|
||||
# This block configures the antispam plugin; you don't need this if you aren't using it.
|
||||
# Antispam is automatically enabled in all channels that it's in, and you can configure
|
||||
# its presence (like with all service bots) by setting 'antispam_channels' on the applicable
|
||||
# networks.
|
||||
# Antispam also integrates with relay by punishing users locally (so that bans, kills, etc.
|
||||
# are not bounced by relay) and not relaying messages marked as spam.
|
||||
|
||||
# IMPORTANT: Antispam is in ALPHA stages as of 2.0-alpha3 and some options may not work yet.
|
||||
|
||||
# Determines the minimum level of channel access needed to be exempt from Antispam.
|
||||
# Note: opers are automatically exempt.
|
||||
# Valid values are "voice", "halfop", and "op". This defaults to "halfop" if not set.
|
||||
#exempt_level: halfop
|
||||
|
||||
# Determines whether Antispam should strip formatting before applying filter checks.
|
||||
# Defaults to true if not set.
|
||||
#strip_formatting: true
|
||||
|
||||
#masshighlight:
|
||||
# This block configures options for antispam's mass highlight blocking. It can also be
|
||||
# overridden (as an entire block) per-network by copying its options under
|
||||
# servers::<server name>::antispam_masshighlight
|
||||
|
||||
# Determines whether mass highlight prevention should be enabled. Defaults to false if not
|
||||
# set.
|
||||
#enabled: true
|
||||
|
||||
# Sets the punishment that Antispam should use against mass highlighters.
|
||||
# Valid values include "kill", "kick", "ban", "quiet", "block", and combinations of these
|
||||
# strung together with "+" (e.g. "kick+ban"). Defaults to "kick+ban" if not set.
|
||||
#punishment: kick+ban
|
||||
|
||||
# Sets the kick / kill message used when mass highlight prevention is triggered.
|
||||
#reason: "Mass highlight spam is prohibited"
|
||||
|
||||
# Sets the minimum message length and amount of nicks needed in a message for
|
||||
# mass highlight prevention to trigger.
|
||||
#min_length: 50
|
||||
#min_nicks: 5
|
||||
|
||||
#textfilter:
|
||||
# This block configures options for antispam's text spamfilters. It can also be overridden
|
||||
# (as an entire block) per-network by copying its options under
|
||||
# servers::<server name>::antispam_textfilter
|
||||
|
||||
# Determines whether text spamfilters should be enabled. Defaults to false if not set.
|
||||
#enabled: true
|
||||
|
||||
# Sets the punishment that Antispam's text spamfilter should use.
|
||||
# Valid values include "kill", "kick", "ban", "quiet", and combinations of these strung
|
||||
# together with "+" (e.g. "kick+ban"). Defaults to "kick+ban+block" if not set.
|
||||
# If you want Antispam to also monitor PM spam, you will want to change this to something
|
||||
# not channel-specific (such as "kill" or "block").
|
||||
#punishment: kick+ban+block
|
||||
|
||||
# Sets the kick / kill message used when the text spamfilter is triggered.
|
||||
#reason: "Spam is prohibited"
|
||||
|
||||
# Determines whether PMs to PyLink clients should also be tracked to prevent spam.
|
||||
# Valid values include false (the boolean value), 'services', and 'all':
|
||||
# - If this is set to false, PMs are ignored by text filtering.
|
||||
# - If this is set to 'services', only PMs sent to services are checked for spam (this allows
|
||||
# them to effectively act as PM spam traps)
|
||||
# - If this is set to 'all', all PyLink clients (including relay users) have incoming PMs
|
||||
# checked for spam.
|
||||
# This defaults to false if not set.
|
||||
#watch_pms: false
|
||||
|
||||
# Configures an (ASCII case-insensitive) list of bad strings to block in messages (PRIVMSG, NOTICE).
|
||||
# Globs are supported; use them to your advantage here.
|
||||
# You can also define server specific lists of bad strings by defining
|
||||
# servers::<server name>::antispam_textfilters_globs
|
||||
# the contents of which will be *merged* into the global list of bad strings specified below.
|
||||
#textfilter_globs:
|
||||
# - "*very bad don't say this*"
|
||||
# - "TWFkZSB5b3UgbG9vayE="
|
||||
|
81
launcher.py
81
launcher.py
@ -14,37 +14,14 @@ try:
|
||||
except ImportError:
|
||||
psutil = None
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Starts an instance of PyLink IRC Services.')
|
||||
parser.add_argument('config', help='specifies the path to the config file (defaults to pylink.yml)', nargs='?', default='pylink.yml')
|
||||
parser.add_argument("-v", "--version", help="displays the program version and exits", action='store_true')
|
||||
parser.add_argument("-c", "--check-pid", help="no-op; kept for compatibility with PyLink <= 1.2.x", action='store_true')
|
||||
parser.add_argument("-n", "--no-pid", help="skips generating and checking PID files", action='store_true')
|
||||
parser.add_argument("-r", "--restart", help="restarts the PyLink instance with the given config file", action='store_true')
|
||||
parser.add_argument("-s", "--stop", help="stops the PyLink instance with the given config file", action='store_true')
|
||||
parser.add_argument("-R", "--rehash", help="rehashes the PyLink instance with the given config file", action='store_true')
|
||||
parser.add_argument("-d", "--daemonize", help="[experimental] daemonizes the PyLink instance on POSIX systems", action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.version: # Display version and exit
|
||||
print('PyLink %s (in VCS: %s)' % (__version__, real_version))
|
||||
sys.exit()
|
||||
|
||||
# XXX: repetitive
|
||||
elif args.no_pid and (args.restart or args.stop or args.rehash):
|
||||
print('ERROR: --no-pid cannot be combined with --restart or --stop')
|
||||
sys.exit(1)
|
||||
elif args.rehash and os.name != 'posix':
|
||||
print('ERROR: Rehashing via the command line is not supported outside Unix.')
|
||||
sys.exit(1)
|
||||
args = {}
|
||||
|
||||
def _main():
|
||||
# FIXME: we can't pass logging on to conf until we set up the config...
|
||||
conf.loadConf(args.config)
|
||||
conf.load_conf(args.config)
|
||||
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc import classes, utils, coremods
|
||||
from pylinkirc import classes, utils, coremods, selectdriver
|
||||
|
||||
# Write and check for an existing PID file unless specifically told not to.
|
||||
if not args.no_pid:
|
||||
@ -146,11 +123,11 @@ def main():
|
||||
|
||||
# Load configured plugins
|
||||
to_load = conf.conf['plugins']
|
||||
utils.resetModuleDirs()
|
||||
utils._reset_module_dirs()
|
||||
|
||||
for plugin in to_load:
|
||||
try:
|
||||
world.plugins[plugin] = pl = utils.loadPlugin(plugin)
|
||||
world.plugins[plugin] = pl = utils._load_plugin(plugin)
|
||||
except Exception as e:
|
||||
log.exception('Failed to load plugin %r: %s: %s', plugin, type(e).__name__, str(e))
|
||||
else:
|
||||
@ -167,11 +144,12 @@ def main():
|
||||
else:
|
||||
# Fetch the correct protocol module.
|
||||
try:
|
||||
proto = utils.getProtocolModule(protoname)
|
||||
proto = utils._get_protocol_module(protoname)
|
||||
|
||||
# Create and connect the network.
|
||||
world.networkobjects[network] = irc = proto.Class(network)
|
||||
log.debug('Connecting to network %r', network)
|
||||
world.networkobjects[network] = classes.Irc(network, proto, conf.conf)
|
||||
irc.connect()
|
||||
except:
|
||||
log.exception('(%s) Failed to connect to network %r, skipping it...',
|
||||
network, network)
|
||||
@ -179,6 +157,43 @@ def main():
|
||||
|
||||
world.started.set()
|
||||
log.info("Loaded plugins: %s", ', '.join(sorted(world.plugins.keys())))
|
||||
selectdriver.start()
|
||||
|
||||
from pylinkirc import coremods
|
||||
coremods.permissions.resetPermissions() # Future note: this is moved to run on import in 2.0
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
global args
|
||||
|
||||
parser = argparse.ArgumentParser(description='Starts an instance of PyLink IRC Services.')
|
||||
parser.add_argument('config', help='specifies the path to the config file (defaults to pylink.yml)', nargs='?', default='pylink.yml')
|
||||
parser.add_argument("-v", "--version", help="displays the program version and exits", action='store_true')
|
||||
parser.add_argument("-c", "--check-pid", help="no-op; kept for compatibility with PyLink <= 1.2.x", action='store_true')
|
||||
parser.add_argument("-n", "--no-pid", help="skips generating and checking PID files", action='store_true')
|
||||
parser.add_argument("-r", "--restart", help="restarts the PyLink instance with the given config file", action='store_true')
|
||||
parser.add_argument("-s", "--stop", help="stops the PyLink instance with the given config file", action='store_true')
|
||||
parser.add_argument("-R", "--rehash", help="rehashes the PyLink instance with the given config file", action='store_true')
|
||||
parser.add_argument("-d", "--daemonize", help="[experimental] daemonizes the PyLink instance on POSIX systems", action='store_true')
|
||||
parser.add_argument("-t", "--trace", help="traces through running Python code; useful for debugging", action='store_true')
|
||||
parser.add_argument('--trace-ignore-mods', help='comma-separated list of extra modules to ignore when tracing', action='store', default='')
|
||||
parser.add_argument('--trace-ignore-dirs', help='comma-separated list of extra directories to ignore when tracing', action='store', default='')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.version: # Display version and exit
|
||||
print('PyLink %s (in VCS: %s)' % (__version__, real_version))
|
||||
sys.exit()
|
||||
|
||||
# XXX: repetitive
|
||||
elif args.no_pid and (args.restart or args.stop or args.rehash):
|
||||
print('ERROR: --no-pid cannot be combined with --restart or --stop')
|
||||
sys.exit(1)
|
||||
elif args.rehash and os.name != 'posix':
|
||||
print('ERROR: Rehashing via the command line is not supported outside Unix.')
|
||||
sys.exit(1)
|
||||
|
||||
if args.trace:
|
||||
import trace
|
||||
tracer = trace.Trace(ignoremods=args.trace_ignore_mods.split(','),
|
||||
ignoredirs=args.trace_ignore_dirs.split(','))
|
||||
tracer.runctx('_main()', globals=globals(), locals=locals())
|
||||
else:
|
||||
_main()
|
||||
|
26
log.py
26
log.py
@ -22,7 +22,7 @@ os.makedirs(logdir, exist_ok=True)
|
||||
_format = '%(asctime)s [%(levelname)s] %(message)s'
|
||||
logformatter = logging.Formatter(_format)
|
||||
|
||||
def getConsoleLogLevel():
|
||||
def _get_console_log_level():
|
||||
"""
|
||||
Returns the configured console log level.
|
||||
"""
|
||||
@ -32,7 +32,7 @@ def getConsoleLogLevel():
|
||||
# Set up logging to STDERR
|
||||
world.console_handler = logging.StreamHandler()
|
||||
world.console_handler.setFormatter(logformatter)
|
||||
world.console_handler.setLevel(getConsoleLogLevel())
|
||||
world.console_handler.setLevel(_get_console_log_level())
|
||||
|
||||
# Get the main logger object; plugins can import this variable for convenience.
|
||||
log = logging.getLogger()
|
||||
@ -43,7 +43,7 @@ log.addHandler(world.console_handler)
|
||||
# the root logger. https://stackoverflow.com/questions/16624695
|
||||
log.setLevel(1)
|
||||
|
||||
def makeFileLogger(filename, level=None):
|
||||
def _make_file_logger(filename, level=None):
|
||||
"""
|
||||
Initializes a file logging target with the given filename and level.
|
||||
"""
|
||||
@ -64,7 +64,7 @@ def makeFileLogger(filename, level=None):
|
||||
filelogger.setFormatter(logformatter)
|
||||
|
||||
# If no log level is specified, use the same one as the console logger.
|
||||
level = level or getConsoleLogLevel()
|
||||
level = level or _get_console_log_level()
|
||||
filelogger.setLevel(level)
|
||||
|
||||
log.addHandler(filelogger)
|
||||
@ -73,7 +73,7 @@ def makeFileLogger(filename, level=None):
|
||||
|
||||
return filelogger
|
||||
|
||||
def stopFileLoggers():
|
||||
def _stop_file_loggers():
|
||||
"""
|
||||
De-initializes all file loggers.
|
||||
"""
|
||||
@ -88,17 +88,17 @@ files = conf.conf['logging'].get('files')
|
||||
if files:
|
||||
for filename, config in files.items():
|
||||
if isinstance(config, dict):
|
||||
makeFileLogger(filename, config.get('loglevel'))
|
||||
_make_file_logger(filename, config.get('loglevel'))
|
||||
else:
|
||||
log.warning('Got invalid file logging pair %r: %r; are your indentation and block '
|
||||
'commenting consistent?', filename, config)
|
||||
|
||||
log.debug("log: Emptying log_queue")
|
||||
log.debug("log: Emptying _log_queue")
|
||||
# Process and empty the log queue
|
||||
while world.log_queue:
|
||||
level, text = world.log_queue.popleft()
|
||||
while world._log_queue:
|
||||
level, text = world._log_queue.popleft()
|
||||
log.log(level, text)
|
||||
log.debug("log: Emptied log_queue")
|
||||
log.debug("log: Emptied _log_queue")
|
||||
|
||||
class PyLinkChannelLogger(logging.Handler):
|
||||
"""
|
||||
@ -137,11 +137,9 @@ class PyLinkChannelLogger(logging.Handler):
|
||||
# 1) irc.pseudoclient must be initialized already
|
||||
# 2) IRC object must be finished bursting
|
||||
# 3) Target channel must exist
|
||||
# 4) Main PyLink client must be in this target channel
|
||||
# 5) This function hasn't been called already (prevents recursive loops).
|
||||
# 4) This function hasn't been called already (prevents recursive loops).
|
||||
if self.irc.pseudoclient and self.irc.connected.is_set() \
|
||||
and self.channel in self.irc.channels and self.irc.pseudoclient.uid in \
|
||||
self.irc.channels[self.channel].users and not self.called:
|
||||
and self.channel in self.irc.channels and not self.called:
|
||||
|
||||
self.called = True
|
||||
msg = self.format(record)
|
||||
|
2
log/.gitignore
vendored
2
log/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
295
plugins/antispam.py
Normal file
295
plugins/antispam.py
Normal file
@ -0,0 +1,295 @@
|
||||
# antispam.py: Basic services-side spamfilters for IRC
|
||||
|
||||
import ircmatch
|
||||
|
||||
from pylinkirc import utils, world, conf
|
||||
from pylinkirc.log import log
|
||||
|
||||
mydesc = ("Provides anti-spam functionality.")
|
||||
sbot = utils.register_service("antispam", default_nick="AntiSpam", desc=mydesc)
|
||||
|
||||
def die(irc=None):
|
||||
utils.unregister_service("antispam")
|
||||
|
||||
PUNISH_OPTIONS = ['kill', 'ban', 'quiet', 'kick', 'block']
|
||||
EXEMPT_OPTIONS = ['voice', 'halfop', 'op']
|
||||
DEFAULT_EXEMPT_OPTION = 'halfop'
|
||||
def _punish(irc, target, channel, punishment, reason):
|
||||
"""Punishes the target user. This function returns True if the user was successfully punished."""
|
||||
if target not in irc.users:
|
||||
log.warning("(%s) antispam: got target %r that isn't a user?", irc.name, target)
|
||||
return False
|
||||
elif irc.is_oper(target):
|
||||
log.debug("(%s) antispam: refusing to punish oper %s/%s", irc.name, target, irc.get_friendly_name(target))
|
||||
return False
|
||||
|
||||
target_nick = irc.get_friendly_name(target)
|
||||
|
||||
if channel:
|
||||
c = irc.channels[channel]
|
||||
exempt_level = irc.get_service_option('antispam', 'exempt_level', DEFAULT_EXEMPT_OPTION).lower()
|
||||
|
||||
if exempt_level not in EXEMPT_OPTIONS:
|
||||
log.error('(%s) Antispam exempt %r is not a valid setting, '
|
||||
'falling back to defaults; accepted settings include: %s',
|
||||
irc.name, exempt_level, ', '.join(EXEMPT_OPTIONS))
|
||||
exempt_level = DEFAULT_EXEMPT_OPTION
|
||||
|
||||
if exempt_level == 'voice' and c.is_voice_plus(target):
|
||||
log.debug("(%s) antispam: refusing to punish voiced and above %s/%s", irc.name, target, target_nick)
|
||||
return False
|
||||
elif exempt_level == 'halfop' and c.is_halfop_plus(target):
|
||||
log.debug("(%s) antispam: refusing to punish halfop and above %s/%s", irc.name, target, target_nick)
|
||||
return False
|
||||
elif exempt_level == 'op' and c.is_op_plus(target):
|
||||
log.debug("(%s) antispam: refusing to punish op and above %s/%s", irc.name, target, target_nick)
|
||||
return False
|
||||
|
||||
my_uid = sbot.uids.get(irc.name)
|
||||
# XXX workaround for single-bot protocols like Clientbot
|
||||
if irc.pseudoclient and not irc.has_cap('can-spawn-clients'):
|
||||
my_uid = irc.pseudoclient.uid
|
||||
|
||||
bans = set()
|
||||
log.debug('(%s) antispam: got %r as punishment for %s/%s', irc.name, punishment,
|
||||
target, irc.get_friendly_name(target))
|
||||
|
||||
def _ban():
|
||||
bans.add(irc.make_channel_ban(target))
|
||||
def _quiet():
|
||||
bans.add(irc.make_channel_ban(target, ban_type='quiet'))
|
||||
def _kick():
|
||||
irc.kick(my_uid, channel, target, reason)
|
||||
irc.call_hooks([my_uid, 'ANTISPAM_KICK', {'channel': channel, 'text': reason, 'target': target,
|
||||
'parse_as': 'KICK'}])
|
||||
def _kill():
|
||||
if target not in irc.users:
|
||||
log.debug('(%s) antispam: not killing %s/%s; they already left', irc.name, target,
|
||||
irc.get_friendly_name(target))
|
||||
return
|
||||
userdata = irc.users[target]
|
||||
irc.kill(my_uid, target, reason)
|
||||
irc.call_hooks([my_uid, 'ANTISPAM_KILL', {'target': target, 'text': reason,
|
||||
'userdata': userdata, 'parse_as': 'KILL'}])
|
||||
|
||||
kill = False
|
||||
successful_punishments = 0
|
||||
for action in set(punishment.split('+')):
|
||||
if action not in PUNISH_OPTIONS:
|
||||
log.error('(%s) Antispam punishment %r is not a valid setting; '
|
||||
'accepted settings include: %s OR any combination of '
|
||||
'these joined together with a "+".',
|
||||
irc.name, punishment, ', '.join(PUNISH_OPTIONS))
|
||||
return
|
||||
elif action == 'block':
|
||||
# We only need to increment this for this function to return True
|
||||
successful_punishments += 1
|
||||
elif action == 'kill':
|
||||
kill = True # Delay kills so that the user data doesn't disappear.
|
||||
# XXX factorize these blocks
|
||||
elif action == 'kick' and channel:
|
||||
try:
|
||||
_kick()
|
||||
except NotImplementedError:
|
||||
log.warning("(%s) antispam: Kicks are not supported on this network, skipping; "
|
||||
"target was %s/%s", irc.name, target_nick, channel)
|
||||
else:
|
||||
successful_punishments += 1
|
||||
elif action == 'ban' and channel:
|
||||
try:
|
||||
_ban()
|
||||
except (ValueError, NotImplementedError):
|
||||
log.warning("(%s) antispam: Bans are not supported on this network, skipping; "
|
||||
"target was %s/%s", irc.name, target_nick, channel)
|
||||
else:
|
||||
successful_punishments += 1
|
||||
elif action == 'quiet' and channel:
|
||||
try:
|
||||
_quiet()
|
||||
except (ValueError, NotImplementedError):
|
||||
log.warning("(%s) antispam: Quiet is not supported on this network, skipping; "
|
||||
"target was %s/%s", irc.name, target_nick, channel)
|
||||
else:
|
||||
successful_punishments += 1
|
||||
|
||||
if bans: # Set all bans at once to prevent spam
|
||||
irc.mode(my_uid, channel, bans)
|
||||
irc.call_hooks([my_uid, 'ANTISPAM_BAN',
|
||||
{'target': channel, 'modes': bans, 'parse_as': 'MODE'}])
|
||||
if kill:
|
||||
try:
|
||||
_kill()
|
||||
except NotImplementedError:
|
||||
log.warning("(%s) antispam: Kills are not supported on this network, skipping; "
|
||||
"target was %s/%s", irc.name, target_nick, channel)
|
||||
else:
|
||||
successful_punishments += 1
|
||||
|
||||
if not successful_punishments:
|
||||
log.warning('(%s) antispam: Failed to punish %s with %r, target was %s', irc.name,
|
||||
target_nick, punishment, channel or 'a PM')
|
||||
|
||||
return bool(successful_punishments)
|
||||
|
||||
MASSHIGHLIGHT_DEFAULTS = {
|
||||
'min_length': 50,
|
||||
'min_nicks': 5,
|
||||
'reason': "Mass highlight spam is prohibited",
|
||||
'punishment': 'kick+ban',
|
||||
'enabled': False
|
||||
}
|
||||
def handle_masshighlight(irc, source, command, args):
|
||||
"""Handles mass highlight attacks."""
|
||||
channel = args['target']
|
||||
text = args['text']
|
||||
mhl_settings = irc.get_service_option('antispam', 'masshighlight',
|
||||
MASSHIGHLIGHT_DEFAULTS)
|
||||
|
||||
if not mhl_settings.get('enabled', False):
|
||||
return
|
||||
|
||||
my_uid = sbot.uids.get(irc.name)
|
||||
|
||||
# XXX workaround for single-bot protocols like Clientbot
|
||||
if irc.pseudoclient and not irc.has_cap('can-spawn-clients'):
|
||||
my_uid = irc.pseudoclient.uid
|
||||
|
||||
if (not irc.connected.is_set()) or (not my_uid):
|
||||
# Break if the network isn't ready.
|
||||
log.debug("(%s) antispam.masshighlight: skipping processing; network isn't ready", irc.name)
|
||||
return
|
||||
elif not irc.is_channel(channel):
|
||||
# Not a channel - mass highlight blocking only makes sense within channels
|
||||
log.debug("(%s) antispam.masshighlight: skipping processing; %r is not a channel", irc.name, channel)
|
||||
return
|
||||
elif irc.is_internal_client(source):
|
||||
# Ignore messages from our own clients.
|
||||
log.debug("(%s) antispam.masshighlight: skipping processing message from internal client %s", irc.name, source)
|
||||
return
|
||||
elif source not in irc.users:
|
||||
log.debug("(%s) antispam.masshighlight: ignoring message from non-user %s", irc.name, source)
|
||||
return
|
||||
elif channel not in irc.channels or my_uid not in irc.channels[channel].users:
|
||||
# We're not monitoring this channel.
|
||||
log.debug("(%s) antispam.masshighlight: skipping processing message from channel %r we're not in", irc.name, channel)
|
||||
return
|
||||
elif len(text) < mhl_settings.get('min_length', MASSHIGHLIGHT_DEFAULTS['min_length']):
|
||||
log.debug("(%s) antispam.masshighlight: skipping processing message %r; it's too short", irc.name, text)
|
||||
return
|
||||
|
||||
if irc.get_service_option('antispam', 'strip_formatting', True):
|
||||
text = utils.strip_irc_formatting(text)
|
||||
|
||||
# Strip :, from potential nicks
|
||||
words = [word.rstrip(':,') for word in text.split()]
|
||||
|
||||
userlist = [irc.users[uid].nick for uid in irc.channels[channel].users.copy()]
|
||||
min_nicks = mhl_settings.get('min_nicks', MASSHIGHLIGHT_DEFAULTS['min_nicks'])
|
||||
|
||||
# Don't allow repeating the same nick to trigger punishment
|
||||
nicks_caught = set()
|
||||
|
||||
punished = False
|
||||
for word in words:
|
||||
if word in userlist:
|
||||
nicks_caught.add(word)
|
||||
if len(nicks_caught) >= min_nicks:
|
||||
# Get the punishment and reason.
|
||||
punishment = mhl_settings.get('punishment', MASSHIGHLIGHT_DEFAULTS['punishment']).lower()
|
||||
reason = mhl_settings.get('reason', MASSHIGHLIGHT_DEFAULTS['reason'])
|
||||
|
||||
log.info("(%s) antispam: punishing %s => %s for mass highlight spam",
|
||||
irc.name,
|
||||
irc.get_friendly_name(source),
|
||||
channel)
|
||||
punished = _punish(irc, source, channel, punishment, reason)
|
||||
break
|
||||
|
||||
log.debug('(%s) antispam.masshighlight: got %s/%s nicks on message to %r', irc.name,
|
||||
len(nicks_caught), min_nicks, channel)
|
||||
return not punished # Filter this message from relay, etc. if it triggered protection
|
||||
|
||||
utils.add_hook(handle_masshighlight, 'PRIVMSG', priority=1000)
|
||||
utils.add_hook(handle_masshighlight, 'NOTICE', priority=1000)
|
||||
|
||||
TEXTFILTER_DEFAULTS = {
|
||||
'reason': "Spam is prohibited",
|
||||
'punishment': 'kick+ban+block',
|
||||
'watch_pms': 'false',
|
||||
'enabled': False
|
||||
}
|
||||
def handle_textfilter(irc, source, command, args):
|
||||
"""Antispam text filter handler."""
|
||||
target = args['target']
|
||||
text = args['text']
|
||||
txf_settings = irc.get_service_option('antispam', 'textfilter',
|
||||
TEXTFILTER_DEFAULTS)
|
||||
|
||||
if not txf_settings.get('enabled', False):
|
||||
return
|
||||
|
||||
my_uid = sbot.uids.get(irc.name)
|
||||
|
||||
# XXX workaround for single-bot protocols like Clientbot
|
||||
if irc.pseudoclient and not irc.has_cap('can-spawn-clients'):
|
||||
my_uid = irc.pseudoclient.uid
|
||||
|
||||
if (not irc.connected.is_set()) or (not my_uid):
|
||||
# Break if the network isn't ready.
|
||||
log.debug("(%s) antispam.textfilters: skipping processing; network isn't ready", irc.name)
|
||||
return
|
||||
elif irc.is_internal_client(source):
|
||||
# Ignore messages from our own clients.
|
||||
log.debug("(%s) antispam.textfilters: skipping processing message from internal client %s", irc.name, source)
|
||||
return
|
||||
elif source not in irc.users:
|
||||
log.debug("(%s) antispam.textfilters: ignoring message from non-user %s", irc.name, source)
|
||||
return
|
||||
|
||||
if irc.is_channel(target):
|
||||
channel_or_none = target
|
||||
if target not in irc.channels or my_uid not in irc.channels[target].users:
|
||||
# We're not monitoring this channel.
|
||||
log.debug("(%s) antispam.textfilters: skipping processing message from channel %r we're not in", irc.name, target)
|
||||
return
|
||||
else:
|
||||
channel_or_none = None
|
||||
watch_pms = txf_settings.get('watch_pms', TEXTFILTER_DEFAULTS['watch_pms'])
|
||||
|
||||
if watch_pms == 'services':
|
||||
if not irc.get_service_bot(target):
|
||||
log.debug("(%s) antispam.textfilters: skipping processing; %r is not a service bot (watch_pms='services')", irc.name, target)
|
||||
return
|
||||
elif watch_pms == 'all':
|
||||
log.debug("(%s) antispam.textfilters: checking all PMs (watch_pms='all')", irc.name)
|
||||
pass
|
||||
else:
|
||||
# Not a channel.
|
||||
log.debug("(%s) antispam.textfilters: skipping processing; %r is not a channel and watch_pms is disabled", irc.name, target)
|
||||
return
|
||||
|
||||
# Merge together global and local textfilter lists.
|
||||
txf_globs = set(conf.conf.get('antispam', {}).get('textfilter_globs', [])) | \
|
||||
set(irc.serverdata.get('antispam_textfilter_globs', []))
|
||||
|
||||
punishment = txf_settings.get('punishment', TEXTFILTER_DEFAULTS['punishment']).lower()
|
||||
reason = txf_settings.get('reason', TEXTFILTER_DEFAULTS['reason'])
|
||||
|
||||
if irc.get_service_option('antispam', 'strip_formatting', True):
|
||||
text = utils.strip_irc_formatting(text)
|
||||
|
||||
punished = False
|
||||
for filterglob in txf_globs:
|
||||
if ircmatch.match(1, filterglob, text):
|
||||
log.info("(%s) antispam: punishing %s => %s for text filter %r",
|
||||
irc.name,
|
||||
irc.get_friendly_name(source),
|
||||
irc.get_friendly_name(target),
|
||||
filterglob)
|
||||
punished = _punish(irc, source, channel_or_none, punishment, reason)
|
||||
break
|
||||
|
||||
return not punished # Filter this message from relay, etc. if it triggered protection
|
||||
|
||||
utils.add_hook(handle_textfilter, 'PRIVMSG', priority=999)
|
||||
utils.add_hook(handle_textfilter, 'NOTICE', priority=999)
|
@ -12,12 +12,12 @@ mydesc = ("The \x02Automode\x02 plugin provides simple channel ACL management by
|
||||
"to users matching hostmasks or exttargets.")
|
||||
|
||||
# Register ourselves as a service.
|
||||
modebot = utils.registerService("automode", desc=mydesc)
|
||||
modebot = utils.register_service("automode", default_nick="Automode", desc=mydesc)
|
||||
reply = modebot.reply
|
||||
error = modebot.error
|
||||
|
||||
# Databasing variables.
|
||||
dbname = utils.getDatabaseName('automode')
|
||||
dbname = conf.get_database_name('automode')
|
||||
datastore = structures.JSONDataStore('automode', dbname, default_db=collections.defaultdict(dict))
|
||||
|
||||
db = datastore.store
|
||||
@ -26,29 +26,40 @@ db = datastore.store
|
||||
default_permissions = {"$ircop": ['automode.manage.relay_owned', 'automode.sync.relay_owned',
|
||||
'automode.list']}
|
||||
|
||||
def _join_db_channels(irc):
|
||||
"""
|
||||
Joins the Automode service client to channels on the current network in its DB.
|
||||
"""
|
||||
if not irc.connected.is_set():
|
||||
log.debug('(%s) _join_db_channels: aborting, network not ready yet', irc.name)
|
||||
return
|
||||
|
||||
for entry in db:
|
||||
netname, channel = entry.split('#', 1)
|
||||
channel = '#' + channel
|
||||
if netname == irc.name:
|
||||
modebot.add_persistent_channel(irc, 'automode', channel)
|
||||
|
||||
def main(irc=None):
|
||||
"""Main function, called during plugin loading at start."""
|
||||
"""Main function, called during plugin loading."""
|
||||
|
||||
# Load the automode database.
|
||||
datastore.load()
|
||||
|
||||
# Register our permissions.
|
||||
permissions.addDefaultPermissions(default_permissions)
|
||||
permissions.add_default_permissions(default_permissions)
|
||||
|
||||
# Queue joins to all channels where Automode has entries.
|
||||
for entry in db:
|
||||
netname, channel = entry.split('#', 1)
|
||||
channel = '#' + channel
|
||||
log.debug('automode: auto-joining %s on %s', channel, netname)
|
||||
modebot.join(netname, channel)
|
||||
if irc: # This was a reload.
|
||||
for ircobj in world.networkobjects.values():
|
||||
_join_db_channels(ircobj)
|
||||
|
||||
def die(irc=None):
|
||||
"""Saves the Automode database and quit."""
|
||||
datastore.die()
|
||||
permissions.removeDefaultPermissions(default_permissions)
|
||||
utils.unregisterService('automode')
|
||||
permissions.remove_default_permissions(default_permissions)
|
||||
utils.unregister_service('automode')
|
||||
|
||||
def checkAccess(irc, uid, channel, command):
|
||||
def _check_automode_access(irc, uid, channel, command):
|
||||
"""Checks the caller's access to Automode."""
|
||||
# Automode defines the following permissions, where <command> is either "manage", "list",
|
||||
# "sync", "clear", "remotemanage", "remotelist", "remotesync", "remoteclear":
|
||||
@ -58,18 +69,18 @@ def checkAccess(irc, uid, channel, command):
|
||||
# - automode.<command>.#channel: ability to <command> automode on the given channel.
|
||||
# - automode.savedb: ability to save the automode DB.
|
||||
log.debug('(%s) Automode: checking access for %s/%s for %s capability on %s', irc.name, uid,
|
||||
irc.getHostmask(uid), command, channel)
|
||||
irc.get_hostmask(uid), command, channel)
|
||||
|
||||
baseperm = 'automode.%s' % command
|
||||
try:
|
||||
# First, check the catch all and channel permissions.
|
||||
perms = [baseperm, baseperm+'.*', '%s.%s' % (baseperm, channel)]
|
||||
return permissions.checkPermissions(irc, uid, perms)
|
||||
return permissions.check_permissions(irc, uid, perms)
|
||||
except utils.NotAuthorizedError:
|
||||
if not command.startswith('remote'):
|
||||
# Relay-based ACL checking only works with local calls.
|
||||
log.debug('(%s) Automode: falling back to automode.%s.relay_owned', irc.name, command)
|
||||
permissions.checkPermissions(irc, uid, [baseperm+'.relay_owned'], also_show=perms)
|
||||
permissions.check_permissions(irc, uid, [baseperm+'.relay_owned'], also_show=perms)
|
||||
|
||||
relay = world.plugins.get('relay')
|
||||
if relay is None:
|
||||
@ -99,7 +110,7 @@ def match(irc, channel, uids=None):
|
||||
|
||||
for mask, modes in dbentry.items():
|
||||
for uid in uids:
|
||||
if irc.matchHost(mask, uid):
|
||||
if irc.match_host(mask, uid):
|
||||
# User matched a mask. Filter the mode list given to only those that are valid
|
||||
# prefix mode characters.
|
||||
outgoing_modes += [('+'+mode, uid) for mode in modes if mode in irc.prefixmodes]
|
||||
@ -114,18 +125,24 @@ def match(irc, channel, uids=None):
|
||||
log.debug("(%s) automode: sending modes from modebot_uid %s",
|
||||
irc.name, modebot_uid)
|
||||
|
||||
irc.proto.mode(modebot_uid, channel, outgoing_modes)
|
||||
irc.mode(modebot_uid, channel, outgoing_modes)
|
||||
|
||||
# Create a hook payload to support plugins like relay.
|
||||
irc.callHooks([modebot_uid, 'AUTOMODE_MODE',
|
||||
irc.call_hooks([modebot_uid, 'AUTOMODE_MODE',
|
||||
{'target': channel, 'modes': outgoing_modes, 'parse_as': 'MODE'}])
|
||||
|
||||
def handle_endburst(irc, source, command, args):
|
||||
"""ENDBURST hook handler - used to join the Automode service to channels where it has entries."""
|
||||
if source == irc.uplink:
|
||||
_join_db_channels(irc)
|
||||
utils.add_hook(handle_endburst, 'ENDBURST')
|
||||
|
||||
def handle_join(irc, source, command, args):
|
||||
"""
|
||||
Automode JOIN listener. This sets modes accordingly if the person joining matches a mask in the
|
||||
ACL.
|
||||
"""
|
||||
channel = irc.toLower(args['channel'])
|
||||
channel = irc.to_lower(args['channel'])
|
||||
match(irc, channel, args['users'])
|
||||
|
||||
utils.add_hook(handle_join, 'JOIN')
|
||||
@ -143,7 +160,7 @@ def handle_services_login(irc, source, command, args):
|
||||
utils.add_hook(handle_services_login, 'CLIENT_SERVICES_LOGIN')
|
||||
utils.add_hook(handle_services_login, 'PYLINK_RELAY_SERVICES_LOGIN')
|
||||
|
||||
def getChannelPair(irc, source, chanpair, perm=None):
|
||||
def _get_channel_pair(irc, source, chanpair, perm=None):
|
||||
"""
|
||||
Fetches the network and channel given a channel pair,
|
||||
also optionally checking the caller's permissions.
|
||||
@ -154,23 +171,25 @@ def getChannelPair(irc, source, chanpair, perm=None):
|
||||
except ValueError:
|
||||
raise ValueError("Invalid channel pair %r" % chanpair)
|
||||
channel = '#' + channel
|
||||
channel = irc.toLower(channel)
|
||||
channel = irc.to_lower(channel)
|
||||
|
||||
assert utils.isChannel(channel), "Invalid channel name %s." % channel
|
||||
if not irc.is_channel(channel):
|
||||
raise ValueError("Invalid channel name %s." % channel)
|
||||
|
||||
if network:
|
||||
ircobj = world.networkobjects.get(network)
|
||||
else:
|
||||
ircobj = irc
|
||||
|
||||
assert ircobj, "Unknown network %s" % network
|
||||
if not ircobj:
|
||||
raise ValueError("Unknown network %s" % network)
|
||||
|
||||
if perm is not None:
|
||||
# Only check for permissions if we're told to and the irc object exists.
|
||||
if ircobj.name != irc.name:
|
||||
perm = 'remote' + perm
|
||||
|
||||
checkAccess(irc, source, channel, perm)
|
||||
_check_automode_access(irc, source, channel, perm)
|
||||
|
||||
return (ircobj, channel)
|
||||
|
||||
@ -199,7 +218,7 @@ def setacc(irc, source, args):
|
||||
error(irc, "Invalid arguments given. Needs 3: channel, mask, mode list.")
|
||||
return
|
||||
else:
|
||||
ircobj, channel = getChannelPair(irc, source, chanpair, perm='manage')
|
||||
ircobj, channel = _get_channel_pair(irc, source, chanpair, perm='manage')
|
||||
|
||||
# Database entries for any network+channel pair are automatically created using
|
||||
# defaultdict. Note: string keys are used here instead of tuples so they can be
|
||||
@ -208,20 +227,21 @@ def setacc(irc, source, args):
|
||||
|
||||
modes = modes.lstrip('+') # remove extraneous leading +'s
|
||||
dbentry[mask] = modes
|
||||
log.info('(%s) %s set modes +%s for %s on %s', ircobj.name, irc.getHostmask(source), modes, mask, channel)
|
||||
log.info('(%s) %s set modes +%s for %s on %s', ircobj.name, irc.get_hostmask(source), modes, mask, channel)
|
||||
reply(irc, "Done. \x02%s\x02 now has modes \x02+%s\x02 in \x02%s\x02." % (mask, modes, channel))
|
||||
|
||||
# Join the Automode bot to the channel if not explicitly told to.
|
||||
modebot.join(ircobj, channel)
|
||||
# Join the Automode bot to the channel persistently.
|
||||
modebot.add_persistent_channel(irc, 'automode', channel)
|
||||
|
||||
modebot.add_cmd(setacc, 'setaccess')
|
||||
modebot.add_cmd(setacc, 'set')
|
||||
modebot.add_cmd(setacc, featured=True)
|
||||
modebot.add_cmd(setacc, aliases=('setaccess', 'set'), featured=True)
|
||||
|
||||
def delacc(irc, source, args):
|
||||
"""<channel/chanpair> <mask>
|
||||
"""<channel/chanpair> <mask or range string>
|
||||
|
||||
Removes the Automode entry for the given mask on the given channel, if one exists.
|
||||
Removes the Automode entry for the given mask or range string, if they exist.
|
||||
|
||||
Range strings are indices (entry numbers) or ranges of them joined together with commas: e.g.
|
||||
"1", "2-10", "1,3,5-8". Entry numbers are shown by LISTACC.
|
||||
"""
|
||||
try:
|
||||
chanpair, mask = args
|
||||
@ -229,7 +249,7 @@ def delacc(irc, source, args):
|
||||
error(irc, "Invalid arguments given. Needs 2: channel, mask")
|
||||
return
|
||||
else:
|
||||
ircobj, channel = getChannelPair(irc, source, chanpair, perm='manage')
|
||||
ircobj, channel = _get_channel_pair(irc, source, chanpair, perm='manage')
|
||||
|
||||
dbentry = db.get(ircobj.name+channel)
|
||||
|
||||
@ -239,19 +259,35 @@ def delacc(irc, source, args):
|
||||
|
||||
if mask in dbentry:
|
||||
del dbentry[mask]
|
||||
log.info('(%s) %s removed modes for %s on %s', ircobj.name, irc.getHostmask(source), mask, channel)
|
||||
log.info('(%s) %s removed modes for %s on %s', ircobj.name, irc.get_hostmask(source), mask, channel)
|
||||
reply(irc, "Done. Removed the Automode access entry for \x02%s\x02 in \x02%s\x02." % (mask, channel))
|
||||
else:
|
||||
error(irc, "No Automode access entry for \x02%s\x02 exists in \x02%s\x02." % (mask, channel))
|
||||
# Treat the mask as a range string.
|
||||
try:
|
||||
new_keys = utils.remove_range(mask, sorted(dbentry.keys()))
|
||||
except ValueError:
|
||||
error(irc, "No Automode access entry for \x02%s\x02 exists in \x02%s\x02." % (mask, channel))
|
||||
return
|
||||
|
||||
# XXX: Automode entries are actually unordered: what we're actually doing is sorting the keys
|
||||
# by name into a list, running remove_range on that, and removing the difference.
|
||||
removed = []
|
||||
source_host = irc.get_hostmask(source)
|
||||
for mask_entry in dbentry.copy():
|
||||
if mask_entry not in new_keys:
|
||||
del dbentry[mask_entry]
|
||||
log.info('(%s) %s removed modes for %s on %s', ircobj.name, source_host, mask_entry, channel)
|
||||
removed.append(mask_entry)
|
||||
|
||||
reply(irc, 'Done. Removed \x02%d\x02 entries on \x02%s\x02: %s' % (len(removed), channel, ', '.join(removed)))
|
||||
|
||||
# Remove channels if no more entries are left.
|
||||
if not dbentry:
|
||||
log.debug("Automode: purging empty channel pair %s/%s", ircobj.name, channel)
|
||||
del db[ircobj.name+channel]
|
||||
modebot.remove_persistent_channel(irc, 'automode', channel)
|
||||
|
||||
modebot.add_cmd(delacc, 'delaccess')
|
||||
modebot.add_cmd(delacc, 'del')
|
||||
modebot.add_cmd(delacc, featured=True)
|
||||
modebot.add_cmd(delacc, aliases=('delaccess', 'del'), featured=True)
|
||||
|
||||
def listacc(irc, source, args):
|
||||
"""<channel/chanpair>
|
||||
@ -263,7 +299,7 @@ def listacc(irc, source, args):
|
||||
error(irc, "Invalid arguments given. Needs 1: channel.")
|
||||
return
|
||||
else:
|
||||
ircobj, channel = getChannelPair(irc, source, chanpair, perm='list')
|
||||
ircobj, channel = _get_channel_pair(irc, source, chanpair, perm='list')
|
||||
|
||||
dbentry = db.get(ircobj.name+channel)
|
||||
if not dbentry:
|
||||
@ -274,19 +310,18 @@ def listacc(irc, source, args):
|
||||
# Iterate over all entries and print them. Do this in private to prevent channel
|
||||
# floods.
|
||||
reply(irc, "Showing Automode entries for \x02%s\x02:" % channel, private=True)
|
||||
for entrynum, entry in enumerate(dbentry.items(), start=1):
|
||||
for entrynum, entry in enumerate(sorted(dbentry.items()), start=1):
|
||||
mask, modes = entry
|
||||
reply(irc, "[%s] \x02%s\x02 has modes +\x02%s\x02" % (entrynum, mask, modes), private=True)
|
||||
reply(irc, "End of Automode entries list.", private=True)
|
||||
|
||||
modebot.add_cmd(listacc, featured=True)
|
||||
modebot.add_cmd(listacc, 'listaccess')
|
||||
modebot.add_cmd(listacc, featured=True, aliases=('listaccess',))
|
||||
|
||||
def save(irc, source, args):
|
||||
"""takes no arguments.
|
||||
|
||||
Saves the Automode database to disk."""
|
||||
permissions.checkPermissions(irc, source, ['automode.savedb'])
|
||||
permissions.check_permissions(irc, source, ['automode.savedb'])
|
||||
datastore.save()
|
||||
reply(irc, 'Done.')
|
||||
|
||||
@ -303,16 +338,14 @@ def syncacc(irc, source, args):
|
||||
error(irc, "Invalid arguments given. Needs 1: channel.")
|
||||
return
|
||||
else:
|
||||
ircobj, channel = getChannelPair(irc, source, chanpair, perm='sync')
|
||||
ircobj, channel = _get_channel_pair(irc, source, chanpair, perm='sync')
|
||||
|
||||
log.info('(%s) %s synced modes on %s', ircobj.name, irc.getHostmask(source), channel)
|
||||
log.info('(%s) %s synced modes on %s', ircobj.name, irc.get_hostmask(source), channel)
|
||||
match(ircobj, channel)
|
||||
|
||||
reply(irc, 'Done.')
|
||||
|
||||
modebot.add_cmd(syncacc, featured=True)
|
||||
modebot.add_cmd(syncacc, 'sync')
|
||||
modebot.add_cmd(syncacc, 'syncaccess')
|
||||
modebot.add_cmd(syncacc, featured=True, aliases=('sync', 'syncaccess'))
|
||||
|
||||
def clearacc(irc, source, args):
|
||||
"""<channel>
|
||||
@ -326,15 +359,14 @@ def clearacc(irc, source, args):
|
||||
error(irc, "Invalid arguments given. Needs 1: channel.")
|
||||
return
|
||||
else:
|
||||
ircobj, channel = getChannelPair(irc, source, chanpair, perm='clear')
|
||||
ircobj, channel = _get_channel_pair(irc, source, chanpair, perm='clear')
|
||||
|
||||
if db.get(ircobj.name+channel):
|
||||
del db[ircobj.name+channel]
|
||||
log.info('(%s) %s cleared modes on %s', ircobj.name, irc.getHostmask(source), channel)
|
||||
log.info('(%s) %s cleared modes on %s', ircobj.name, irc.get_hostmask(source), channel)
|
||||
reply(irc, "Done. Removed all Automode access entries for \x02%s\x02." % channel)
|
||||
modebot.remove_persistent_channel(irc, 'automode', channel)
|
||||
else:
|
||||
error(irc, "No Automode access entries exist for \x02%s\x02." % channel)
|
||||
|
||||
modebot.add_cmd(clearacc, 'clearaccess')
|
||||
modebot.add_cmd(clearacc, 'clear')
|
||||
modebot.add_cmd(clearacc, featured=True)
|
||||
modebot.add_cmd(clearacc, aliases=('clearaccess', 'clear'), featured=True)
|
||||
|
103
plugins/bots.py
103
plugins/bots.py
@ -10,62 +10,68 @@ from pylinkirc.coremods import permissions
|
||||
def spawnclient(irc, source, args):
|
||||
"""<nick> <ident> <host>
|
||||
|
||||
Admin-only. Spawns the specified client on the PyLink server.
|
||||
Spawns the specified client on the PyLink server.
|
||||
Note: this doesn't check the validity of any fields you give it!"""
|
||||
permissions.checkPermissions(irc, source, ['bots.spawnclient'])
|
||||
|
||||
if not irc.has_cap('can-spawn-clients'):
|
||||
irc.error("This network does not support client spawning.")
|
||||
return
|
||||
|
||||
permissions.check_permissions(irc, source, ['bots.spawnclient'])
|
||||
try:
|
||||
nick, ident, host = args[:3]
|
||||
except ValueError:
|
||||
irc.error("Not enough arguments. Needs 3: nick, user, host.")
|
||||
return
|
||||
irc.proto.spawnClient(nick, ident, host, manipulatable=True)
|
||||
irc.spawn_client(nick, ident, host, manipulatable=True)
|
||||
irc.reply("Done.")
|
||||
|
||||
@utils.add_cmd
|
||||
def quit(irc, source, args):
|
||||
"""<target> [<reason>]
|
||||
|
||||
Admin-only. Quits the PyLink client with nick <target>, if one exists."""
|
||||
permissions.checkPermissions(irc, source, ['bots.quit'])
|
||||
Quits the PyLink client with nick <target>, if one exists."""
|
||||
permissions.check_permissions(irc, source, ['bots.quit'])
|
||||
|
||||
try:
|
||||
nick = args[0]
|
||||
except IndexError:
|
||||
irc.error("Not enough arguments. Needs 1-2: nick, reason (optional).")
|
||||
return
|
||||
if irc.pseudoclient.uid == irc.nickToUid(nick):
|
||||
|
||||
u = irc.nick_to_uid(nick)
|
||||
|
||||
if irc.pseudoclient.uid == u:
|
||||
irc.error("Cannot quit the main PyLink client!")
|
||||
return
|
||||
|
||||
u = irc.nickToUid(nick)
|
||||
|
||||
quitmsg = ' '.join(args[1:]) or 'Client Quit'
|
||||
|
||||
if not irc.isManipulatableClient(u):
|
||||
if not irc.is_manipulatable_client(u):
|
||||
irc.error("Cannot force quit a protected PyLink services client.")
|
||||
return
|
||||
|
||||
irc.proto.quit(u, quitmsg)
|
||||
irc.quit(u, quitmsg)
|
||||
irc.reply("Done.")
|
||||
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}])
|
||||
irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}])
|
||||
|
||||
def joinclient(irc, source, args):
|
||||
"""[<target>] <channel1>[,<channel2>,<channel3>,...]
|
||||
|
||||
Admin-only. Joins <target>, the nick of a PyLink client, to a comma-separated list of channels.
|
||||
Joins <target>, the nick of a PyLink client, to a comma-separated list of channels.
|
||||
If <target> is not given, it defaults to the main PyLink client.
|
||||
|
||||
For the channel arguments, prefixes can also be specified to join the given client with
|
||||
(e.g. @#channel will join the client with op, while ~@#channel will join it with +qo.
|
||||
"""
|
||||
permissions.checkPermissions(irc, source, ['bots.joinclient'])
|
||||
permissions.check_permissions(irc, source, ['bots.join', 'bots.joinclient'])
|
||||
|
||||
try:
|
||||
# Check if the first argument is an existing PyLink client. If it is not,
|
||||
# then assume that the first argument was actually the channels being joined.
|
||||
u = irc.nickToUid(args[0])
|
||||
u = irc.nick_to_uid(args[0])
|
||||
|
||||
if not irc.isInternalClient(u): # First argument isn't one of our clients
|
||||
if not irc.is_internal_client(u): # First argument isn't one of our clients
|
||||
raise IndexError
|
||||
|
||||
clist = args[1]
|
||||
@ -82,7 +88,7 @@ def joinclient(irc, source, args):
|
||||
irc.error("No valid channels given.")
|
||||
return
|
||||
|
||||
if not (irc.isManipulatableClient(u) or irc.getServiceBot(u)):
|
||||
if not (irc.is_manipulatable_client(u) or irc.get_service_bot(u)):
|
||||
irc.error("Cannot force join a protected PyLink services client.")
|
||||
return
|
||||
|
||||
@ -93,20 +99,24 @@ def joinclient(irc, source, args):
|
||||
prefixes = channel[:len(channel)-len(real_channel)]
|
||||
joinmodes = ''.join(prefix_to_mode[prefix] for prefix in prefixes)
|
||||
|
||||
if not utils.isChannel(real_channel):
|
||||
if not irc.is_channel(real_channel):
|
||||
irc.error("Invalid channel name %r." % real_channel)
|
||||
return
|
||||
|
||||
# join() doesn't support prefixes.
|
||||
if prefixes:
|
||||
irc.proto.sjoin(irc.sid, real_channel, [(joinmodes, u)])
|
||||
irc.sjoin(irc.sid, real_channel, [(joinmodes, u)])
|
||||
else:
|
||||
irc.proto.join(u, real_channel)
|
||||
irc.join(u, real_channel)
|
||||
|
||||
try:
|
||||
modes = irc.channels[real_channel].modes
|
||||
except KeyError:
|
||||
modes = []
|
||||
|
||||
# Call a join hook manually so other plugins like relay can understand it.
|
||||
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_JOIN', {'channel': real_channel, 'users': [u],
|
||||
'modes': irc.channels[real_channel].modes,
|
||||
'parse_as': 'JOIN'}])
|
||||
irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_JOIN', {'channel': real_channel, 'users': [u],
|
||||
'modes': modes, 'parse_as': 'JOIN'}])
|
||||
irc.reply("Done.")
|
||||
utils.add_cmd(joinclient, name='join')
|
||||
|
||||
@ -114,9 +124,9 @@ utils.add_cmd(joinclient, name='join')
|
||||
def nick(irc, source, args):
|
||||
"""[<target>] <newnick>
|
||||
|
||||
Admin-only. Changes the nick of <target>, a PyLink client, to <newnick>. If <target> is not given, it defaults to the main PyLink client."""
|
||||
Changes the nick of <target>, a PyLink client, to <newnick>. If <target> is not given, it defaults to the main PyLink client."""
|
||||
|
||||
permissions.checkPermissions(irc, source, ['bots.nick'])
|
||||
permissions.check_permissions(irc, source, ['bots.nick'])
|
||||
|
||||
try:
|
||||
nick = args[0]
|
||||
@ -128,30 +138,30 @@ def nick(irc, source, args):
|
||||
except IndexError:
|
||||
irc.error("Not enough arguments. Needs 1-2: nick (optional), newnick.")
|
||||
return
|
||||
u = irc.nickToUid(nick)
|
||||
u = irc.nick_to_uid(nick)
|
||||
|
||||
if newnick in ('0', u): # Allow /nick 0 to work
|
||||
newnick = u
|
||||
|
||||
elif not utils.isNick(newnick):
|
||||
elif not irc.is_nick(newnick):
|
||||
irc.error('Invalid nickname %r.' % newnick)
|
||||
return
|
||||
|
||||
elif not (irc.isManipulatableClient(u) or irc.getServiceBot(u)):
|
||||
elif not (irc.is_manipulatable_client(u) or irc.get_service_bot(u)):
|
||||
irc.error("Cannot force nick changes for a protected PyLink services client.")
|
||||
return
|
||||
|
||||
irc.proto.nick(u, newnick)
|
||||
irc.nick(u, newnick)
|
||||
irc.reply("Done.")
|
||||
# Ditto above: manually send a NICK change hook payload to other plugins.
|
||||
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_NICK', {'newnick': newnick, 'oldnick': nick, 'parse_as': 'NICK'}])
|
||||
irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_NICK', {'newnick': newnick, 'oldnick': nick, 'parse_as': 'NICK'}])
|
||||
|
||||
@utils.add_cmd
|
||||
def part(irc, source, args):
|
||||
"""[<target>] <channel1>,[<channel2>],... [<reason>]
|
||||
|
||||
Admin-only. Parts <target>, the nick of a PyLink client, from a comma-separated list of channels. If <target> is not given, it defaults to the main PyLink client."""
|
||||
permissions.checkPermissions(irc, source, ['bots.part'])
|
||||
Parts <target>, the nick of a PyLink client, from a comma-separated list of channels. If <target> is not given, it defaults to the main PyLink client."""
|
||||
permissions.check_permissions(irc, source, ['bots.part'])
|
||||
|
||||
try:
|
||||
nick = args[0]
|
||||
@ -161,8 +171,8 @@ def part(irc, source, args):
|
||||
|
||||
# First, check if the first argument is an existing PyLink client. If it is not,
|
||||
# then assume that the first argument was actually the channels being parted.
|
||||
u = irc.nickToUid(nick)
|
||||
if not irc.isInternalClient(u): # First argument isn't one of our clients
|
||||
u = irc.nick_to_uid(nick)
|
||||
if not irc.is_internal_client(u): # First argument isn't one of our clients
|
||||
raise IndexError
|
||||
|
||||
except IndexError: # No nick was given; shift arguments one to the left.
|
||||
@ -180,25 +190,24 @@ def part(irc, source, args):
|
||||
irc.error("No valid channels given.")
|
||||
return
|
||||
|
||||
if not (irc.isManipulatableClient(u) or irc.getServiceBot(u)):
|
||||
if not (irc.is_manipulatable_client(u) or irc.get_service_bot(u)):
|
||||
irc.error("Cannot force part a protected PyLink services client.")
|
||||
return
|
||||
|
||||
for channel in clist:
|
||||
if not utils.isChannel(channel):
|
||||
if not irc.is_channel(channel):
|
||||
irc.error("Invalid channel name %r." % channel)
|
||||
return
|
||||
irc.proto.part(u, channel, reason)
|
||||
irc.part(u, channel, reason)
|
||||
|
||||
irc.reply("Done.")
|
||||
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_PART', {'channels': clist, 'text': reason, 'parse_as': 'PART'}])
|
||||
irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_PART', {'channels': clist, 'text': reason, 'parse_as': 'PART'}])
|
||||
|
||||
@utils.add_cmd
|
||||
def msg(irc, source, args):
|
||||
"""[<source>] <target> <text>
|
||||
|
||||
Admin-only. Sends message <text> from <source>, where <source> is the nick of a PyLink client. If <source> is not given, it defaults to the main PyLink client."""
|
||||
permissions.checkPermissions(irc, source, ['bots.msg'])
|
||||
Sends message <text> from <source>, where <source> is the nick of a PyLink client. If <source> is not given, it defaults to the main PyLink client."""
|
||||
permissions.check_permissions(irc, source, ['bots.msg'])
|
||||
|
||||
# Because we want the source nick to be optional, this argument parsing gets a bit tricky.
|
||||
try:
|
||||
@ -208,8 +217,8 @@ def msg(irc, source, args):
|
||||
|
||||
# First, check if the first argument is an existing PyLink client. If it is not,
|
||||
# then assume that the first argument was actually the message TARGET.
|
||||
sourceuid = irc.nickToUid(msgsource)
|
||||
if not irc.isInternalClient(sourceuid): # First argument isn't one of our clients
|
||||
sourceuid = irc.nick_to_uid(msgsource)
|
||||
if not irc.is_internal_client(sourceuid): # First argument isn't one of our clients
|
||||
raise IndexError
|
||||
|
||||
if not text:
|
||||
@ -227,16 +236,16 @@ def msg(irc, source, args):
|
||||
irc.error('No text given.')
|
||||
return
|
||||
|
||||
if not utils.isChannel(target):
|
||||
if not irc.is_channel(target):
|
||||
# Convert nick of the message target to a UID, if the target isn't a channel
|
||||
real_target = irc.nickToUid(target)
|
||||
real_target = irc.nick_to_uid(target)
|
||||
if real_target is None: # Unknown target user, if target isn't a valid channel name
|
||||
irc.error('Unknown user %r.' % target)
|
||||
return
|
||||
else:
|
||||
real_target = target
|
||||
|
||||
irc.proto.message(sourceuid, real_target, text)
|
||||
irc.message(sourceuid, real_target, text)
|
||||
irc.reply("Done.")
|
||||
irc.callHooks([sourceuid, 'PYLINK_BOTSPLUGIN_MSG', {'target': real_target, 'text': text, 'parse_as': 'PRIVMSG'}])
|
||||
utils.add_cmd(msg, 'say')
|
||||
irc.call_hooks([sourceuid, 'PYLINK_BOTSPLUGIN_MSG', {'target': real_target, 'text': text, 'parse_as': 'PRIVMSG'}])
|
||||
utils.add_cmd(msg, aliases=('say',))
|
||||
|
@ -19,7 +19,7 @@ def _changehost(irc, target, args):
|
||||
|
||||
if target not in irc.users:
|
||||
return
|
||||
elif irc.isInternalClient(target):
|
||||
elif irc.is_internal_client(target):
|
||||
log.debug('(%s) Skipping changehost on internal client %s', irc.name, target)
|
||||
return
|
||||
|
||||
@ -48,7 +48,7 @@ def _changehost(irc, target, args):
|
||||
|
||||
for host_glob, host_template in changehost_hosts.items():
|
||||
log.debug('(%s) Changehost: checking mask %s', irc.name, host_glob)
|
||||
if irc.matchHost(host_glob, target, ip=match_ip, realhost=match_realhosts):
|
||||
if irc.match_host(host_glob, target, ip=match_ip, realhost=match_realhosts):
|
||||
log.debug('(%s) Changehost matched mask %s', irc.name, host_glob)
|
||||
# This uses template strings for simple substitution:
|
||||
# https://docs.python.org/3/library/string.html#template-strings
|
||||
@ -78,7 +78,7 @@ def _changehost(irc, target, args):
|
||||
if char not in allowed_chars:
|
||||
new_host = new_host.replace(char, '-')
|
||||
|
||||
irc.proto.updateClient(target, 'HOST', new_host)
|
||||
irc.update_client(target, 'HOST', new_host)
|
||||
|
||||
# Only operate on the first match.
|
||||
break
|
||||
@ -103,20 +103,20 @@ def handle_chghost(irc, sender, command, args):
|
||||
|
||||
target = args['target']
|
||||
|
||||
if (not irc.isInternalClient(sender)) and (not irc.isInternalServer(sender)):
|
||||
if (not irc.is_internal_client(sender)) and (not irc.is_internal_server(sender)):
|
||||
if irc.name in changehost_conf.get('enforced_nets', []):
|
||||
log.debug('(%s) Enforce for network is on, re-checking host for target %s/%s',
|
||||
irc.name, target, irc.getFriendlyName(target))
|
||||
irc.name, target, irc.get_friendly_name(target))
|
||||
|
||||
for ex in changehost_conf.get("enforce_exceptions", []):
|
||||
if irc.matchHost(ex, target):
|
||||
if irc.match_host(ex, target):
|
||||
log.debug('(%s) Skipping host change for target %s; they are exempted by mask %s',
|
||||
irc.name, target, ex)
|
||||
return
|
||||
|
||||
userdata = irc.users.get(target)
|
||||
if userdata:
|
||||
_changehost(irc, target, userdata.__dict__)
|
||||
userobj = irc.users.get(target)
|
||||
if userobj:
|
||||
_changehost(irc, target, userobj.get_fields())
|
||||
|
||||
utils.add_hook(handle_chghost, 'CHGHOST')
|
||||
|
||||
@ -126,7 +126,7 @@ def applyhosts(irc, sender, args):
|
||||
|
||||
Applies all configured hosts for users on the given network, or the current network if none is specified."""
|
||||
|
||||
permissions.checkPermissions(irc, sender, ['changehost.applyhosts'])
|
||||
permissions.check_permissions(irc, sender, ['changehost.applyhosts'])
|
||||
|
||||
try: # Try to get network from the command line.
|
||||
network = world.networkobjects[args[0]]
|
||||
|
@ -1,5 +1,5 @@
|
||||
# commands.py: base PyLink commands
|
||||
from time import ctime
|
||||
import time
|
||||
|
||||
from pylinkirc import utils, __version__, world, real_version
|
||||
from pylinkirc.log import log
|
||||
@ -12,24 +12,24 @@ default_permissions = {"*!*@*": ['commands.status', 'commands.showuser', 'comman
|
||||
def main(irc=None):
|
||||
"""Commands plugin main function, called on plugin load."""
|
||||
# Register our permissions.
|
||||
permissions.addDefaultPermissions(default_permissions)
|
||||
permissions.add_default_permissions(default_permissions)
|
||||
|
||||
def die(irc=None):
|
||||
"""Commands plugin die function, called on plugin unload."""
|
||||
permissions.removeDefaultPermissions(default_permissions)
|
||||
permissions.remove_default_permissions(default_permissions)
|
||||
|
||||
@utils.add_cmd
|
||||
def status(irc, source, args):
|
||||
"""takes no arguments.
|
||||
|
||||
Returns your current PyLink login status."""
|
||||
permissions.checkPermissions(irc, source, ['commands.status'])
|
||||
permissions.check_permissions(irc, source, ['commands.status'])
|
||||
identified = irc.users[source].account
|
||||
if identified:
|
||||
irc.reply('You are identified as \x02%s\x02.' % identified)
|
||||
else:
|
||||
irc.reply('You are not identified as anyone.')
|
||||
irc.reply('Operator access: \x02%s\x02' % bool(irc.isOper(source)))
|
||||
irc.reply('Operator access: \x02%s\x02' % bool(irc.is_oper(source)))
|
||||
|
||||
_none = '\x1D(none)\x1D'
|
||||
@utils.add_cmd
|
||||
@ -37,16 +37,16 @@ def showuser(irc, source, args):
|
||||
"""<user>
|
||||
|
||||
Shows information about <user>."""
|
||||
permissions.checkPermissions(irc, source, ['commands.showuser'])
|
||||
permissions.check_permissions(irc, source, ['commands.showuser'])
|
||||
try:
|
||||
target = args[0]
|
||||
except IndexError:
|
||||
irc.error("Not enough arguments. Needs 1: nick.")
|
||||
return
|
||||
u = irc.nickToUid(target) or target
|
||||
u = irc.nick_to_uid(target) or target
|
||||
# Only show private info if the person is calling 'showuser' on themselves,
|
||||
# or is an oper.
|
||||
verbose = irc.isOper(source) or u == source
|
||||
verbose = irc.is_oper(source) or u == source
|
||||
if u not in irc.users:
|
||||
irc.error('Unknown user %r.' % target)
|
||||
return
|
||||
@ -57,17 +57,19 @@ def showuser(irc, source, args):
|
||||
f('Showing information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident,
|
||||
userobj.host, userobj.realname))
|
||||
|
||||
sid = irc.getServer(u)
|
||||
sid = irc.get_server(u)
|
||||
serverobj = irc.servers[sid]
|
||||
ts = userobj.ts
|
||||
|
||||
# Show connected server & nick TS
|
||||
f('\x02Home server\x02: %s (%s); \x02Nick TS:\x02 %s (%s)' % \
|
||||
(serverobj.name, sid, ctime(float(ts)), ts))
|
||||
# Show connected server & nick TS if available
|
||||
serverinfo = '%s[%s]' % (serverobj.name, sid) \
|
||||
if irc.has_cap('can-track-servers') else 'N/A'
|
||||
tsinfo = '%s [UTC] (%s)' % (time.asctime(time.gmtime(int(ts))), ts) \
|
||||
if irc.has_cap('has-ts') else 'N/A'
|
||||
f('\x02Home server\x02: %s; \x02Nick TS:\x02 %s' % (serverinfo, tsinfo))
|
||||
|
||||
if verbose: # Oper only data: user modes, channels on, account info, etc.
|
||||
|
||||
f('\x02User modes\x02: %s' % irc.joinModes(userobj.modes, sort=True))
|
||||
if verbose: # Oper/self only data: user modes, channels in, account info, etc.
|
||||
f('\x02User modes\x02: %s' % irc.join_modes(userobj.modes, sort=True))
|
||||
f('\x02Protocol UID\x02: %s; \x02Real host\x02: %s; \x02IP\x02: %s' % \
|
||||
(u, userobj.realhost, userobj.ip))
|
||||
channels = sorted(userobj.channels)
|
||||
@ -81,9 +83,9 @@ def showchan(irc, source, args):
|
||||
"""<channel>
|
||||
|
||||
Shows information about <channel>."""
|
||||
permissions.checkPermissions(irc, source, ['commands.showchan'])
|
||||
permissions.check_permissions(irc, source, ['commands.showchan'])
|
||||
try:
|
||||
channel = irc.toLower(args[0])
|
||||
channel = args[0]
|
||||
except IndexError:
|
||||
irc.error("Not enough arguments. Needs 1: channel.")
|
||||
return
|
||||
@ -95,7 +97,7 @@ def showchan(irc, source, args):
|
||||
|
||||
c = irc.channels[channel]
|
||||
# Only show verbose info if caller is oper or is in the target channel.
|
||||
verbose = source in c.users or irc.isOper(source)
|
||||
verbose = source in c.users or irc.is_oper(source)
|
||||
secret = ('s', None) in c.modes
|
||||
if secret and not verbose:
|
||||
# Hide secret channels from normal users.
|
||||
@ -109,18 +111,20 @@ def showchan(irc, source, args):
|
||||
f('\x02Channel topic\x02: %s' % c.topic)
|
||||
|
||||
# Mark TS values as untrusted on Clientbot and others (where TS is read-only or not trackable)
|
||||
f('\x02Channel creation time\x02: %s (%s)%s' % (ctime(c.ts), c.ts,
|
||||
' [UNTRUSTED]' if not irc.proto.hasCap('has-ts') else ''))
|
||||
f('\x02Channel creation time\x02: %s (%s) [UTC]%s' %
|
||||
(time.asctime(time.gmtime(int(c.ts))), c.ts,
|
||||
' [UNTRUSTED]' if not irc.has_cap('has-ts') else ''))
|
||||
|
||||
# Show only modes that aren't list-style modes.
|
||||
modes = irc.joinModes([m for m in c.modes if m[0] not in irc.cmodes['*A']], sort=True)
|
||||
modes = irc.join_modes([m for m in c.modes if m[0] not in irc.cmodes['*A']], sort=True)
|
||||
f('\x02Channel modes\x02: %s' % modes)
|
||||
if verbose:
|
||||
nicklist = []
|
||||
# Iterate over the user list, sorted by nick.
|
||||
for user, nick in sorted(zip(c.users, nicks),
|
||||
key=lambda userpair: userpair[1].lower()):
|
||||
for pmode in c.getPrefixModes(user):
|
||||
# Note: reversed() is used here because we're adding prefixes onto the nick in reverse
|
||||
for pmode in reversed(c.get_prefix_modes(user)):
|
||||
# Show prefix modes in order from highest to lowest.
|
||||
nick = irc.prefixmodes.get(irc.cmodes.get(pmode, ''), '') + nick
|
||||
nicklist.append(nick)
|
||||
@ -142,7 +146,10 @@ def echo(irc, source, args):
|
||||
"""<text>
|
||||
|
||||
Echoes the text given."""
|
||||
permissions.checkPermissions(irc, source, ['commands.echo'])
|
||||
permissions.check_permissions(irc, source, ['commands.echo'])
|
||||
if not args:
|
||||
irc.error('No text to send!')
|
||||
return
|
||||
irc.reply(' '.join(args))
|
||||
|
||||
def _check_logout_access(irc, source, target, perms):
|
||||
@ -154,7 +161,7 @@ def _check_logout_access(irc, source, target, perms):
|
||||
assert source in irc.users, "Unknown source user"
|
||||
assert target in irc.users, "Unknown target user"
|
||||
try:
|
||||
permissions.checkPermissions(irc, source, perms)
|
||||
permissions.check_permissions(irc, source, perms)
|
||||
except utils.NotAuthorizedError:
|
||||
if irc.users[source].account and (irc.users[source].account == irc.users[target].account):
|
||||
return True
|
||||
@ -179,7 +186,7 @@ def logout(irc, source, args):
|
||||
irc.error("You are not logged in!")
|
||||
return
|
||||
else:
|
||||
otheruid = irc.nickToUid(othernick)
|
||||
otheruid = irc.nick_to_uid(othernick)
|
||||
if not otheruid:
|
||||
irc.error("Unknown user %s." % othernick)
|
||||
return
|
||||
@ -200,7 +207,7 @@ def loglevel(irc, source, args):
|
||||
|
||||
Sets the log level to the given <level>. <level> must be either DEBUG, INFO, WARNING, ERROR, or CRITICAL.
|
||||
If no log level is given, shows the current one."""
|
||||
permissions.checkPermissions(irc, source, ['commands.loglevel'])
|
||||
permissions.check_permissions(irc, source, ['commands.loglevel'])
|
||||
try:
|
||||
level = args[0].upper()
|
||||
try:
|
||||
|
@ -5,25 +5,59 @@ import datetime
|
||||
from pylinkirc import utils
|
||||
from pylinkirc.log import log
|
||||
|
||||
def handle_ctcpversion(irc, source, args):
|
||||
def handle_ctcp(irc, source, command, args):
|
||||
"""
|
||||
CTCP event handler.
|
||||
"""
|
||||
text = args['text']
|
||||
if not (text.startswith('\x01') and text.endswith('\x01')):
|
||||
return None # Pass through to other plugins
|
||||
|
||||
target = args['target']
|
||||
if not irc.get_service_bot(target):
|
||||
# Ignore this message if the target isn't a service bot
|
||||
return None
|
||||
|
||||
text = text.strip('\x01')
|
||||
try:
|
||||
ctcp_command, data = text.split(" ", 1)
|
||||
except ValueError:
|
||||
ctcp_command = text
|
||||
data = ''
|
||||
|
||||
ctcp_command = ctcp_command.upper()
|
||||
log.debug('(%s) ctcp: got CTCP command %r, data %r',
|
||||
irc.name, ctcp_command, data)
|
||||
|
||||
if ctcp_command in SUPPORTED_COMMANDS:
|
||||
log.info('(%s) Received CTCP %s from %s to %s',
|
||||
irc.name, ctcp_command, irc.get_hostmask(source),
|
||||
irc.get_friendly_name(target))
|
||||
|
||||
# Call the helper function and display its result.
|
||||
result = SUPPORTED_COMMANDS[ctcp_command](irc, source, ctcp_command, data)
|
||||
if result and source in irc.users:
|
||||
# Note, do NOT use irc.reply() in hook handlers because nothing except the
|
||||
# command handler system actually updates the last caller.
|
||||
irc.msg(source, '\x01%s %s\x01' % (ctcp_command, result),
|
||||
notice=True, source=target)
|
||||
|
||||
return False # Block this message from reaching the general command handler
|
||||
else:
|
||||
log.info('(%s) Received unknown CTCP %s from %s to %s',
|
||||
irc.name, ctcp_command, irc.get_hostmask(source),
|
||||
irc.get_friendly_name(target))
|
||||
return False
|
||||
|
||||
utils.add_hook(handle_ctcp, 'PRIVMSG', priority=200)
|
||||
|
||||
def handle_ctcpversion(irc, source, ctcp, data):
|
||||
"""
|
||||
Handles CTCP version requests.
|
||||
"""
|
||||
irc.msg(source, '\x01VERSION %s\x01' % irc.version(), notice=True)
|
||||
return irc.version()
|
||||
|
||||
utils.add_cmd(handle_ctcpversion, '\x01version')
|
||||
utils.add_cmd(handle_ctcpversion, '\x01version\x01')
|
||||
|
||||
def handle_ctcpping(irc, source, args):
|
||||
"""
|
||||
Handles CTCP ping requests.
|
||||
"""
|
||||
# CTCP PING 23152511
|
||||
pingarg = ' '.join(args).strip('\x01')
|
||||
irc.msg(source, '\x01PING %s\x01' % pingarg, notice=True)
|
||||
utils.add_cmd(handle_ctcpping, '\x01ping')
|
||||
|
||||
def handle_ctcpeaster(irc, source, args):
|
||||
def handle_ctcpeaster(irc, source, ctcp, data):
|
||||
"""
|
||||
Secret easter egg.
|
||||
"""
|
||||
@ -44,11 +78,10 @@ def handle_ctcpeaster(irc, source, args):
|
||||
"Hey, can you keep a secret? \x031,1 %s" % " " * random.randint(1,20),
|
||||
]
|
||||
|
||||
irc.msg(source, '\x01EASTER %s\x01' % random.choice(responses), notice=True)
|
||||
return random.choice(responses)
|
||||
|
||||
utils.add_cmd(handle_ctcpeaster, '\x01easter')
|
||||
utils.add_cmd(handle_ctcpeaster, '\x01easter\x01')
|
||||
utils.add_cmd(handle_ctcpeaster, '\x01about')
|
||||
utils.add_cmd(handle_ctcpeaster, '\x01about\x01')
|
||||
utils.add_cmd(handle_ctcpeaster, '\x01pylink')
|
||||
utils.add_cmd(handle_ctcpeaster, '\x01pylink\x01')
|
||||
# Map CTCP commands to functions generating an appropriate text response.
|
||||
SUPPORTED_COMMANDS = {'VERSION': handle_ctcpversion,
|
||||
'PING': lambda irc, source, ctcp, data: data,
|
||||
'ABOUT': handle_ctcpeaster,
|
||||
'EASTER': handle_ctcpeaster}
|
||||
|
@ -18,9 +18,9 @@ def hook_privmsg(irc, source, command, args):
|
||||
channel = args['target']
|
||||
text = args['text']
|
||||
|
||||
# irc.pseudoclient stores the IrcUser object of the main PyLink client.
|
||||
# irc.pseudoclient stores the User object of the main PyLink client.
|
||||
# (i.e. the user defined in the bot: section of the config)
|
||||
if utils.isChannel(channel) and irc.pseudoclient.nick in text:
|
||||
if irc.is_channel(channel) and irc.pseudoclient.nick in text:
|
||||
irc.msg(channel, 'hi there!')
|
||||
# log.debug, log.info, log.warning, log.error, log.exception (within except: clauses)
|
||||
# and log.critical are supported here.
|
||||
@ -31,7 +31,8 @@ utils.add_hook(hook_privmsg, 'PRIVMSG')
|
||||
|
||||
# Example command function. @utils.add_cmd binds it to an IRC command of the same name,
|
||||
# but you can also use a different name by specifying a second 'name' argument (see below).
|
||||
@utils.add_cmd
|
||||
#@utils.add_cmd
|
||||
|
||||
# irc: The IRC object where the command was called.
|
||||
# source: The UID/numeric of the calling user.
|
||||
# args: A list of command args (excluding the command name) that the command was called with.
|
||||
@ -43,9 +44,7 @@ def randint(irc, source, args):
|
||||
# line, even though it is physically written on two.
|
||||
# - Double line breaks are treated as breaks between two paragraphs, and will be shown
|
||||
# as distinct lines in IRC.
|
||||
|
||||
# Note: you shouldn't make any one paragraph too long, since they may get cut off. Automatic
|
||||
# word-wrap may be added in the future; see https://github.com/GLolol/PyLink/issues/153
|
||||
# As of PyLink 2.0, long paragraphs are automatically word-wrapped by irc.reply().
|
||||
"""[<min> <max>]
|
||||
|
||||
Returns a random number between <min> and <max>. <min> and <max> default to 1 and 10
|
||||
@ -64,6 +63,5 @@ def randint(irc, source, args):
|
||||
# it will send replies into the channel instead of in your PM.
|
||||
irc.reply(str(n))
|
||||
|
||||
# You can also bind a command function multiple times, and/or to different command names via a
|
||||
# second argument.
|
||||
utils.add_cmd(randint, "random")
|
||||
# You can bind a command function to multiple names using the 'aliases' option.
|
||||
utils.add_cmd(randint, "random", aliases=("randint", "getrandint"))
|
||||
|
@ -1,62 +0,0 @@
|
||||
# example_service.py: An example using the PyLink services API.
|
||||
import random
|
||||
|
||||
from pylinkirc import utils
|
||||
from pylinkirc.log import log
|
||||
|
||||
# The first step is to register ourselves as a service. utils.registerService() passes keyword
|
||||
# arguments (configuration options) to ServiceBot, which in turn supports the following:
|
||||
#
|
||||
# - name (required): The name of the service.
|
||||
#
|
||||
# - default_help=True: Determines whether the built-in 'help' command should be enabled for this
|
||||
# bot.
|
||||
#
|
||||
# - default_list=True: Determines whether the built-in 'list' command should be enabled for this
|
||||
# bot.
|
||||
#
|
||||
# - nick=None: The fallback nick that the service bot should use if nothing is specified
|
||||
# in the config (i.e. both serverdata:SERVICENAME_nick and conf:SERVICE:nick
|
||||
# are missing). If left empty, the fallback nick will just be the service
|
||||
# name.
|
||||
#
|
||||
# - ident=None: The fallback ident that the service bot should use if nothing is specified
|
||||
# in the config (i.e. both serverdata:SERVICENAME_ident and
|
||||
# conf:SERVICE:ident are missing). If left empty, the fallback ident will
|
||||
# just be the service name.
|
||||
#
|
||||
# - manipulatable=False: Determines whether the service bot should be manipulable by things like
|
||||
# the 'join' command in the 'bots' plugin. Depending on the nature of your
|
||||
# plugin, it's really up to you whether you want to enable this.
|
||||
#
|
||||
# - desc=None: An optional service description that's shown (if present) when the 'help'
|
||||
# command is called without an argument.
|
||||
|
||||
mydesc = "Example service plugin."
|
||||
# Note: the service name is case-insensitive and always lowercase.
|
||||
servicebot = utils.registerService("exampleserv", manipulatable=True, desc=mydesc,
|
||||
nick='ExampleServ')
|
||||
|
||||
# These convenience assignments allow calling reply() and error() more quickly, but you can remove
|
||||
# them and call the functions directly if you don't want them.
|
||||
reply = servicebot.reply
|
||||
error = servicebot.error
|
||||
|
||||
# Command functions for service bots are mostly the same as commands for the main PyLink client,
|
||||
# with a couple of key differences:
|
||||
def greet(irc, source, args):
|
||||
"""takes no arguments.
|
||||
|
||||
Greets the caller.
|
||||
"""
|
||||
response = random.choice(['Hi!', 'Hello!'])
|
||||
# 1) Instead of calling irc.reply() or irc.error(), which return data through the main PyLink
|
||||
# bot, use the reply() and error() commands in the ServiceBot instance (servicebot).
|
||||
# These functions take the Irc object as the first argument, but otherwise use the same
|
||||
# options as irc.reply().
|
||||
reply(irc, response)
|
||||
|
||||
# 2) Instead of using utils.add_cmd(function, 'name'), bind functions to your ServiceBot instance.
|
||||
# You can also use the featured=True argument to display the command's syntax directly in 'list'.
|
||||
servicebot.add_cmd(greet, featured=True)
|
||||
servicebot.add_cmd(greet, 'g')
|
@ -2,14 +2,14 @@
|
||||
exec.py: Provides commands for executing raw code and debugging PyLink.
|
||||
"""
|
||||
import pprint
|
||||
import threading
|
||||
|
||||
from pylinkirc import utils, world
|
||||
from pylinkirc import *
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.coremods import permissions
|
||||
|
||||
# These imports are not strictly necessary, but make the following modules
|
||||
# easier to access through eval and exec.
|
||||
import threading
|
||||
import re
|
||||
import time
|
||||
import pylinkirc
|
||||
@ -25,7 +25,7 @@ def _exec(irc, source, args, locals_dict=None):
|
||||
Admin-only. Executes <code> in the current PyLink instance. This command performs backslash escaping of characters, so things like \\n and \\ will work.
|
||||
|
||||
\x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02"""
|
||||
permissions.checkPermissions(irc, source, ['exec.exec'])
|
||||
permissions.check_permissions(irc, source, ['exec.exec'])
|
||||
|
||||
# Allow using \n in the code, while escaping backslashes correctly otherwise.
|
||||
args = bytes(' '.join(args), 'utf-8').decode("unicode_escape")
|
||||
@ -34,7 +34,7 @@ def _exec(irc, source, args, locals_dict=None):
|
||||
return
|
||||
|
||||
log.info('(%s) Executing %r for %s', irc.name, args,
|
||||
irc.getHostmask(source))
|
||||
irc.get_hostmask(source))
|
||||
if locals_dict is None:
|
||||
locals_dict = locals()
|
||||
else:
|
||||
@ -69,7 +69,7 @@ def _eval(irc, source, args, locals_dict=None, pretty_print=False):
|
||||
Admin-only. Evaluates the given Python expression and returns the result.
|
||||
|
||||
\x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02"""
|
||||
permissions.checkPermissions(irc, source, ['exec.eval'])
|
||||
permissions.check_permissions(irc, source, ['exec.eval'])
|
||||
|
||||
args = ' '.join(args)
|
||||
if not args.strip():
|
||||
@ -86,7 +86,7 @@ def _eval(irc, source, args, locals_dict=None, pretty_print=False):
|
||||
locals_dict['args'] = args
|
||||
|
||||
log.info('(%s) Evaluating %r for %s', irc.name, args,
|
||||
irc.getHostmask(source))
|
||||
irc.get_hostmask(source))
|
||||
|
||||
result = eval(args, globals(), locals_dict)
|
||||
|
||||
@ -97,7 +97,9 @@ def _eval(irc, source, args, locals_dict=None, pretty_print=False):
|
||||
if len(lines) > PPRINT_MAX_LINES:
|
||||
irc.reply('Suppressing %s more line(s) of output.' % (len(lines) - PPRINT_MAX_LINES))
|
||||
else:
|
||||
irc.reply(repr(result))
|
||||
# Purposely disable text wrapping so results are cut instead of potentially flooding;
|
||||
# 'peval' is specifically designed to work around that.
|
||||
irc.reply(repr(result), wrap=False)
|
||||
|
||||
utils.add_cmd(_eval, 'eval')
|
||||
|
||||
@ -135,26 +137,6 @@ def pieval(irc, source, args):
|
||||
"""
|
||||
_eval(irc, source, args, locals_dict=exec_locals_dict, pretty_print=True)
|
||||
|
||||
@utils.add_cmd
|
||||
def raw(irc, source, args):
|
||||
"""<text>
|
||||
|
||||
Admin-only. Sends raw text to the uplink IRC server.
|
||||
|
||||
\x02**WARNING: THIS CAN BREAK YOUR NETWORK IF USED IMPROPERLY!**\x02"""
|
||||
permissions.checkPermissions(irc, source, ['exec.raw'])
|
||||
|
||||
args = ' '.join(args)
|
||||
if not args.strip():
|
||||
irc.reply('No text entered!')
|
||||
return
|
||||
|
||||
log.debug('(%s) Sending raw text %r to IRC for %s', irc.name, args,
|
||||
irc.getHostmask(source))
|
||||
irc.send(args)
|
||||
|
||||
irc.reply("Done.")
|
||||
|
||||
@utils.add_cmd
|
||||
def inject(irc, source, args):
|
||||
"""<text>
|
||||
@ -162,7 +144,7 @@ def inject(irc, source, args):
|
||||
Admin-only. Injects raw text into the running PyLink protocol module, replying with the hook data returned.
|
||||
|
||||
\x02**WARNING: THIS CAN BREAK YOUR NETWORK IF USED IMPROPERLY!**\x02"""
|
||||
permissions.checkPermissions(irc, source, ['exec.inject'])
|
||||
permissions.check_permissions(irc, source, ['exec.inject'])
|
||||
|
||||
args = ' '.join(args)
|
||||
if not args.strip():
|
||||
@ -170,5 +152,25 @@ def inject(irc, source, args):
|
||||
return
|
||||
|
||||
log.info('(%s) Injecting raw text %r into protocol module for %s', irc.name,
|
||||
args, irc.getHostmask(source))
|
||||
irc.reply(irc.runline(args))
|
||||
args, irc.get_hostmask(source))
|
||||
irc.reply(repr(irc.parse_irc_command(args)))
|
||||
|
||||
@utils.add_cmd
|
||||
def threadinfo(irc, source, args):
|
||||
"""takes no arguments.
|
||||
|
||||
Lists all threads currently present in this PyLink instance."""
|
||||
permissions.check_permissions(irc, source, ['exec.threadinfo'])
|
||||
|
||||
for t in sorted(threading.enumerate(), key=lambda t: t.name.lower()):
|
||||
name = t.name
|
||||
# Unnamed threads are something we want to avoid throughout PyLink.
|
||||
if name.startswith('Thread-'):
|
||||
name = '\x0305%s\x03' % t.name
|
||||
# Also VERY bad: remaining threads for networks not in the networks index anymore!
|
||||
elif name.startswith(('Listener for', 'Ping timer loop for', 'Queue thread for')) and name.rsplit(" ", 1)[-1] not in world.networkobjects:
|
||||
name = '\x0304%s\x03' % t.name
|
||||
|
||||
irc.reply('\x02%s\x02[%s]: daemon=%s; alive=%s' % (name, t.ident, t.daemon, t.is_alive()), private=True)
|
||||
|
||||
irc.reply("Total of %s threads." % threading.active_count())
|
||||
|
@ -12,7 +12,7 @@ def handle_fantasy(irc, source, command, args):
|
||||
channel = args['target']
|
||||
orig_text = args['text']
|
||||
|
||||
if utils.isChannel(channel) and not irc.isInternalClient(source):
|
||||
if irc.is_channel(channel) and not irc.is_internal_client(source):
|
||||
# The following conditions must be met for an incoming message for
|
||||
# fantasy to trigger:
|
||||
# 1) The message target is a channel.
|
||||
@ -29,7 +29,7 @@ def handle_fantasy(irc, source, command, args):
|
||||
# 2) The global "pylink::respond_to_nick" option
|
||||
# 3) The (deprecated) global "bot::respondtonick" option.
|
||||
respondtonick = conf.conf.get(botname, {}).get('respond_to_nick',
|
||||
conf.conf['pylink'].get("respond_to_nick", conf.conf['bot'].get("respondtonick")))
|
||||
conf.conf['pylink'].get("respond_to_nick", conf.conf['pylink'].get("respondtonick")))
|
||||
|
||||
log.debug('(%s) fantasy: checking bot %s', irc.name, botname)
|
||||
servuid = sbot.uids.get(irc.name)
|
||||
@ -38,11 +38,11 @@ def handle_fantasy(irc, source, command, args):
|
||||
# Look up a string prefix for this bot in either its own configuration block, or
|
||||
# in bot::prefixes::<botname>.
|
||||
prefixes = [conf.conf.get(botname, {}).get('prefix',
|
||||
conf.conf['bot'].get('prefixes', {}).get(botname))]
|
||||
conf.conf['pylink'].get('prefixes', {}).get(botname))]
|
||||
|
||||
# If responding to nick is enabled, add variations of the current nick
|
||||
# to the prefix list: "<nick>," and "<nick>:"
|
||||
nick = irc.toLower(irc.users[servuid].nick)
|
||||
nick = irc.to_lower(irc.users[servuid].nick)
|
||||
|
||||
nick_prefixes = [nick+',', nick+':']
|
||||
if respondtonick:
|
||||
@ -52,7 +52,7 @@ def handle_fantasy(irc, source, command, args):
|
||||
# No prefixes were set, so skip.
|
||||
continue
|
||||
|
||||
lowered_text = irc.toLower(orig_text)
|
||||
lowered_text = irc.to_lower(orig_text)
|
||||
for prefix in filter(None, prefixes): # Cycle through the prefixes list we finished with.
|
||||
if lowered_text.startswith(prefix):
|
||||
|
||||
|
@ -1,17 +1,14 @@
|
||||
"""
|
||||
games.py: Create a bot that provides game functionality (dice, 8ball, etc).
|
||||
games.py: Creates a bot providing a few simple games.
|
||||
"""
|
||||
import random
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from pylinkirc import utils
|
||||
from pylinkirc.log import log
|
||||
|
||||
mydesc = "The \x02Games\x02 plugin provides simple games for IRC."
|
||||
|
||||
gameclient = utils.registerService("Games", manipulatable=True, desc=mydesc)
|
||||
gameclient = utils.register_service("Games", default_nick="Games", manipulatable=True, desc=mydesc)
|
||||
reply = gameclient.reply # TODO find a better syntax for ServiceBot.reply()
|
||||
error = gameclient.error # TODO find a better syntax for ServiceBot.error()
|
||||
# commands
|
||||
@ -43,8 +40,7 @@ def dice(irc, source, args):
|
||||
s = 'You rolled %s: %s (total: %s)' % (args[0], ' '.join([str(x) for x in results]), sum(results))
|
||||
reply(irc, s)
|
||||
|
||||
gameclient.add_cmd(dice, 'd')
|
||||
gameclient.add_cmd(dice, featured=True)
|
||||
gameclient.add_cmd(dice, aliases=('d'), featured=True)
|
||||
|
||||
eightball_responses = ["It is certain.",
|
||||
"It is decidedly so.",
|
||||
@ -72,55 +68,7 @@ def eightball(irc, source, args):
|
||||
Asks the Magic 8-ball a question.
|
||||
"""
|
||||
reply(irc, random.choice(eightball_responses))
|
||||
gameclient.add_cmd(eightball, featured=True)
|
||||
gameclient.add_cmd(eightball, '8ball')
|
||||
gameclient.add_cmd(eightball, '8b')
|
||||
|
||||
def fml(irc, source, args):
|
||||
"""[<id>]
|
||||
|
||||
Displays an entry from fmylife.com. If <id> is not given, fetch a random entry from the API."""
|
||||
try:
|
||||
query = args[0]
|
||||
except IndexError:
|
||||
# Get a random FML from the API.
|
||||
query = 'random'
|
||||
|
||||
# TODO: configurable language?
|
||||
url = ('http://api.betacie.com/view/%s/nocomment'
|
||||
'?key=4be9c43fc03fe&language=en' % query)
|
||||
try:
|
||||
data = urllib.request.urlopen(url).read()
|
||||
except urllib.error as e:
|
||||
error(irc, '%s' % e)
|
||||
return
|
||||
|
||||
tree = ElementTree.fromstring(data.decode('utf-8'))
|
||||
tree = tree.find('items/item')
|
||||
|
||||
try:
|
||||
category = tree.find('category').text
|
||||
text = tree.find('text').text
|
||||
fmlid = tree.attrib['id']
|
||||
url = tree.find('short_url').text
|
||||
except AttributeError as e:
|
||||
log.debug("games.FML: Error fetching FML %s from URL %s: %s",
|
||||
query, url, e)
|
||||
error(irc, "That FML does not exist or there was an error "
|
||||
"fetching data from the API.")
|
||||
return
|
||||
|
||||
if not fmlid:
|
||||
error(irc, "That FML does not exist.")
|
||||
return
|
||||
|
||||
# TODO: customizable formatting
|
||||
votes = "\x02[Agreed: %s / Deserved: %s]\x02" % \
|
||||
(tree.find('agree').text, tree.find('deserved').text)
|
||||
s = '\x02#%s [%s]\x02: %s - %s \x02<\x0311%s\x03>\x02' % \
|
||||
(fmlid, category, text, votes, url)
|
||||
reply(irc, s)
|
||||
gameclient.add_cmd(fml, featured=True)
|
||||
gameclient.add_cmd(eightball, featured=True, aliases=('8ball', '8b'))
|
||||
|
||||
def die(irc=None):
|
||||
utils.unregisterService('games')
|
||||
utils.unregister_service('games')
|
||||
|
@ -13,24 +13,50 @@ def g(irc, source, args):
|
||||
|
||||
Sends out a Instance-wide notice.
|
||||
"""
|
||||
permissions.checkPermissions(irc, source, ["global.global"])
|
||||
message = " ".join(args)
|
||||
permissions.check_permissions(irc, source, ["global.global"])
|
||||
message = " ".join(args).strip()
|
||||
|
||||
if not message:
|
||||
irc.error("Refusing to send an empty message.")
|
||||
return
|
||||
|
||||
global_conf = conf.conf.get('global') or {}
|
||||
template = string.Template(global_conf.get('format', DEFAULT_FORMAT))
|
||||
|
||||
for name, ircd in world.networkobjects.items():
|
||||
exempt_channels = set(global_conf.get('exempt_channels', set()))
|
||||
|
||||
netcount = 0
|
||||
chancount = 0
|
||||
for netname, ircd in world.networkobjects.items():
|
||||
if ircd.connected.is_set(): # Only attempt to send to connected networks
|
||||
netcount += 1
|
||||
for channel in ircd.pseudoclient.channels:
|
||||
subst = {'sender': irc.getFriendlyName(source),
|
||||
|
||||
local_exempt_channels = exempt_channels | set(ircd.serverdata.get('global_exempt_channels', set()))
|
||||
|
||||
skip = False
|
||||
for exempt in local_exempt_channels:
|
||||
if irc.match_text(exempt, channel):
|
||||
log.debug('global: Skipping channel %s%s for exempt %r', netname, channel, exempt)
|
||||
skip = True
|
||||
break
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
subst = {'sender': irc.get_friendly_name(source),
|
||||
'network': irc.name,
|
||||
'fullnetwork': irc.getFullNetworkName(),
|
||||
'fullnetwork': irc.get_full_network_name(),
|
||||
'current_channel': channel,
|
||||
'current_network': ircd.name,
|
||||
'current_fullnetwork': ircd.getFullNetworkName(),
|
||||
'current_network': netname,
|
||||
'current_fullnetwork': ircd.get_full_network_name(),
|
||||
'text': message}
|
||||
|
||||
# Disable relaying or other plugins handling the global message.
|
||||
ircd.msg(channel, template.safe_substitute(subst), loopback=False)
|
||||
|
||||
chancount += 1
|
||||
|
||||
irc.reply('Done. Sent to %d channels across %d networks.' % (chancount, netcount))
|
||||
|
||||
utils.add_cmd(g, "global", featured=True)
|
||||
|
@ -16,7 +16,7 @@ def disconnect(irc, source, args):
|
||||
Disconnects the network <network>. When all networks are disconnected, PyLink will automatically exit.
|
||||
|
||||
To reconnect a network disconnected using this command, use REHASH to reload the networks list."""
|
||||
permissions.checkPermissions(irc, source, ['networks.disconnect'])
|
||||
permissions.check_permissions(irc, source, ['networks.disconnect'])
|
||||
try:
|
||||
netname = args[0]
|
||||
network = world.networkobjects[netname]
|
||||
@ -27,6 +27,7 @@ def disconnect(irc, source, args):
|
||||
irc.error('No such network "%s" (case sensitive).' % netname)
|
||||
return
|
||||
irc.reply("Done. If you want to reconnect this network, use the 'rehash' command.")
|
||||
log.info('Disconnecting network %r per %s', netname, irc.get_hostmask(source))
|
||||
|
||||
control.remove_network(network)
|
||||
|
||||
@ -36,7 +37,7 @@ def autoconnect(irc, source, args):
|
||||
|
||||
Sets the autoconnect time for <network> to <seconds>.
|
||||
You can disable autoconnect for a network by setting <seconds> to a negative value."""
|
||||
permissions.checkPermissions(irc, source, ['networks.autoconnect'])
|
||||
permissions.check_permissions(irc, source, ['networks.autoconnect'])
|
||||
try:
|
||||
netname = args[0]
|
||||
seconds = float(args[1])
|
||||
@ -54,23 +55,34 @@ def autoconnect(irc, source, args):
|
||||
irc.reply("Done.")
|
||||
|
||||
remote_parser = utils.IRCParser()
|
||||
remote_parser.add_argument('network')
|
||||
remote_parser.add_argument('--service', type=str, default='pylink')
|
||||
remote_parser.add_argument('network')
|
||||
remote_parser.add_argument('command', nargs=utils.IRCParser.REMAINDER)
|
||||
@utils.add_cmd
|
||||
def remote(irc, source, args):
|
||||
"""<network> [--service <service name>] <command>
|
||||
"""[--service <service name>] <network> <command>
|
||||
|
||||
Runs <command> on the remote network <network>. Plugin responses sent using irc.reply() are
|
||||
supported and returned here, but others are dropped due to protocol limitations."""
|
||||
permissions.checkPermissions(irc, source, ['networks.remote'])
|
||||
|
||||
args = remote_parser.parse_args(args)
|
||||
if not args.command:
|
||||
irc.error('No command given!')
|
||||
return
|
||||
|
||||
netname = args.network
|
||||
|
||||
if not args.command:
|
||||
irc.error("No command specified!")
|
||||
return
|
||||
permissions.check_permissions(irc, source, [
|
||||
# Quite a few permissions are allowed. 'networks.remote' is the global permission,
|
||||
'networks.remote',
|
||||
# networks.remote.<network> allows running any command on a specific network,
|
||||
'networks.remote.%s' % netname,
|
||||
# networks.remote.<network>.<service> allows running any command on the given service on a
|
||||
# specific network,
|
||||
'networks.remote.%s.%s' % (netname, args.service),
|
||||
# and networks.remote.<network>.<service>.<command> narrows this further into which command
|
||||
# can be used.
|
||||
'networks.remote.%s.%s.%s' % (netname, args.service, args.command[0])
|
||||
])
|
||||
|
||||
# XXX: things like 'remote network1 remote network2 echo hi' will crash PyLink if the source network is network1...
|
||||
global REMOTE_IN_USE
|
||||
@ -101,6 +113,10 @@ def remote(irc, source, args):
|
||||
irc.error('Network %r is not connected.' % netname)
|
||||
REMOTE_IN_USE.clear()
|
||||
return
|
||||
elif not world.services[args.service].uids.get(netname):
|
||||
irc.error('The requested service %r is not available on %r.' % (args.service, netname))
|
||||
REMOTE_IN_USE.clear()
|
||||
return
|
||||
|
||||
# Force remoteirc.called_in to something private in order to prevent
|
||||
# accidental information leakage from replies.
|
||||
@ -130,7 +146,7 @@ def remote(irc, source, args):
|
||||
|
||||
old_reply = remoteirc._reply
|
||||
|
||||
with remoteirc.reply_lock:
|
||||
with remoteirc._reply_lock:
|
||||
try: # Remotely call the command (use the PyLink client as a dummy user).
|
||||
# Override the remote irc.reply() to send replies HERE.
|
||||
log.debug('(%s) networks.remote: overriding reply() of IRC object %s', irc.name, netname)
|
||||
@ -154,14 +170,14 @@ def reloadproto(irc, source, args):
|
||||
"""<protocol module name>
|
||||
|
||||
Reloads the given protocol module without restart. You will have to manually disconnect and reconnect any network using the module for changes to apply."""
|
||||
permissions.checkPermissions(irc, source, ['networks.reloadproto'])
|
||||
permissions.check_permissions(irc, source, ['networks.reloadproto'])
|
||||
try:
|
||||
name = args[0]
|
||||
except IndexError:
|
||||
irc.error('Not enough arguments (needs 1: protocol module name)')
|
||||
return
|
||||
|
||||
proto = utils.getProtocolModule(name)
|
||||
proto = utils._get_protocol_module(name)
|
||||
importlib.reload(proto)
|
||||
|
||||
irc.reply("Done. You will have to manually disconnect and reconnect any network using the %r module for changes to apply." % name)
|
||||
|
@ -1,98 +1,353 @@
|
||||
"""
|
||||
opercmds.py: Provides a subset of network management commands.
|
||||
"""
|
||||
from pylinkirc import utils
|
||||
import argparse
|
||||
|
||||
from pylinkirc import utils, world
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.coremods import permissions
|
||||
|
||||
@utils.add_cmd
|
||||
def checkban(irc, source, args):
|
||||
"""<banmask (nick!user@host or user@host)> [<nick or hostmask to check>]
|
||||
# Having a hard limit here is sensible because otherwise it can flood the client or server off.
|
||||
CHECKBAN_MAX_RESULTS = 200
|
||||
|
||||
Oper only. If a nick or hostmask is given, return whether the given banmask will match it. Otherwise, returns a list of connected users that would be affected by such a ban, up to 50 results."""
|
||||
permissions.checkPermissions(irc, source, ['opercmds.checkban'])
|
||||
def _checkban_positiveint(value):
|
||||
value = int(value)
|
||||
if value <= 0 or value > CHECKBAN_MAX_RESULTS:
|
||||
raise argparse.ArgumentTypeError("%s is not a positive integer between 1 and %s." % (value, CHECKBAN_MAX_RESULTS))
|
||||
return value
|
||||
|
||||
try:
|
||||
banmask = args[0]
|
||||
except IndexError:
|
||||
irc.error("Not enough arguments. Needs 1-2: banmask, nick or hostmask to check (optional).")
|
||||
return
|
||||
checkban_parser = utils.IRCParser()
|
||||
checkban_parser.add_argument('banmask')
|
||||
checkban_parser.add_argument('target', nargs='?', default='')
|
||||
checkban_parser.add_argument('--channel', default='')
|
||||
checkban_parser.add_argument('--maxresults', type=_checkban_positiveint, default=50)
|
||||
|
||||
try:
|
||||
targetmask = args[1]
|
||||
except IndexError:
|
||||
# No hostmask was given, return a list of affected users.
|
||||
def checkban(irc, source, args, use_regex=False):
|
||||
"""<banmask> [<target nick or hostmask>] [--channel #channel] [--maxresults <num>]
|
||||
|
||||
irc.msg(source, "Checking for hosts that match \x02%s\x02:" % banmask, notice=True)
|
||||
CHECKBAN provides a ban checker command based on nick!user@host masks, user@host masks, and
|
||||
PyLink extended targets.
|
||||
|
||||
If a target nick or hostmask is given, this command returns whether the given banmask will match it.
|
||||
Otherwise, it will display a list of connected users matching the banmask.
|
||||
|
||||
If the --channel argument is given without a target mask, the returned results will only
|
||||
include users in the given channel.
|
||||
|
||||
The --maxresults option configures how many responses will be shown."""
|
||||
permissions.check_permissions(irc, source, ['opercmds.checkban'])
|
||||
|
||||
args = checkban_parser.parse_args(args)
|
||||
if not args.target:
|
||||
# No hostmask was given, return a list of matched users.
|
||||
results = 0
|
||||
for uid, userobj in irc.users.copy().items():
|
||||
if irc.matchHost(banmask, uid):
|
||||
if results < 50: # XXX rather arbitrary limit
|
||||
s = "\x02%s\x02 (%s@%s) [%s] {\x02%s\x02}" % (userobj.nick, userobj.ident,
|
||||
userobj.host, userobj.realname, irc.getFriendlyName(irc.getServer(uid)))
|
||||
|
||||
# Always reply in private to prevent information leaks.
|
||||
irc.reply(s, private=True)
|
||||
results += 1
|
||||
userlist_func = irc.match_all_re if use_regex else irc.match_all
|
||||
irc.reply("Checking for hosts that match \x02%s\x02:" % args.banmask, private=True)
|
||||
for uid in userlist_func(args.banmask, channel=args.channel):
|
||||
if results < args.maxresults:
|
||||
userobj = irc.users[uid]
|
||||
s = "\x02%s\x02 (%s@%s) [%s] {\x02%s\x02}" % (userobj.nick, userobj.ident,
|
||||
userobj.host, userobj.realname, irc.get_friendly_name(irc.get_server(uid)))
|
||||
|
||||
# Always reply in private to prevent information leaks.
|
||||
irc.reply(s, private=True)
|
||||
results += 1
|
||||
else:
|
||||
if results:
|
||||
irc.msg(source, "\x02%s\x02 out of \x02%s\x02 results shown." %
|
||||
(min([results, 50]), results), notice=True)
|
||||
irc.reply("\x02%s\x02 out of \x02%s\x02 results shown." %
|
||||
(min([results, args.maxresults]), results), private=True)
|
||||
else:
|
||||
irc.msg(source, "No results found.", notice=True)
|
||||
irc.reply("No results found.", private=True)
|
||||
else:
|
||||
# Target can be both a nick (of an online user) or a hostmask. irc.matchHost() handles this
|
||||
# Target can be both a nick (of an online user) or a hostmask. irc.match_host() handles this
|
||||
# automatically.
|
||||
if irc.matchHost(banmask, targetmask):
|
||||
irc.reply('Yes, \x02%s\x02 matches \x02%s\x02.' % (targetmask, banmask))
|
||||
if irc.match_host(args.banmask, args.target):
|
||||
irc.reply('Yes, \x02%s\x02 matches \x02%s\x02.' % (args.target, args.banmask))
|
||||
else:
|
||||
irc.reply('No, \x02%s\x02 does not match \x02%s\x02.' % (targetmask, banmask))
|
||||
irc.reply('No, \x02%s\x02 does not match \x02%s\x02.' % (args.target, args.banmask))
|
||||
utils.add_cmd(checkban, aliases=('cban', 'trace'))
|
||||
|
||||
def checkbanre(irc, source, args):
|
||||
"""<regular expression> [<target nick or hostmask>] [--channel #channel] [--maxresults <num>]
|
||||
|
||||
CHECKBANRE provides a ban checker command based on regular expressions matched against
|
||||
users' "nick!user@host [gecos]" mask.
|
||||
|
||||
If a target nick or hostmask is given, this command returns whether the given banmask will match it.
|
||||
Otherwise, it will display a list of connected users matching the banmask.
|
||||
|
||||
If the --channel argument is given without a target mask, the returned results will only
|
||||
include users in the given channel.
|
||||
|
||||
The --maxresults option configures how many responses will be shown."""
|
||||
permissions.check_permissions(irc, source, ['opercmds.checkban.re'])
|
||||
return checkban(irc, source, args, use_regex=True)
|
||||
|
||||
utils.add_cmd(checkbanre, aliases=('crban',))
|
||||
|
||||
massban_parser = utils.IRCParser()
|
||||
massban_parser.add_argument('channel')
|
||||
massban_parser.add_argument('banmask')
|
||||
# Regarding default ban reason: it's a good idea not to leave in the caller to prevent retaliation...
|
||||
massban_parser.add_argument('reason', nargs='*', default=["User banned"])
|
||||
massban_parser.add_argument('--quiet', '-q', action='store_true')
|
||||
massban_parser.add_argument('--force', '-f', action='store_true')
|
||||
massban_parser.add_argument('--include-opers', '-o', action='store_true')
|
||||
|
||||
def massban(irc, source, args, use_regex=False):
|
||||
"""<channel> <banmask / exttarget> [<kick reason>] [--quiet/-q] [--force/-f] [--include-opers/-o]
|
||||
|
||||
Applies (i.e. kicks affected users) the given PyLink banmask on the specified channel.
|
||||
|
||||
The --quiet option can also be given to mass-mute the given user on networks where this is supported
|
||||
(currently ts6, unreal, and inspircd). No kicks will be sent in this case.
|
||||
|
||||
By default, this command will ignore opers. This behaviour can be suppressed using the --include-opers option.
|
||||
|
||||
Relay CLAIM checking is used on Relay channels if it is enabled; use the --force option
|
||||
to override this if needed."""
|
||||
permissions.check_permissions(irc, source, ['opercmds.massban'])
|
||||
|
||||
args = massban_parser.parse_args(args)
|
||||
reason = ' '.join(args.reason)
|
||||
|
||||
if args.force:
|
||||
permissions.check_permissions(irc, source, ['opercmds.massban.force'])
|
||||
|
||||
if args.channel not in irc.channels:
|
||||
irc.error("Unknown channel %r." % args.channel)
|
||||
return
|
||||
elif 'relay' in world.plugins and (not world.plugins['relay'].check_claim(irc, args.channel, source)) and (not args.force):
|
||||
irc.error("You do not have access to set bans in %s. Ask someone to op you or use the --force option." % args.channel)
|
||||
return
|
||||
|
||||
results = 0
|
||||
|
||||
userlist_func = irc.match_all_re if use_regex else irc.match_all
|
||||
for uid in userlist_func(args.banmask, channel=args.channel):
|
||||
|
||||
if irc.is_oper(uid) and not args.include_opers:
|
||||
irc.reply('Skipping banning \x02%s\x02 because they are opered.' % irc.users[uid].nick)
|
||||
continue
|
||||
elif irc.get_service_bot(uid):
|
||||
irc.reply('Skipping banning \x02%s\x02 because it is a service client.' % irc.users[uid].nick)
|
||||
continue
|
||||
|
||||
# Remove the target's access before banning them.
|
||||
bans = [('-%s' % irc.cmodes[prefix], uid) for prefix in irc.channels[args.channel].get_prefix_modes(uid) if prefix in irc.cmodes]
|
||||
|
||||
# Then, add the actual ban.
|
||||
bans += [irc.make_channel_ban(uid, ban_type='quiet' if args.quiet else 'ban')]
|
||||
irc.mode(irc.pseudoclient.uid, args.channel, bans)
|
||||
|
||||
try:
|
||||
irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_MASSBAN',
|
||||
{'target': args.channel, 'modes': bans, 'parse_as': 'MODE'}])
|
||||
except:
|
||||
log.exception('(%s) Failed to send process massban hook; some bans may have not '
|
||||
'been sent to plugins / relay networks!', irc.name)
|
||||
|
||||
if not args.quiet:
|
||||
irc.kick(irc.pseudoclient.uid, args.channel, uid, reason)
|
||||
|
||||
# XXX: this better not be blocking...
|
||||
try:
|
||||
irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_MASSKICK',
|
||||
{'channel': args.channel, 'target': uid, 'text': reason, 'parse_as': 'KICK'}])
|
||||
|
||||
except:
|
||||
log.exception('(%s) Failed to send process massban hook; some kicks may have not '
|
||||
'been sent to plugins / relay networks!', irc.name)
|
||||
|
||||
results += 1
|
||||
else:
|
||||
irc.reply('Banned %s users on %r.' % (results, args.channel))
|
||||
log.info('(%s) Ran massban%s for %s on %s (%s user(s) removed)', irc.name, 're' if use_regex else '',
|
||||
irc.get_hostmask(source), args.channel, results)
|
||||
utils.add_cmd(massban, aliases=('mban',))
|
||||
|
||||
def massbanre(irc, source, args):
|
||||
"""<channel> <regular expression> [<kick reason>] [--quiet/-q] [--include-opers/-o]
|
||||
|
||||
Bans users on the specified channel whose "nick!user@host [gecos]" mask matches the given Python-style regular expression.
|
||||
(https://docs.python.org/3/library/re.html#regular-expression-syntax describes supported syntax)
|
||||
|
||||
The --quiet option can also be given to mass-mute the given user on networks where this is supported
|
||||
(currently ts6, unreal, and inspircd). No kicks will be sent in this case.
|
||||
|
||||
By default, this command will ignore opers. This behaviour can be suppressed using the --include-opers option.
|
||||
|
||||
\x02Be careful when using this command, as it is easy to make mistakes with regex. Use 'checkbanre'
|
||||
to check your bans first!\x02
|
||||
"""
|
||||
permissions.check_permissions(irc, source, ['opercmds.massban.re'])
|
||||
return massban(irc, source, args, use_regex=True)
|
||||
|
||||
utils.add_cmd(massbanre, aliases=('rban',))
|
||||
|
||||
masskill_parser = utils.IRCParser()
|
||||
masskill_parser.add_argument('banmask')
|
||||
# Regarding default ban reason: it's a good idea not to leave in the caller to prevent retaliation...
|
||||
masskill_parser.add_argument('reason', nargs='*', default=["User banned"], type=str)
|
||||
masskill_parser.add_argument('--akill', '-ak', action='store_true')
|
||||
masskill_parser.add_argument('--force-kb', '-f', action='store_true')
|
||||
masskill_parser.add_argument('--include-opers', '-o', action='store_true')
|
||||
|
||||
def masskill(irc, source, args, use_regex=False):
|
||||
"""<banmask / exttarget> [<kill/ban reason>] [--akill/ak] [--force-kb/-f] [--include-opers/-o]
|
||||
|
||||
Kills all users matching the given PyLink banmask.
|
||||
|
||||
The --akill option can also be given to convert kills to akills, which expire after 7 days.
|
||||
|
||||
For relay users, attempts to kill are forwarded as a kickban to every channel where the calling user
|
||||
meets claim requirements to set a ban (i.e. this is true if you are opped, if your network is in claim list, etc.;
|
||||
see "help CLAIM" for more specific rules). This can also be extended to all shared channels
|
||||
the user is in using the --force-kb option (we hope this feature is only used for good).
|
||||
|
||||
By default, this command will ignore opers. This behaviour can be suppressed using the --include-opers option.
|
||||
|
||||
To properly kill abusers on another network, combine this command with the 'remote' command in the
|
||||
'networks' plugin and adjust your banmasks accordingly."""
|
||||
permissions.check_permissions(irc, source, ['opercmds.masskill'])
|
||||
|
||||
args = masskill_parser.parse_args(args)
|
||||
|
||||
if args.force_kb:
|
||||
permissions.check_permissions(irc, source, ['opercmds.masskill.force'])
|
||||
|
||||
reason = ' '.join(args.reason)
|
||||
|
||||
results = killed = 0
|
||||
|
||||
userlist_func = irc.match_all_re if use_regex else irc.match_all
|
||||
|
||||
seen_users = set()
|
||||
for uid in userlist_func(args.banmask):
|
||||
userobj = irc.users[uid]
|
||||
|
||||
if irc.is_oper(uid) and not args.include_opers:
|
||||
irc.reply('Skipping killing \x02%s\x02 because they are opered.' % userobj.nick)
|
||||
continue
|
||||
elif irc.get_service_bot(uid):
|
||||
irc.reply('Skipping killing \x02%s\x02 because it is a service client.' % userobj.nick)
|
||||
continue
|
||||
|
||||
relay = world.plugins.get('relay')
|
||||
if relay and hasattr(userobj, 'remote'):
|
||||
# For relay users, forward kill attempts as kickban because we don't want networks k-lining each others' users.
|
||||
bans = [irc.make_channel_ban(uid)]
|
||||
for channel in userobj.channels.copy(): # Look in which channels the user appears to be in locally
|
||||
|
||||
if (args.force_kb or relay.check_claim(irc, channel, source)):
|
||||
irc.mode(irc.pseudoclient.uid, channel, bans)
|
||||
irc.kick(irc.pseudoclient.uid, channel, uid, reason)
|
||||
|
||||
# XXX: code duplication with massban.
|
||||
try:
|
||||
irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_MASSKILL_BAN',
|
||||
{'target': channel, 'modes': bans, 'parse_as': 'MODE'}])
|
||||
irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_MASSKILL_KICK',
|
||||
{'channel': channel, 'target': uid, 'text': reason, 'parse_as': 'KICK'}])
|
||||
except:
|
||||
log.exception('(%s) Failed to send process massban hook; some kickbans may have not '
|
||||
'been sent to plugins / relay networks!', irc.name)
|
||||
|
||||
if uid not in seen_users: # Don't count users multiple times on different channels
|
||||
killed += 1
|
||||
else:
|
||||
irc.reply("Not kicking \x02%s\x02 from \x02%s\x02 because you don't have CLAIM access. If this is "
|
||||
"another network's channel, ask someone to op you or use the --force-kb option." % (userobj.nick, channel))
|
||||
else:
|
||||
if args.akill: # TODO: configurable length via strings such as "2w3d5h6m3s" - though month and minute clash this way?
|
||||
if not (userobj.realhost or userobj.ip):
|
||||
irc.reply("Skipping akill on %s because PyLink doesn't know the real host." % irc.get_hostmask(uid))
|
||||
continue
|
||||
irc.set_server_ban(irc.pseudoclient.uid, 604800, host=userobj.realhost or userobj.ip or userobj.host, reason=reason)
|
||||
else:
|
||||
irc.kill(irc.pseudoclient.uid, uid, reason)
|
||||
try:
|
||||
irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_MASSKILL',
|
||||
{'target': uid, 'parse_as': 'KILL', 'userdata': userobj, 'text': reason}])
|
||||
except:
|
||||
log.exception('(%s) Failed to send process massban hook; some kickbans may have not '
|
||||
'been sent to plugins / relay networks!', irc.name)
|
||||
killed += 1
|
||||
results += 1
|
||||
seen_users.add(uid)
|
||||
else:
|
||||
log.info('(%s) Ran masskill%s for %s (%s/%s user(s) removed)', irc.name, 're' if use_regex else '',
|
||||
irc.get_hostmask(source), killed, results)
|
||||
irc.reply('Masskilled %s/%s users.' % (killed, results))
|
||||
utils.add_cmd(masskill, aliases=('mkill',))
|
||||
|
||||
def masskillre(irc, source, args):
|
||||
"""<regular expression> [<kill/ban reason>] [--akill/ak] [--force-kb/-f] [--include-opers/-o]
|
||||
|
||||
Kills all users whose "nick!user@host [gecos]" mask matches the given Python-style regular expression.
|
||||
(https://docs.python.org/3/library/re.html#regular-expression-syntax describes supported syntax)
|
||||
|
||||
The --akill option can also be given to convert kills to akills that expire after 7 days.
|
||||
|
||||
For relay users, attempts to kill are forwarded as a kickban to every channel where the calling user
|
||||
meets claim requirements to set a ban (i.e. this is true if you are opped, if your network is in claim list, etc.;
|
||||
see "help CLAIM" for more specific rules). This can also be extended to all shared channels
|
||||
the user is in using the --force-kb option (we hope this feature is only used for good).
|
||||
|
||||
By default, this command will ignore opers. This behaviour can be suppressed using the --include-opers option.
|
||||
|
||||
\x02Be careful when using this command, as it is easy to make mistakes with regex. Use 'checkbanre'
|
||||
to check your bans first!\x02
|
||||
|
||||
"""
|
||||
permissions.check_permissions(irc, source, ['opercmds.masskill.re'])
|
||||
return masskill(irc, source, args, use_regex=True)
|
||||
|
||||
utils.add_cmd(masskillre, aliases=('rkill',))
|
||||
|
||||
@utils.add_cmd
|
||||
def jupe(irc, source, args):
|
||||
"""<server> [<reason>]
|
||||
|
||||
Admin only, jupes the given server."""
|
||||
Jupes the given server."""
|
||||
|
||||
# Check that the caller is either opered or logged in as admin.
|
||||
permissions.checkPermissions(irc, source, ['opercmds.jupe'])
|
||||
permissions.check_permissions(irc, source, ['opercmds.jupe'])
|
||||
|
||||
try:
|
||||
servername = args[0]
|
||||
reason = ' '.join(args[1:]) or "No reason given"
|
||||
desc = "Juped by %s: [%s]" % (irc.getHostmask(source), reason)
|
||||
desc = "Juped by %s: [%s]" % (irc.get_hostmask(source), reason)
|
||||
except IndexError:
|
||||
irc.error('Not enough arguments. Needs 1-2: servername, reason (optional).')
|
||||
return
|
||||
|
||||
if not utils.isServerName(servername):
|
||||
irc.error("Invalid server name '%s'." % servername)
|
||||
if not irc.is_server_name(servername):
|
||||
irc.error("Invalid server name %r." % servername)
|
||||
return
|
||||
|
||||
sid = irc.proto.spawnServer(servername, desc=desc)
|
||||
sid = irc.spawn_server(servername, desc=desc)
|
||||
|
||||
irc.callHooks([irc.pseudoclient.uid, 'OPERCMDS_SPAWNSERVER',
|
||||
irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_SPAWNSERVER',
|
||||
{'name': servername, 'sid': sid, 'text': desc}])
|
||||
|
||||
irc.reply("Done.")
|
||||
|
||||
|
||||
@utils.add_cmd
|
||||
def kick(irc, source, args):
|
||||
"""<channel> <user> [<reason>]
|
||||
|
||||
Admin only. Kicks <user> from the specified channel."""
|
||||
permissions.checkPermissions(irc, source, ['opercmds.kick'])
|
||||
Kicks <user> from the specified channel."""
|
||||
permissions.check_permissions(irc, source, ['opercmds.kick'])
|
||||
try:
|
||||
channel = irc.toLower(args[0])
|
||||
channel = args[0]
|
||||
target = args[1]
|
||||
reason = ' '.join(args[2:])
|
||||
except IndexError:
|
||||
irc.error("Not enough arguments. Needs 2-3: channel, target, reason (optional).")
|
||||
return
|
||||
|
||||
targetu = irc.nickToUid(target)
|
||||
targetu = irc.nick_to_uid(target)
|
||||
|
||||
if channel not in irc.channels: # KICK only works on channels that exist.
|
||||
irc.error("Unknown channel %r." % channel)
|
||||
@ -100,21 +355,21 @@ def kick(irc, source, args):
|
||||
|
||||
if not targetu:
|
||||
# Whatever we were told to kick doesn't exist!
|
||||
irc.error("No such target nick '%s'." % target)
|
||||
irc.error("No such target nick %r." % target)
|
||||
return
|
||||
|
||||
sender = irc.pseudoclient.uid
|
||||
irc.proto.kick(sender, channel, targetu, reason)
|
||||
irc.kick(sender, channel, targetu, reason)
|
||||
irc.reply("Done.")
|
||||
irc.callHooks([sender, 'CHANCMDS_KICK', {'channel': channel, 'target': targetu,
|
||||
'text': reason, 'parse_as': 'KICK'}])
|
||||
irc.call_hooks([sender, 'OPERCMDS_KICK', {'channel': channel, 'target': targetu,
|
||||
'text': reason, 'parse_as': 'KICK'}])
|
||||
|
||||
@utils.add_cmd
|
||||
def kill(irc, source, args):
|
||||
"""<target> [<reason>]
|
||||
|
||||
Admin only. Kills the given target."""
|
||||
permissions.checkPermissions(irc, source, ['opercmds.kill'])
|
||||
Kills the given target."""
|
||||
permissions.check_permissions(irc, source, ['opercmds.kill'])
|
||||
try:
|
||||
target = args[0]
|
||||
reason = ' '.join(args[1:])
|
||||
@ -124,31 +379,39 @@ def kill(irc, source, args):
|
||||
|
||||
# Convert the source and target nicks to UIDs.
|
||||
sender = irc.pseudoclient.uid
|
||||
targetu = irc.nickToUid(target)
|
||||
targetu = irc.nick_to_uid(target)
|
||||
userdata = irc.users.get(targetu)
|
||||
|
||||
if targetu not in irc.users:
|
||||
# Whatever we were told to kick doesn't exist!
|
||||
irc.error("No such nick '%s'." % target)
|
||||
irc.error("No such nick %r." % target)
|
||||
return
|
||||
elif irc.pseudoclient.uid == targetu:
|
||||
irc.error("Cannot kill the main PyLink client!")
|
||||
return
|
||||
|
||||
irc.proto.kill(sender, targetu, reason)
|
||||
# Deliver a more complete kill reason if our target is a non-PyLink client.
|
||||
# We skip this for PyLink clients so that relayed kills don't get
|
||||
# "Killed (abc (...))" tacked on both here and by the receiving IRCd.
|
||||
if not irc.is_internal_client(targetu):
|
||||
reason = "Killed (%s (Requested by %s: %s))" % (
|
||||
irc.get_friendly_name(sender),
|
||||
irc.get_friendly_name(source),
|
||||
reason)
|
||||
|
||||
# Format the kill reason properly in hooks.
|
||||
reason = "Killed (%s (%s))" % (irc.getFriendlyName(sender), reason)
|
||||
irc.kill(sender, targetu, reason)
|
||||
|
||||
irc.reply("Done.")
|
||||
irc.callHooks([sender, 'CHANCMDS_KILL', {'target': targetu, 'text': reason,
|
||||
'userdata': userdata, 'parse_as': 'KILL'}])
|
||||
irc.call_hooks([source, 'OPERCMDS_KILL', {'target': targetu, 'text': reason,
|
||||
'userdata': userdata, 'parse_as': 'KILL'}])
|
||||
|
||||
@utils.add_cmd
|
||||
def mode(irc, source, args):
|
||||
"""<channel> <modes>
|
||||
|
||||
Oper-only, sets modes <modes> on the target channel."""
|
||||
Sets the given modes on the target channel."""
|
||||
|
||||
# Check that the caller is either opered or logged in as admin.
|
||||
permissions.checkPermissions(irc, source, ['opercmds.mode'])
|
||||
permissions.check_permissions(irc, source, ['opercmds.mode'])
|
||||
|
||||
try:
|
||||
target, modes = args[0], args[1:]
|
||||
@ -157,14 +420,14 @@ def mode(irc, source, args):
|
||||
return
|
||||
|
||||
if target not in irc.channels:
|
||||
irc.error("Unknown channel '%s'." % target)
|
||||
irc.error("Unknown channel %r." % target)
|
||||
return
|
||||
elif not modes:
|
||||
# No modes were given before parsing (i.e. mode list was blank).
|
||||
irc.error("No valid modes were given.")
|
||||
return
|
||||
|
||||
parsedmodes = irc.parseModes(target, modes)
|
||||
parsedmodes = irc.parse_modes(target, modes)
|
||||
|
||||
if not parsedmodes:
|
||||
# Modes were given but they failed to parse into anything meaningful.
|
||||
@ -173,10 +436,10 @@ def mode(irc, source, args):
|
||||
irc.error("No valid modes were given.")
|
||||
return
|
||||
|
||||
irc.proto.mode(irc.pseudoclient.uid, target, parsedmodes)
|
||||
irc.mode(irc.pseudoclient.uid, target, parsedmodes)
|
||||
|
||||
# Call the appropriate hooks for plugins like relay.
|
||||
irc.callHooks([irc.pseudoclient.uid, 'OPERCMDS_MODEOVERRIDE',
|
||||
irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_MODE',
|
||||
{'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}])
|
||||
|
||||
irc.reply("Done.")
|
||||
@ -185,8 +448,8 @@ def mode(irc, source, args):
|
||||
def topic(irc, source, args):
|
||||
"""<channel> <topic>
|
||||
|
||||
Admin only. Updates the topic in a channel."""
|
||||
permissions.checkPermissions(irc, source, ['opercmds.topic'])
|
||||
Changes the topic in a channel."""
|
||||
permissions.check_permissions(irc, source, ['opercmds.topic'])
|
||||
try:
|
||||
channel = args[0]
|
||||
topic = ' '.join(args[1:])
|
||||
@ -198,9 +461,49 @@ def topic(irc, source, args):
|
||||
irc.error("Unknown channel %r." % channel)
|
||||
return
|
||||
|
||||
irc.proto.topic(irc.pseudoclient.uid, channel, topic)
|
||||
irc.topic(irc.pseudoclient.uid, channel, topic)
|
||||
|
||||
irc.reply("Done.")
|
||||
irc.callHooks([irc.pseudoclient.uid, 'CHANCMDS_TOPIC',
|
||||
irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_TOPIC',
|
||||
{'channel': channel, 'text': topic, 'setter': source,
|
||||
'parse_as': 'TOPIC'}])
|
||||
|
||||
@utils.add_cmd
|
||||
def chghost(irc, source, args):
|
||||
"""<user> <new host>
|
||||
|
||||
Changes the visible host of the target user."""
|
||||
chgfield(irc, source, args, 'host')
|
||||
|
||||
@utils.add_cmd
|
||||
def chgident(irc, source, args):
|
||||
"""<user> <new ident>
|
||||
|
||||
Changes the ident of the target user."""
|
||||
chgfield(irc, source, args, 'ident')
|
||||
|
||||
@utils.add_cmd
|
||||
def chgname(irc, source, args):
|
||||
"""<user> <new name>
|
||||
|
||||
Changes the GECOS (realname) of the target user."""
|
||||
chgfield(irc, source, args, 'name', 'GECOS')
|
||||
|
||||
def chgfield(irc, source, args, human_field, internal_field=None):
|
||||
permissions.check_permissions(irc, source, ['opercmds.chg' + human_field])
|
||||
try:
|
||||
target = args[0]
|
||||
new = args[1]
|
||||
except IndexError:
|
||||
irc.error("Not enough arguments. Needs 2: target, new %s." % human_field)
|
||||
return
|
||||
|
||||
# Find the user
|
||||
targetu = irc.nick_to_uid(target)
|
||||
if targetu not in irc.users:
|
||||
irc.error("No such nick %r." % target)
|
||||
return
|
||||
|
||||
internal_field = internal_field or human_field.upper()
|
||||
irc.update_client(targetu, internal_field, new)
|
||||
irc.reply("Done.")
|
||||
|
35
plugins/raw.py
Normal file
35
plugins/raw.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""
|
||||
raw.py: Provides a 'raw' command for sending raw text to IRC.
|
||||
"""
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.coremods import permissions
|
||||
from pylinkirc import utils
|
||||
|
||||
@utils.add_cmd
|
||||
def raw(irc, source, args):
|
||||
"""<text>
|
||||
|
||||
Sends raw text to the IRC server.
|
||||
|
||||
This command is not officially supported on non-Clientbot networks, where it
|
||||
requires a separate permission."""
|
||||
|
||||
if irc.protoname == 'clientbot':
|
||||
# exec.raw is included for backwards compatibility with PyLink 1.x
|
||||
perms = ['raw.raw', 'exec.raw']
|
||||
else:
|
||||
perms = ['raw.raw.unsupported_network']
|
||||
permissions.check_permissions(irc, source, perms)
|
||||
|
||||
args = ' '.join(args)
|
||||
if not args.strip():
|
||||
irc.reply('No text entered!')
|
||||
return
|
||||
|
||||
# Note: This is loglevel debug so that we don't risk leaking things like
|
||||
# NickServ passwords on Clientbot networks.
|
||||
log.debug('(%s) Sending raw text %r to IRC for %s', irc.name, args,
|
||||
irc.get_hostmask(source))
|
||||
irc.send(args)
|
||||
|
||||
irc.reply("Done.")
|
1668
plugins/relay.py
1668
plugins/relay.py
File diff suppressed because it is too large
Load Diff
@ -7,16 +7,17 @@ from pylinkirc.log import log
|
||||
|
||||
# Clientbot default styles:
|
||||
# These use template strings as documented @ https://docs.python.org/3/library/string.html#template-strings
|
||||
default_styles = {'MESSAGE': '\x02[$netname]\x02 <$colored_sender> $text',
|
||||
default_styles = {'MESSAGE': '\x02[$netname]\x02 <$mode_prefix$colored_sender> $text',
|
||||
'KICK': '\x02[$netname]\x02 - $colored_sender$sender_identhost has kicked $target_nick from $channel ($text)',
|
||||
'PART': '\x02[$netname]\x02 - $colored_sender$sender_identhost has left $channel ($text)',
|
||||
'JOIN': '\x02[$netname]\x02 - $colored_sender$sender_identhost has joined $channel',
|
||||
'NICK': '\x02[$netname]\x02 - $colored_sender$sender_identhost is now known as $newnick',
|
||||
'QUIT': '\x02[$netname]\x02 - $colored_sender$sender_identhost has quit ($text)',
|
||||
'ACTION': '\x02[$netname]\x02 * $colored_sender $text',
|
||||
'NOTICE': '\x02[$netname]\x02 - Notice from $colored_sender: $text',
|
||||
'ACTION': '\x02[$netname]\x02 * $mode_prefix$colored_sender $text',
|
||||
'NOTICE': '\x02[$netname]\x02 - Notice from $mode_prefix$colored_sender: $text',
|
||||
'SQUIT': '\x02[$netname]\x02 - Netsplit lost users: $colored_nicks',
|
||||
'SJOIN': '\x02[$netname]\x02 - Netjoin gained users: $colored_nicks',
|
||||
'MODE': '\x02[$netname]\x02 - $colored_sender$sender_identhost sets mode $modes on $channel',
|
||||
'PM': 'PM from $sender on $netname: $text',
|
||||
'PNOTICE': '<$sender> $text',
|
||||
}
|
||||
@ -42,14 +43,14 @@ def cb_relay_core(irc, source, command, args):
|
||||
|
||||
if irc.pseudoclient and relay:
|
||||
try:
|
||||
sourcename = irc.getFriendlyName(source)
|
||||
sourcename = irc.get_friendly_name(source)
|
||||
except KeyError: # User has left due to /quit
|
||||
sourcename = args['userdata'].nick
|
||||
|
||||
relay_conf = conf.conf.get('relay', {})
|
||||
relay_conf = conf.conf.get('relay') or {}
|
||||
|
||||
# Be less floody on startup: don't relay non-PRIVMSGs for the first X seconds after connect.
|
||||
startup_delay = relay_conf.get('clientbot_startup_delay', 5)
|
||||
startup_delay = relay_conf.get('clientbot_startup_delay', 20)
|
||||
|
||||
# Special case for CTCPs.
|
||||
if real_command == 'MESSAGE':
|
||||
@ -59,13 +60,7 @@ def cb_relay_core(irc, source, command, args):
|
||||
|
||||
real_command = 'ACTION'
|
||||
|
||||
# We only handle # channels - this skips utils.isChannel() so that
|
||||
# @#channel messages aren't incorrectly interpreted as non-channel
|
||||
# specific.
|
||||
# A more thorough fix for this bug exists in 2.0[1] but depends on
|
||||
# somewhat invasive changes to how relay handles STATUSMSG entirely.
|
||||
# [1]: https://github.com/GLolol/PyLink/commit/57334183
|
||||
elif '#' not in args['target']:
|
||||
elif not irc.is_channel(args['target'].lstrip(''.join(irc.prefixmodes.values()))):
|
||||
# Target is a user; handle this accordingly.
|
||||
if relay_conf.get('allow_clientbot_pms'):
|
||||
real_command = 'PNOTICE' if args.get('is_notice') else 'PM'
|
||||
@ -85,16 +80,17 @@ def cb_relay_core(irc, source, command, args):
|
||||
# Try to fetch the format for the given command from the relay:clientbot_styles:$command
|
||||
# key, falling back to one defined in default_styles above, and then nothing if not found
|
||||
# there.
|
||||
text_template = relay_conf.get('clientbot_styles', {}).get(real_command,
|
||||
default_styles.get(real_command, ''))
|
||||
text_template = irc.get_service_option('relay', 'clientbot_styles', {}).get(
|
||||
real_command, default_styles.get(real_command, ''))
|
||||
text_template = string.Template(text_template)
|
||||
|
||||
if text_template:
|
||||
if irc.getServiceBot(source):
|
||||
if irc.get_service_bot(source):
|
||||
# HACK: service bots are global and lack the relay state we look for.
|
||||
# just pretend the message comes from the current network.
|
||||
log.debug('(%s) relay_cb_core: Overriding network origin to local (source=%s)', irc.name, source)
|
||||
sourcenet = irc.name
|
||||
realsource = source
|
||||
else:
|
||||
# Get the original client that the relay client source was meant for.
|
||||
log.debug('(%s) relay_cb_core: Trying to find original sender (user) for %s', irc.name, source)
|
||||
@ -103,12 +99,13 @@ def cb_relay_core(irc, source, command, args):
|
||||
except (AttributeError, KeyError):
|
||||
log.debug('(%s) relay_cb_core: Trying to find original sender (server) for %s. serverdata=%s', irc.name, source, args.get('serverdata'))
|
||||
try:
|
||||
origuser = ((args.get('serverdata') or irc.servers[source]).remote,)
|
||||
localsid = args.get('serverdata') or irc.servers[source]
|
||||
origuser = (localsid.remote, world.networkobjects[localsid.remote].uplink)
|
||||
except (AttributeError, KeyError):
|
||||
return
|
||||
|
||||
log.debug('(%s) relay_cb_core: Original sender found as %s', irc.name, origuser)
|
||||
sourcenet = origuser[0]
|
||||
sourcenet, realsource = origuser
|
||||
|
||||
try: # Try to get the full network name
|
||||
netname = conf.conf['servers'][sourcenet]['netname']
|
||||
@ -116,8 +113,12 @@ def cb_relay_core(irc, source, command, args):
|
||||
netname = sourcenet
|
||||
|
||||
# Figure out where the message is destined to.
|
||||
target = args.get('channel') or args.get('target')
|
||||
if target is None or not ('#' in target or private):
|
||||
stripped_target = target = args.get('channel') or args.get('target')
|
||||
if target is not None:
|
||||
# HACK: cheap fix to prevent @#channel messages from interpreted as non-channel specific
|
||||
stripped_target = target.lstrip(''.join(irc.prefixmodes.values()))
|
||||
|
||||
if target is None or not (irc.is_channel(stripped_target) or private):
|
||||
# Non-channel specific message (e.g. QUIT or NICK). If this isn't a PM, figure out
|
||||
# all channels that the sender shares over the relay, and relay them to those
|
||||
# channels.
|
||||
@ -126,35 +127,59 @@ def cb_relay_core(irc, source, command, args):
|
||||
# No user data given. This was probably some other global event such as SQUIT.
|
||||
userdata = irc.pseudoclient
|
||||
|
||||
targets = [channel for channel in userdata.channels if relay.get_relay((irc.name, channel))]
|
||||
targets = [channel for channel in userdata.channels if relay.get_relay(irc, channel)]
|
||||
else:
|
||||
# Pluralize the channel so that we can iterate over it.
|
||||
targets = [target]
|
||||
args['channel'] = stripped_target
|
||||
log.debug('(%s) relay_cb_core: Relaying event %s to channels: %s', irc.name, real_command, targets)
|
||||
|
||||
identhost = ''
|
||||
if source in irc.users:
|
||||
try:
|
||||
identhost = irc.getHostmask(source).split('!')[-1]
|
||||
identhost = irc.get_hostmask(source).split('!')[-1]
|
||||
except KeyError: # User got removed due to quit
|
||||
identhost = '%s@%s' % (args['olduser'].ident, args['olduser'].host)
|
||||
# This is specifically spaced so that ident@host is only shown for users that have
|
||||
# one, and not servers.
|
||||
identhost = ' (%s)' % identhost
|
||||
else:
|
||||
identhost = ''
|
||||
|
||||
# $target_nick: Convert the target for kicks, etc. from a UID to a nick
|
||||
if args.get("target") in irc.users:
|
||||
args["target_nick"] = irc.getFriendlyName(args['target'])
|
||||
args["target_nick"] = irc.get_friendly_name(args['target'])
|
||||
|
||||
args.update({'netname': netname, 'sender': sourcename, 'sender_identhost': identhost,
|
||||
'colored_sender': color_text(sourcename), 'colored_netname': color_text(netname)})
|
||||
# Join up modes from their list form
|
||||
if args.get('modes'):
|
||||
args['modes'] = irc.join_modes(args['modes'])
|
||||
|
||||
mode_prefix = ''
|
||||
if 'channel' in args:
|
||||
# Display the real channel instead of the local name, if applicable
|
||||
# Display the real (remote) channel name instead of the local one, if applicable.
|
||||
args['local_channel'] = args['channel']
|
||||
args['channel'] = relay.get_remote_channel(irc, world.networkobjects[sourcenet], args['channel'])
|
||||
log.debug('(%s) relay_clientbot: coersing $channel from %s to %s', irc.name, args['local_channel'], args['channel'])
|
||||
|
||||
sourceirc = world.networkobjects.get(sourcenet)
|
||||
log.debug('(%s) relay_clientbot: Checking prefix modes for %s on %s (relaying to %s)',
|
||||
irc.name, realsource, sourcenet, args['channel'])
|
||||
if sourceirc:
|
||||
args['channel'] = remotechan = relay.get_remote_channel(irc, sourceirc, args['channel'])
|
||||
if source in irc.users and remotechan in sourceirc.channels and \
|
||||
realsource in sourceirc.channels[remotechan].users:
|
||||
# Fetch the prefixmode prefixes (e.g. ~@%) for the sender, if available.
|
||||
prefixmodes = sourceirc.channels[remotechan].get_prefix_modes(realsource)
|
||||
log.debug('(%s) relay_clientbot: got prefix modes %s for %s on %s@%s',
|
||||
irc.name, prefixmodes, realsource, remotechan, sourcenet)
|
||||
if prefixmodes:
|
||||
# Only pick the highest prefix.
|
||||
mode_prefix = sourceirc.prefixmodes.get(
|
||||
sourceirc.cmodes.get(prefixmodes[0]))
|
||||
|
||||
args.update({
|
||||
'netname': netname, 'sender': sourcename, 'sender_identhost': identhost,
|
||||
'colored_sender': color_text(sourcename), 'colored_netname': color_text(netname),
|
||||
'mode_prefix': mode_prefix
|
||||
})
|
||||
|
||||
for target in targets:
|
||||
cargs = args.copy() # Copy args list to manipulate them in a channel specific way
|
||||
|
||||
@ -164,7 +189,6 @@ def cb_relay_core(irc, source, command, args):
|
||||
# still have to be relayed as such.
|
||||
nicklist = args.get('nicks')
|
||||
if nicklist:
|
||||
|
||||
# Get channel-specific nick list if relevent.
|
||||
if isinstance(nicklist, dict):
|
||||
nicklist = nicklist.get(target, [])
|
||||
@ -191,6 +215,7 @@ utils.add_hook(cb_relay_core, 'CLIENTBOT_QUIT')
|
||||
utils.add_hook(cb_relay_core, 'CLIENTBOT_NICK')
|
||||
utils.add_hook(cb_relay_core, 'CLIENTBOT_SJOIN')
|
||||
utils.add_hook(cb_relay_core, 'CLIENTBOT_SQUIT')
|
||||
utils.add_hook(cb_relay_core, 'RELAY_RAW_MODE')
|
||||
|
||||
@utils.add_cmd
|
||||
def rpm(irc, source, args):
|
||||
@ -207,7 +232,7 @@ def rpm(irc, source, args):
|
||||
return
|
||||
|
||||
relay = world.plugins.get('relay')
|
||||
if irc.proto.hasCap('can-spawn-clients'):
|
||||
if irc.has_cap('can-spawn-clients'):
|
||||
irc.error('This command is only supported on Clientbot networks. Try /msg %s <text>' % target)
|
||||
return
|
||||
elif relay is None:
|
||||
@ -221,15 +246,15 @@ def rpm(irc, source, args):
|
||||
'administratively disabled.')
|
||||
return
|
||||
|
||||
uid = irc.nickToUid(target)
|
||||
uid = irc.nick_to_uid(target)
|
||||
if not uid:
|
||||
irc.error('Unknown user %s.' % target)
|
||||
return
|
||||
elif not relay.isRelayClient(irc, uid):
|
||||
elif not relay.is_relay_client(irc, uid):
|
||||
irc.error('%s is not a relay user.' % target)
|
||||
return
|
||||
else:
|
||||
assert not irc.isInternalClient(source), "rpm is not allowed from PyLink bots"
|
||||
assert not irc.is_internal_client(source), "rpm is not allowed from PyLink bots"
|
||||
# Send the message through relay by faking a hook for its handler.
|
||||
relay.handle_messages(irc, source, 'RELAY_CLIENTBOT_PRIVMSG', {'target': uid, 'text': text})
|
||||
irc.reply('Message sent.')
|
||||
|
@ -6,6 +6,17 @@ from pylinkirc.coremods import permissions
|
||||
|
||||
import collections
|
||||
|
||||
DEFAULT_PERMISSIONS = {"$ircop": ['servermaps.localmap']}
|
||||
|
||||
def main(irc=None):
|
||||
"""Servermaps plugin main function, called on plugin load."""
|
||||
# Register our permissions.
|
||||
permissions.add_default_permissions(DEFAULT_PERMISSIONS)
|
||||
|
||||
def die(irc=None):
|
||||
"""Servermaps plugin die function, called on plugin unload."""
|
||||
permissions.remove_default_permissions(DEFAULT_PERMISSIONS)
|
||||
|
||||
def _percent(num, total):
|
||||
return '%.1f' % (num/total*100)
|
||||
|
||||
@ -14,7 +25,11 @@ def _map(irc, source, args, show_relay=True):
|
||||
|
||||
Shows the network map for the given network, or the current network if not specified."""
|
||||
|
||||
permissions.checkPermissions(irc, source, ['servermaps.map'])
|
||||
if show_relay:
|
||||
perm = 'servermaps.map'
|
||||
else:
|
||||
perm = 'servermaps.localmap'
|
||||
permissions.check_permissions(irc, source, [perm])
|
||||
|
||||
try:
|
||||
netname = args[0]
|
||||
@ -52,8 +67,8 @@ def _map(irc, source, args, show_relay=True):
|
||||
if hops == 0:
|
||||
# Show our root server once.
|
||||
rootusers = len(serverlist[sid].users)
|
||||
reply('\x02%s\x02[%s]: %s user(s) (%s%%)' % (serverlist[sid].name, sid,
|
||||
rootusers, _percent(rootusers, usercount)))
|
||||
reply('\x02%s\x02[%s]: %s user(s) (%s%%) {hopcount: %d}' % (serverlist[sid].name, sid,
|
||||
rootusers, _percent(rootusers, usercount), serverlist[sid].hopcount))
|
||||
|
||||
log.debug('(%s) servermaps: servers under sid %s: %s', irc.name, sid, servers)
|
||||
|
||||
@ -68,19 +83,25 @@ def _map(irc, source, args, show_relay=True):
|
||||
serverusers = len(serverlist[leaf].users)
|
||||
if is_relay_server:
|
||||
# Skip showing user data for relay servers.
|
||||
reply("%s\x02%s\x02[%s] (via PyLink Relay)" % (' '*hops, serverlist[leaf].name, leaf))
|
||||
reply("%s\x02%s\x02[%s] (via PyLink Relay)" %
|
||||
(' '*hops, serverlist[leaf].name, leaf))
|
||||
else:
|
||||
reply("%s\x02%s\x02[%s]: %s user(s) (%s%%)" % (' '*hops, serverlist[leaf].name, leaf,
|
||||
serverusers, _percent(serverusers, usercount)))
|
||||
reply("%s\x02%s\x02[%s]: %s user(s) (%s%%) {hopcount: %d}" %
|
||||
(' '*hops, serverlist[leaf].name, leaf,
|
||||
serverusers, _percent(serverusers, usercount), serverlist[leaf].hopcount))
|
||||
showall(ircobj, leaf, hops, is_relay_server=is_relay_server)
|
||||
|
||||
if (not is_relay_server) and hasattr(serverlist[leaf], 'remote') and show_relay:
|
||||
# This is a relay server - display the remote map of the network it represents
|
||||
relay_server = serverlist[leaf].remote
|
||||
remoteirc = world.networkobjects[relay_server]
|
||||
if remoteirc.proto.hasCap('can-track-servers'):
|
||||
if remoteirc.has_cap('can-track-servers'):
|
||||
# Only ever show relay subservers once - this prevents infinite loops.
|
||||
showall(remoteirc, remoteirc.sid, hops=hops, is_relay_server=True)
|
||||
else:
|
||||
# For Clientbot links, show the server we're actually connected to.
|
||||
reply("%s\x02%s\x02 (actual server name)" %
|
||||
(' '*(hops+1), remoteirc.uplink))
|
||||
|
||||
else:
|
||||
# Afterwards, decrement the hopcount.
|
||||
|
@ -18,7 +18,7 @@ def handle_kill(irc, numeric, command, args):
|
||||
automatically disconnects from the network.
|
||||
"""
|
||||
|
||||
if (args['userdata'] and irc.isInternalServer(args['userdata'].server)) or irc.isInternalClient(args['target']):
|
||||
if (args['userdata'] and irc.is_internal_server(args['userdata'].server)) or irc.is_internal_client(args['target']):
|
||||
if killcache.setdefault(irc.name, 1) >= length:
|
||||
log.error('(%s) servprotect: Too many kills received, aborting!', irc.name)
|
||||
irc.disconnect()
|
||||
@ -33,7 +33,7 @@ def handle_save(irc, numeric, command, args):
|
||||
Tracks SAVEs (nick collision) against PyLink clients. If too many are received,
|
||||
automatically disconnects from the network.
|
||||
"""
|
||||
if irc.isInternalClient(args['target']):
|
||||
if irc.is_internal_client(args['target']):
|
||||
if savecache.setdefault(irc.name, 0) >= length:
|
||||
log.error('(%s) servprotect: Too many nick collisions, aborting!', irc.name)
|
||||
irc.disconnect()
|
||||
|
@ -32,7 +32,7 @@ def uptime(irc, source, args):
|
||||
|
||||
Returns the uptime for PyLink and the given network's connection (or the current network if not specified).
|
||||
The --all argument can also be given to show the uptime for all networks."""
|
||||
permissions.checkPermissions(irc, source, ['stats.uptime'])
|
||||
permissions.check_permissions(irc, source, ['stats.uptime'])
|
||||
|
||||
try:
|
||||
network = args[0]
|
||||
@ -69,4 +69,62 @@ def uptime(irc, source, args):
|
||||
)
|
||||
)
|
||||
|
||||
def handle_stats(irc, source, command, args):
|
||||
"""/STATS handler. Currently supports the following:
|
||||
|
||||
c - link blocks
|
||||
o - oper blocks (accounts)
|
||||
u - shows uptime
|
||||
"""
|
||||
|
||||
stats_type = args['stats_type'][0].lower() # stats_type shouldn't be more than 1 char anyways
|
||||
|
||||
perms = ['stats.%s' % stats_type]
|
||||
|
||||
if stats_type == 'u':
|
||||
perms.append('stats.uptime') # Consistency
|
||||
|
||||
try:
|
||||
permissions.check_permissions(irc, source, perms)
|
||||
except utils.NotAuthorizedError as e:
|
||||
# Note, no irc.error() because this is not a command, but a handler
|
||||
irc.msg(source, 'Error: %s' % e, notice=True)
|
||||
return
|
||||
|
||||
log.info('(%s) /STATS %s requested by %s', irc.name, stats_type, irc.get_hostmask(source))
|
||||
|
||||
def _num(num, text):
|
||||
irc.numeric(args['target'], num, source, text)
|
||||
|
||||
if stats_type == 'c':
|
||||
# 213/RPL_STATSCLINE: "C <host> * <name> <port> <class>"
|
||||
for netname, serverdata in sorted(conf.conf['servers'].items()):
|
||||
# We're cramming as much as we can into the class field...
|
||||
_num(213, "C %s * %s %s [%s:%s:%s]" %
|
||||
(serverdata.get('ip', '0.0.0.0'),
|
||||
netname,
|
||||
serverdata.get('port', 0),
|
||||
serverdata['protocol'],
|
||||
'ssl' if serverdata.get('ssl') else 'no-ssl',
|
||||
serverdata.get('encoding', 'utf-8'))
|
||||
)
|
||||
elif stats_type == 'o':
|
||||
# 243/RPL_STATSOLINE: "O <hostmask> * <nick> [:<info>]"
|
||||
# New style accounts only!
|
||||
for accountname, accountdata in conf.conf['login'].get('accounts', {}).items():
|
||||
_num(243, "O %s * %s :network_filter:%s require_oper:%s" %
|
||||
(' '.join(accountdata.get('hosts', [])) or '*@*',
|
||||
accountname,
|
||||
','.join(accountdata.get('networks', [])) or '*',
|
||||
bool(accountdata.get('require_oper'))
|
||||
)
|
||||
)
|
||||
|
||||
elif stats_type == 'u':
|
||||
# 242/RPL_STATSUPTIME: ":Server Up <days> days <hours>:<minutes>:<seconds>"
|
||||
_num(242, ':Server Up %s' % timediff(world.start_ts, int(time.time())))
|
||||
|
||||
else:
|
||||
log.info('(%s) Unknown /STATS type %r requested by %s', irc.name, stats_type, irc.get_hostmask(source))
|
||||
_num(219, "%s :End of /STATS report" % stats_type)
|
||||
utils.add_hook(handle_stats, 'STATS')
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,7 @@
|
||||
"""
|
||||
hybrid.py: IRCD-Hybrid protocol module for PyLink.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from pylinkirc import utils, conf
|
||||
@ -5,22 +9,19 @@ from pylinkirc.log import log
|
||||
from pylinkirc.classes import *
|
||||
from pylinkirc.protocols.ts6 import *
|
||||
|
||||
# This protocol module inherits from the TS6 protocol.
|
||||
class HybridProtocol(TS6Protocol):
|
||||
def __init__(self, irc):
|
||||
# This protocol module inherits from the TS6 protocol.
|
||||
super().__init__(irc)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.casemapping = 'ascii'
|
||||
self.caps = {}
|
||||
self.hook_map = {'EOB': 'ENDBURST', 'TBURST': 'TOPIC', 'SJOIN': 'JOIN'}
|
||||
self.has_eob = False
|
||||
self.protocol_caps -= {'slash-in-hosts'}
|
||||
|
||||
def connect(self):
|
||||
def post_connect(self):
|
||||
"""Initializes a connection to a server."""
|
||||
ts = self.irc.start_ts
|
||||
self.has_eob = False
|
||||
f = self.irc.send
|
||||
ts = self.start_ts
|
||||
f = self.send
|
||||
|
||||
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L80
|
||||
# Note: according to hybrid source code, +p is paranoia, noknock,
|
||||
@ -40,7 +41,7 @@ class HybridProtocol(TS6Protocol):
|
||||
'*A': 'beI', '*B': 'k', '*C': 'l', '*D': 'cimnprstCMORS'
|
||||
}
|
||||
|
||||
self.irc.cmodes = cmodes
|
||||
self.cmodes = cmodes
|
||||
|
||||
umodes = {
|
||||
'oper': 'o', 'invisible': 'i', 'wallops': 'w', 'locops': 'l',
|
||||
@ -55,13 +56,14 @@ class HybridProtocol(TS6Protocol):
|
||||
'*A': '', '*B': '', '*C': '', '*D': 'DFGHRSWabcdefgijklnopqrsuwxy'
|
||||
}
|
||||
|
||||
self.irc.umodes = umodes
|
||||
self.umodes = umodes
|
||||
self.extbans_matching.clear()
|
||||
|
||||
# halfops is mandatory on Hybrid
|
||||
self.irc.prefixmodes = {'o': '@', 'h': '%', 'v': '+'}
|
||||
self.prefixmodes = {'o': '@', 'h': '%', 'v': '+'}
|
||||
|
||||
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L55
|
||||
f('PASS %s TS 6 %s' % (self.irc.serverdata["sendpass"], self.irc.sid))
|
||||
f('PASS %s TS 6 %s' % (self.serverdata["sendpass"], self.sid))
|
||||
|
||||
# We request the following capabilities (for hybrid):
|
||||
|
||||
@ -71,7 +73,7 @@ class HybridProtocol(TS6Protocol):
|
||||
# CHW: Allow sending messages to @#channel and the like.
|
||||
# KNOCK: Support for /knock
|
||||
# SVS: Deal with extended NICK/UID messages that contain service IDs/stamps
|
||||
# TBURST: Topic Burst command; we send this in topicBurst
|
||||
# TBURST: Topic Burst command; we send this in topic_burst
|
||||
# DLN: DLINE command
|
||||
# UNDLN: UNDLINE command
|
||||
# KLN: KLINE command
|
||||
@ -82,13 +84,13 @@ class HybridProtocol(TS6Protocol):
|
||||
# EOB: Supports EOB (end of burst) command
|
||||
f('CAPAB :TBURST DLN KNOCK UNDLN UNKLN KLN ENCAP IE EX HOPS CHW SVS CLUSTER EOB QS')
|
||||
|
||||
f('SERVER %s 0 :%s' % (self.irc.serverdata["hostname"],
|
||||
self.irc.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc']))
|
||||
f('SERVER %s 0 :%s' % (self.serverdata["hostname"],
|
||||
self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']))
|
||||
|
||||
# send endburst now
|
||||
self.irc.send(':%s EOB' % (self.irc.sid,))
|
||||
self.send(':%s EOB' % (self.sid,))
|
||||
|
||||
def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(),
|
||||
def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(),
|
||||
server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None,
|
||||
manipulatable=False):
|
||||
"""
|
||||
@ -98,27 +100,31 @@ class HybridProtocol(TS6Protocol):
|
||||
up to plugins to make sure they don't introduce anything invalid.
|
||||
"""
|
||||
|
||||
server = server or self.irc.sid
|
||||
if not self.irc.isInternalServer(server):
|
||||
server = server or self.sid
|
||||
if not self.is_internal_server(server):
|
||||
raise ValueError('Server %r is not a PyLink server!' % server)
|
||||
|
||||
uid = self.uidgen[server].next_uid()
|
||||
|
||||
ts = ts or int(time.time())
|
||||
realname = realname or conf.conf['bot']['realname']
|
||||
realname = realname or conf.conf['pylink']['realname']
|
||||
realhost = realhost or host
|
||||
raw_modes = self.irc.joinModes(modes)
|
||||
u = self.irc.users[uid] = IrcUser(nick, ts, uid, server, ident=ident, host=host, realname=realname,
|
||||
raw_modes = self.join_modes(modes)
|
||||
|
||||
u = self.users[uid] = User(self, nick, ts, uid, server, ident=ident, host=host, realname=realname,
|
||||
realhost=realhost, ip=ip, manipulatable=manipulatable)
|
||||
self.irc.applyModes(uid, modes)
|
||||
self.irc.servers[server].users.add(uid)
|
||||
self._send(server, "UID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} "
|
||||
|
||||
self.apply_modes(uid, modes)
|
||||
self.servers[server].users.add(uid)
|
||||
|
||||
self._send_with_prefix(server, "UID {nick} {hopcount} {ts} {modes} {ident} {host} {ip} {uid} "
|
||||
"* :{realname}".format(ts=ts, host=host,
|
||||
nick=nick, ident=ident, uid=uid,
|
||||
modes=raw_modes, ip=ip, realname=realname))
|
||||
modes=raw_modes, ip=ip, realname=realname,
|
||||
hopcount=self.servers[server].hopcount))
|
||||
return u
|
||||
|
||||
def updateClient(self, target, field, text):
|
||||
def update_client(self, target, field, text):
|
||||
"""Updates the ident, host, or realname of a PyLink client."""
|
||||
# https://github.com/ircd-hybrid/ircd-hybrid/blob/58323b8/modules/m_svsmode.c#L40-L103
|
||||
# parv[0] = command
|
||||
@ -128,28 +134,42 @@ class HybridProtocol(TS6Protocol):
|
||||
# parv[4] = optional argument (services account, vhost)
|
||||
field = field.upper()
|
||||
|
||||
ts = self.irc.users[target].ts
|
||||
ts = self.users[target].ts
|
||||
|
||||
if field == 'HOST':
|
||||
self.irc.users[target].host = text
|
||||
self.users[target].host = text
|
||||
# On Hybrid, it appears that host changing is actually just forcing umode
|
||||
# "+x <hostname>" on the target. -GLolol
|
||||
self._send(self.irc.sid, 'SVSMODE %s %s +x %s' % (target, ts, text))
|
||||
self._send_with_prefix(self.sid, 'SVSMODE %s %s +x %s' % (target, ts, text))
|
||||
else:
|
||||
raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field)
|
||||
|
||||
def topicBurst(self, numeric, target, text):
|
||||
def set_server_ban(self, source, duration, user='*', host='*', reason='User banned'):
|
||||
"""
|
||||
Sets a server ban.
|
||||
"""
|
||||
# source: user
|
||||
# parameters: target server mask, duration, user mask, host mask, reason
|
||||
assert not (user == host == '*'), "Refusing to set ridiculous ban on *@*"
|
||||
|
||||
if not source in self.users:
|
||||
log.debug('(%s) Forcing KLINE sender to %s as TS6 does not allow KLINEs from servers', self.name, self.pseudoclient.uid)
|
||||
source = self.pseudoclient.uid
|
||||
|
||||
self._send_with_prefix(source, 'KLINE * %s %s %s :%s' % (duration, user, host, reason))
|
||||
|
||||
def topic_burst(self, numeric, target, text):
|
||||
"""Sends a topic change from a PyLink server. This is usually used on burst."""
|
||||
# <- :0UY TBURST 1459308205 #testchan 1459309379 dan!~d@localhost :sdf
|
||||
if not self.irc.isInternalServer(numeric):
|
||||
if not self.is_internal_server(numeric):
|
||||
raise LookupError('No such PyLink server exists.')
|
||||
|
||||
ts = self.irc.channels[target].ts
|
||||
servername = self.irc.servers[numeric].name
|
||||
ts = self._channels[target].ts
|
||||
servername = self.servers[numeric].name
|
||||
|
||||
self._send(numeric, 'TBURST %s %s %s %s :%s' % (ts, target, int(time.time()), servername, text))
|
||||
self.irc.channels[target].topic = text
|
||||
self.irc.channels[target].topicset = True
|
||||
self._send_with_prefix(numeric, 'TBURST %s %s %s %s :%s' % (ts, target, int(time.time()), servername, text))
|
||||
self._channels[target].topic = text
|
||||
self._channels[target].topicset = True
|
||||
|
||||
# command handlers
|
||||
|
||||
@ -157,14 +177,11 @@ class HybridProtocol(TS6Protocol):
|
||||
# We only get a list of keywords here. Hybrid obviously assumes that
|
||||
# we know what modes it supports (indeed, this is a standard list).
|
||||
# <- CAPAB :UNDLN UNKLN KLN TBURST KNOCK ENCAP DLN IE EX HOPS CHW SVS CLUSTER EOB QS
|
||||
self.irc.caps = caps = args[0].split()
|
||||
self._caps = caps = args[0].split()
|
||||
for required_cap in ('SVS', 'EOB', 'HOPS', 'QS', 'TBURST'):
|
||||
if required_cap not in caps:
|
||||
raise ProtocolError('%s not found in TS6 capabilities list; this is required! (got %r)' % (required_cap, caps))
|
||||
|
||||
log.debug('(%s) self.irc.connected set!', self.irc.name)
|
||||
self.irc.connected.set()
|
||||
|
||||
def handle_uid(self, numeric, command, args):
|
||||
"""
|
||||
Handles Hybrid-style UID commands (user introduction). This is INCOMPATIBLE
|
||||
@ -172,48 +189,52 @@ class HybridProtocol(TS6Protocol):
|
||||
"""
|
||||
# <- :0UY UID dan 1 1451041551 +Facdeiklosuw ~ident localhost 127.0.0.1 0UYAAAAAB * :realname
|
||||
nick = args[0]
|
||||
self.check_nick_collision(nick)
|
||||
self._check_nick_collision(nick)
|
||||
ts, modes, ident, host, ip, uid, account, realname = args[2:10]
|
||||
ts = int(ts)
|
||||
if account == '*':
|
||||
account = None
|
||||
log.debug('(%s) handle_uid: got args nick=%s ts=%s uid=%s ident=%s '
|
||||
'host=%s realname=%s ip=%s', self.irc.name, nick, ts, uid,
|
||||
'host=%s realname=%s ip=%s', self.name, nick, ts, uid,
|
||||
ident, host, realname, ip)
|
||||
|
||||
self.irc.users[uid] = IrcUser(nick, ts, uid, numeric, ident, host, realname, host, ip)
|
||||
self.users[uid] = User(self, nick, ts, uid, numeric, ident, host, realname, host, ip)
|
||||
|
||||
parsedmodes = self.irc.parseModes(uid, [modes])
|
||||
log.debug('(%s) handle_uid: Applying modes %s for %s', self.irc.name, parsedmodes, uid)
|
||||
self.irc.applyModes(uid, parsedmodes)
|
||||
self.irc.servers[numeric].users.add(uid)
|
||||
parsedmodes = self.parse_modes(uid, [modes])
|
||||
log.debug('(%s) handle_uid: Applying modes %s for %s', self.name, parsedmodes, uid)
|
||||
self.apply_modes(uid, parsedmodes)
|
||||
self.servers[numeric].users.add(uid)
|
||||
|
||||
# Call the OPERED UP hook if +o is being added to the mode list.
|
||||
if ('+o', None) in parsedmodes:
|
||||
self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': 'IRC_Operator'}])
|
||||
self._check_oper_status_change(uid, parsedmodes)
|
||||
|
||||
# Set the account name if present
|
||||
if account:
|
||||
self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}])
|
||||
self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}])
|
||||
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realname': realname, 'host': host, 'ident': ident, 'ip': ip}
|
||||
|
||||
def handle_tburst(self, numeric, command, args):
|
||||
"""Handles incoming topic burst (TBURST) commands."""
|
||||
# <- :0UY TBURST 1459308205 #testchan 1459309379 dan!~d@localhost :sdf
|
||||
channel = self.irc.toLower(args[1])
|
||||
channel = args[1]
|
||||
ts = args[2]
|
||||
setter = args[3]
|
||||
topic = args[-1]
|
||||
self.irc.channels[channel].topic = topic
|
||||
self.irc.channels[channel].topicset = True
|
||||
self._channels[channel].topic = topic
|
||||
self._channels[channel].topicset = True
|
||||
return {'channel': channel, 'setter': setter, 'ts': ts, 'text': topic}
|
||||
|
||||
def handle_eob(self, numeric, command, args):
|
||||
log.debug('(%s) end of burst received', self.irc.name)
|
||||
if not self.has_eob: # Only call ENDBURST hooks if we haven't already.
|
||||
return {}
|
||||
"""EOB (end-of-burst) handler."""
|
||||
log.debug('(%s) end of burst received from %s', self.name, numeric)
|
||||
if not self.servers[numeric].has_eob:
|
||||
# Don't fight with TS6's generic PING-as-EOB
|
||||
self.servers[numeric].has_eob = True
|
||||
|
||||
self.has_eob = True
|
||||
if numeric == self.uplink:
|
||||
self.connected.set()
|
||||
return {}
|
||||
|
||||
def handle_svsmode(self, numeric, command, args):
|
||||
"""
|
||||
@ -224,7 +245,7 @@ class HybridProtocol(TS6Protocol):
|
||||
target = args[0]
|
||||
ts = args[1]
|
||||
modes = args[2:]
|
||||
parsedmodes = self.irc.parseModes(target, modes)
|
||||
parsedmodes = self.parse_modes(target, modes)
|
||||
|
||||
for modepair in parsedmodes:
|
||||
if modepair[0] == '+d':
|
||||
@ -244,7 +265,7 @@ class HybridProtocol(TS6Protocol):
|
||||
|
||||
# Send the login hook, and remove this mode from the mode
|
||||
# list, as it shouldn't be parsed literally.
|
||||
self.irc.callHooks([target, 'CLIENT_SERVICES_LOGIN', {'text': account}])
|
||||
self.call_hooks([target, 'CLIENT_SERVICES_LOGIN', {'text': account}])
|
||||
parsedmodes.remove(modepair)
|
||||
|
||||
elif modepair[0] == '+x':
|
||||
@ -253,16 +274,16 @@ class HybridProtocol(TS6Protocol):
|
||||
# to some.host, for example.
|
||||
host = args[-1]
|
||||
|
||||
self.irc.users[target].host = host
|
||||
self.users[target].host = host
|
||||
|
||||
# Propagate the hostmask change as a hook.
|
||||
self.irc.callHooks([numeric, 'CHGHOST',
|
||||
{'target': target, 'newhost': host}])
|
||||
self.call_hooks([numeric, 'CHGHOST',
|
||||
{'target': target, 'newhost': host}])
|
||||
|
||||
parsedmodes.remove(modepair)
|
||||
|
||||
if parsedmodes:
|
||||
self.irc.applyModes(target, parsedmodes)
|
||||
self.apply_modes(target, parsedmodes)
|
||||
|
||||
return {'target': target, 'modes': parsedmodes}
|
||||
|
||||
|
@ -11,8 +11,11 @@ from pylinkirc.log import log
|
||||
from pylinkirc.protocols.ts6_common import *
|
||||
|
||||
class InspIRCdProtocol(TS6BaseProtocol):
|
||||
def __init__(self, irc):
|
||||
super().__init__(irc)
|
||||
|
||||
S2S_BUFSIZE = 0 # InspIRCd allows infinitely long S2S messages, so set bufsize to infinite
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.protocol_caps |= {'slash-in-nicks', 'slash-in-hosts', 'underscore-in-hosts'}
|
||||
|
||||
@ -31,9 +34,16 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
self.proto_ver = 1202
|
||||
self.max_proto_ver = 1202 # Anything above should warn (not officially supported)
|
||||
|
||||
# Track the modules supported by the uplink.
|
||||
self._modsupport = set()
|
||||
|
||||
# Settable by plugins (e.g. relay) as needed, used to work around +j being triggered
|
||||
# by bursting users.
|
||||
self._endburst_delay = 0
|
||||
|
||||
### Outgoing commands
|
||||
|
||||
def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(),
|
||||
def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(),
|
||||
server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator',
|
||||
manipulatable=False):
|
||||
"""
|
||||
@ -42,30 +52,31 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
Note: No nick collision / valid nickname checks are done here; it is
|
||||
up to plugins to make sure they don't introduce anything invalid.
|
||||
"""
|
||||
server = server or self.irc.sid
|
||||
server = server or self.sid
|
||||
|
||||
if not self.irc.isInternalServer(server):
|
||||
if not self.is_internal_server(server):
|
||||
raise ValueError('Server %r is not a PyLink server!' % server)
|
||||
|
||||
uid = self.uidgen[server].next_uid()
|
||||
|
||||
ts = ts or int(time.time())
|
||||
realname = realname or conf.conf['bot']['realname']
|
||||
realname = realname or conf.conf['pylink']['realname']
|
||||
realhost = realhost or host
|
||||
raw_modes = self.irc.joinModes(modes)
|
||||
u = self.irc.users[uid] = IrcUser(nick, ts, uid, server, ident=ident, host=host, realname=realname,
|
||||
realhost=realhost, ip=ip, manipulatable=manipulatable, opertype=opertype)
|
||||
raw_modes = self.join_modes(modes)
|
||||
u = self.users[uid] = User(self, nick, ts, uid, server, ident=ident, host=host,
|
||||
realname=realname, realhost=realhost, ip=ip,
|
||||
manipulatable=manipulatable, opertype=opertype)
|
||||
|
||||
self.irc.applyModes(uid, modes)
|
||||
self.irc.servers[server].users.add(uid)
|
||||
self.apply_modes(uid, modes)
|
||||
self.servers[server].users.add(uid)
|
||||
|
||||
self._send(server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}"
|
||||
" {ts} {modes} + :{realname}".format(ts=ts, host=host,
|
||||
nick=nick, ident=ident, uid=uid,
|
||||
modes=raw_modes, ip=ip, realname=realname,
|
||||
realhost=realhost))
|
||||
self._send_with_prefix(server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}"
|
||||
" {ts} {modes} + :{realname}".format(ts=ts, host=host,
|
||||
nick=nick, ident=ident, uid=uid,
|
||||
modes=raw_modes, ip=ip, realname=realname,
|
||||
realhost=realhost))
|
||||
if ('o', None) in modes or ('+o', None) in modes:
|
||||
self._operUp(uid, opertype)
|
||||
self._oper_up(uid, opertype)
|
||||
return u
|
||||
|
||||
def join(self, client, channel):
|
||||
@ -73,20 +84,19 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
# InspIRCd doesn't distinguish between burst joins and regular joins,
|
||||
# so what we're actually doing here is sending FJOIN from the server,
|
||||
# on behalf of the clients that are joining.
|
||||
channel = self.irc.toLower(channel)
|
||||
|
||||
server = self.irc.getServer(client)
|
||||
if not self.irc.isInternalServer(server):
|
||||
log.error('(%s) Error trying to join %r to %r (no such client exists)', self.irc.name, client, channel)
|
||||
server = self.get_server(client)
|
||||
if not self.is_internal_server(server):
|
||||
log.error('(%s) Error trying to join %r to %r (no such client exists)', self.name, client, channel)
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
|
||||
# Strip out list-modes, they shouldn't be ever sent in FJOIN.
|
||||
modes = [m for m in self.irc.channels[channel].modes if m[0] not in self.irc.cmodes['*A']]
|
||||
self._send(server, "FJOIN {channel} {ts} {modes} :,{uid}".format(
|
||||
ts=self.irc.channels[channel].ts, uid=client, channel=channel,
|
||||
modes=self.irc.joinModes(modes)))
|
||||
self.irc.channels[channel].users.add(client)
|
||||
self.irc.users[client].channels.add(channel)
|
||||
modes = [m for m in self._channels[channel].modes if m[0] not in self.cmodes['*A']]
|
||||
self._send_with_prefix(server, "FJOIN {channel} {ts} {modes} :,{uid}".format(
|
||||
ts=self._channels[channel].ts, uid=client, channel=channel,
|
||||
modes=self.join_modes(modes)))
|
||||
self._channels[channel].users.add(client)
|
||||
self.users[client].channels.add(channel)
|
||||
|
||||
def sjoin(self, server, channel, users, ts=None, modes=set()):
|
||||
"""Sends an SJOIN for a group of users to a channel.
|
||||
@ -97,29 +107,28 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
|
||||
Example uses:
|
||||
sjoin('100', '#test', [('', '100AAABBC'), ('qo', 100AAABBB'), ('h', '100AAADDD')])
|
||||
sjoin(self.irc.sid, '#test', [('o', self.irc.pseudoclient.uid)])
|
||||
sjoin(self.sid, '#test', [('o', self.pseudoclient.uid)])
|
||||
"""
|
||||
channel = self.irc.toLower(channel)
|
||||
server = server or self.irc.sid
|
||||
server = server or self.sid
|
||||
assert users, "sjoin: No users sent?"
|
||||
log.debug('(%s) sjoin: got %r for users', self.irc.name, users)
|
||||
log.debug('(%s) sjoin: got %r for users', self.name, users)
|
||||
|
||||
if not server:
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
|
||||
# Strip out list-modes, they shouldn't ever be sent in FJOIN (protocol rules).
|
||||
modes = modes or self.irc.channels[channel].modes
|
||||
orig_ts = self.irc.channels[channel].ts
|
||||
modes = modes or self._channels[channel].modes
|
||||
orig_ts = self._channels[channel].ts
|
||||
ts = ts or orig_ts
|
||||
|
||||
banmodes = []
|
||||
regularmodes = []
|
||||
for mode in modes:
|
||||
modechar = mode[0][-1]
|
||||
if modechar in self.irc.cmodes['*A']:
|
||||
if modechar in self.cmodes['*A']:
|
||||
# Track bans separately (they are sent as a normal FMODE instead of in FJOIN.
|
||||
# However, don't reset bans that have already been set.
|
||||
if (modechar, mode[1]) not in self.irc.channels[channel].modes:
|
||||
if (modechar, mode[1]) not in self._channels[channel].modes:
|
||||
banmodes.append(mode)
|
||||
else:
|
||||
regularmodes.append(mode)
|
||||
@ -137,25 +146,25 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
for m in prefixes:
|
||||
changedmodes.add(('+%s' % m, user))
|
||||
try:
|
||||
self.irc.users[user].channels.add(channel)
|
||||
self.users[user].channels.add(channel)
|
||||
except KeyError: # Not initialized yet?
|
||||
log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user)
|
||||
log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.name, channel, user)
|
||||
|
||||
namelist = ' '.join(namelist)
|
||||
self._send(server, "FJOIN {channel} {ts} {modes} :{users}".format(
|
||||
self._send_with_prefix(server, "FJOIN {channel} {ts} {modes} :{users}".format(
|
||||
ts=ts, users=namelist, channel=channel,
|
||||
modes=self.irc.joinModes(modes)))
|
||||
self.irc.channels[channel].users.update(uids)
|
||||
modes=self.join_modes(modes)))
|
||||
self._channels[channel].users.update(uids)
|
||||
|
||||
if banmodes:
|
||||
# Burst ban modes if there are any.
|
||||
# <- :1ML FMODE #test 1461201525 +bb *!*@bad.user *!*@rly.bad.user
|
||||
self._send(server, "FMODE {channel} {ts} {modes} ".format(
|
||||
ts=ts, channel=channel, modes=self.irc.joinModes(banmodes)))
|
||||
self._send_with_prefix(server, "FMODE {channel} {ts} {modes} ".format(
|
||||
ts=ts, channel=channel, modes=self.join_modes(banmodes)))
|
||||
|
||||
self.updateTS(server, channel, ts, changedmodes)
|
||||
|
||||
def _operUp(self, target, opertype=None):
|
||||
def _oper_up(self, target, opertype=None):
|
||||
"""Opers a client up (internal function specific to InspIRCd).
|
||||
|
||||
This should be called whenever user mode +o is set on anyone, because
|
||||
@ -163,19 +172,19 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
recognize ANY non-burst oper ups.
|
||||
|
||||
Plugins don't have to call this function themselves, but they can
|
||||
set the opertype attribute of an IrcUser object (in self.irc.users),
|
||||
set the opertype attribute of an User object (in self.users),
|
||||
and the change will be reflected here."""
|
||||
userobj = self.irc.users[target]
|
||||
userobj = self.users[target]
|
||||
try:
|
||||
otype = opertype or userobj.opertype or 'IRC Operator'
|
||||
except AttributeError:
|
||||
log.debug('(%s) opertype field for %s (%s) isn\'t filled yet!',
|
||||
self.irc.name, target, userobj.nick)
|
||||
self.name, target, userobj.nick)
|
||||
# whatever, this is non-standard anyways.
|
||||
otype = 'IRC Operator'
|
||||
assert otype, "Tried to send an empty OPERTYPE!"
|
||||
log.debug('(%s) Sending OPERTYPE from %s to oper them up.',
|
||||
self.irc.name, target)
|
||||
self.name, target)
|
||||
userobj.opertype = otype
|
||||
|
||||
# InspIRCd 2.x uses _ in OPERTYPE to denote spaces, while InspIRCd 3.x does not. This is not
|
||||
@ -187,75 +196,66 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
else:
|
||||
otype = ':' + otype
|
||||
|
||||
self._send(target, 'OPERTYPE %s' % otype)
|
||||
self._send_with_prefix(target, 'OPERTYPE %s' % otype)
|
||||
|
||||
def mode(self, numeric, target, modes, ts=None):
|
||||
"""Sends mode changes from a PyLink client/server."""
|
||||
# -> :9PYAAAAAA FMODE #pylink 1433653951 +os 9PYAAAAAA
|
||||
# -> :9PYAAAAAA MODE 9PYAAAAAA -i+w
|
||||
|
||||
if (not self.irc.isInternalClient(numeric)) and \
|
||||
(not self.irc.isInternalServer(numeric)):
|
||||
if (not self.is_internal_client(numeric)) and \
|
||||
(not self.is_internal_server(numeric)):
|
||||
raise LookupError('No such PyLink client/server exists.')
|
||||
|
||||
log.debug('(%s) inspircd._sendModes: received %r for mode list', self.irc.name, modes)
|
||||
if ('+o', None) in modes and not utils.isChannel(target):
|
||||
if ('+o', None) in modes and not self.is_channel(target):
|
||||
# https://github.com/inspircd/inspircd/blob/master/src/modules/m_spanningtree/opertype.cpp#L26-L28
|
||||
# Servers need a special command to set umode +o on people.
|
||||
self._operUp(target)
|
||||
self.irc.applyModes(target, modes)
|
||||
joinedmodes = self.irc.joinModes(modes)
|
||||
if utils.isChannel(target):
|
||||
ts = ts or self.irc.channels[self.irc.toLower(target)].ts
|
||||
self._send(numeric, 'FMODE %s %s %s' % (target, ts, joinedmodes))
|
||||
self._oper_up(target)
|
||||
|
||||
self.apply_modes(target, modes)
|
||||
joinedmodes = self.join_modes(modes)
|
||||
|
||||
if self.is_channel(target):
|
||||
ts = ts or self._channels[target].ts
|
||||
self._send_with_prefix(numeric, 'FMODE %s %s %s' % (target, ts, joinedmodes))
|
||||
else:
|
||||
self._send(numeric, 'MODE %s %s' % (target, joinedmodes))
|
||||
self._send_with_prefix(numeric, 'MODE %s %s' % (target, joinedmodes))
|
||||
|
||||
def kill(self, numeric, target, reason):
|
||||
"""Sends a kill from a PyLink client/server."""
|
||||
if (not self.irc.isInternalClient(numeric)) and \
|
||||
(not self.irc.isInternalServer(numeric)):
|
||||
if (not self.is_internal_client(numeric)) and \
|
||||
(not self.is_internal_server(numeric)):
|
||||
raise LookupError('No such PyLink client/server exists.')
|
||||
|
||||
# InspIRCd will show the raw kill message sent from our server as the quit message.
|
||||
# So, make the kill look actually like a kill instead of someone quitting with
|
||||
# an arbitrary message.
|
||||
if numeric in self.irc.servers:
|
||||
sourcenick = self.irc.servers[numeric].name
|
||||
if numeric in self.servers:
|
||||
sourcenick = self.servers[numeric].name
|
||||
else:
|
||||
sourcenick = self.irc.users[numeric].nick
|
||||
sourcenick = self.users[numeric].nick
|
||||
|
||||
self._send(numeric, 'KILL %s :Killed (%s (%s))' % (target, sourcenick, reason))
|
||||
self._send_with_prefix(numeric, 'KILL %s :Killed (%s (%s))' % (target, sourcenick, reason))
|
||||
|
||||
# We only need to call removeClient here if the target is one of our
|
||||
# clients, since any remote servers will send a QUIT from
|
||||
# their target if the command succeeds.
|
||||
if self.irc.isInternalClient(target):
|
||||
self.removeClient(target)
|
||||
self._remove_client(target)
|
||||
|
||||
def topicBurst(self, numeric, target, text):
|
||||
def topic_burst(self, numeric, target, text):
|
||||
"""Sends a topic change from a PyLink server. This is usually used on burst."""
|
||||
if not self.irc.isInternalServer(numeric):
|
||||
if not self.is_internal_server(numeric):
|
||||
raise LookupError('No such PyLink server exists.')
|
||||
ts = int(time.time())
|
||||
servername = self.irc.servers[numeric].name
|
||||
self._send(numeric, 'FTOPIC %s %s %s :%s' % (target, ts, servername, text))
|
||||
self.irc.channels[target].topic = text
|
||||
self.irc.channels[target].topicset = True
|
||||
|
||||
def invite(self, numeric, target, channel):
|
||||
"""Sends an INVITE from a PyLink client.."""
|
||||
if not self.irc.isInternalClient(numeric):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
self._send(numeric, 'INVITE %s %s' % (target, channel))
|
||||
servername = self.servers[numeric].name
|
||||
self._send_with_prefix(numeric, 'FTOPIC %s %s %s :%s' % (target, ts, servername, text))
|
||||
self._channels[target].topic = text
|
||||
self._channels[target].topicset = True
|
||||
|
||||
def knock(self, numeric, target, text):
|
||||
"""Sends a KNOCK from a PyLink client."""
|
||||
if not self.irc.isInternalClient(numeric):
|
||||
if not self.is_internal_client(numeric):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
self._send(numeric, 'ENCAP * KNOCK %s :%s' % (target, text))
|
||||
self._send_with_prefix(numeric, 'ENCAP * KNOCK %s :%s' % (target, text))
|
||||
|
||||
def updateClient(self, target, field, text):
|
||||
def update_client(self, target, field, text):
|
||||
"""Updates the ident, host, or realname of any connected client."""
|
||||
field = field.upper()
|
||||
|
||||
@ -263,59 +263,49 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
raise NotImplementedError("Changing field %r of a client is "
|
||||
"unsupported by this protocol." % field)
|
||||
|
||||
if self.irc.isInternalClient(target):
|
||||
if self.is_internal_client(target):
|
||||
# It is one of our clients, use FIDENT/HOST/NAME.
|
||||
if field == 'IDENT':
|
||||
self.irc.users[target].ident = text
|
||||
self._send(target, 'FIDENT %s' % text)
|
||||
self.users[target].ident = text
|
||||
self._send_with_prefix(target, 'FIDENT %s' % text)
|
||||
elif field == 'HOST':
|
||||
self.irc.users[target].host = text
|
||||
self._send(target, 'FHOST %s' % text)
|
||||
self.users[target].host = text
|
||||
self._send_with_prefix(target, 'FHOST %s' % text)
|
||||
elif field in ('REALNAME', 'GECOS'):
|
||||
self.irc.users[target].realname = text
|
||||
self._send(target, 'FNAME :%s' % text)
|
||||
self.users[target].realname = text
|
||||
self._send_with_prefix(target, 'FNAME :%s' % text)
|
||||
else:
|
||||
# It is a client on another server, use CHGIDENT/HOST/NAME.
|
||||
if field == 'IDENT':
|
||||
if 'm_chgident.so' not in self.modsupport:
|
||||
log.warning('(%s) Failed to change ident of %s to %r: load m_chgident.so!', self.irc.name, target, text)
|
||||
return
|
||||
if 'm_chgident.so' not in self._modsupport:
|
||||
raise NotImplementedError('Cannot change idents as m_chgident.so is not loaded')
|
||||
|
||||
self.irc.users[target].ident = text
|
||||
self._send(self.irc.sid, 'CHGIDENT %s %s' % (target, text))
|
||||
self.users[target].ident = text
|
||||
self._send_with_prefix(self.sid, 'CHGIDENT %s %s' % (target, text))
|
||||
|
||||
# Send hook payloads for other plugins to listen to.
|
||||
self.irc.callHooks([self.irc.sid, 'CHGIDENT',
|
||||
self.call_hooks([self.sid, 'CHGIDENT',
|
||||
{'target': target, 'newident': text}])
|
||||
elif field == 'HOST':
|
||||
if 'm_chghost.so' not in self.modsupport:
|
||||
log.warning('(%s) Failed to change host of %s to %r: load m_chghost.so!', self.irc.name, target, text)
|
||||
return
|
||||
if 'm_chghost.so' not in self._modsupport:
|
||||
raise NotImplementedError('Cannot change hosts as m_chghost.so is not loaded')
|
||||
|
||||
self.irc.users[target].host = text
|
||||
self._send(self.irc.sid, 'CHGHOST %s %s' % (target, text))
|
||||
self.users[target].host = text
|
||||
self._send_with_prefix(self.sid, 'CHGHOST %s %s' % (target, text))
|
||||
|
||||
self.irc.callHooks([self.irc.sid, 'CHGHOST',
|
||||
self.call_hooks([self.sid, 'CHGHOST',
|
||||
{'target': target, 'newhost': text}])
|
||||
|
||||
elif field in ('REALNAME', 'GECOS'):
|
||||
if 'm_chgname.so' not in self.modsupport:
|
||||
log.warning('(%s) Failed to change real name of %s to %r: load m_chgname.so!', self.irc.name, target, text)
|
||||
return
|
||||
self.irc.users[target].realname = text
|
||||
self._send(self.irc.sid, 'CHGNAME %s :%s' % (target, text))
|
||||
if 'm_chgname.so' not in self._modsupport:
|
||||
raise NotImplementedError('Cannot change real names as m_chgname.so is not loaded')
|
||||
|
||||
self.irc.callHooks([self.irc.sid, 'CHGNAME',
|
||||
self.users[target].realname = text
|
||||
self._send_with_prefix(self.sid, 'CHGNAME %s :%s' % (target, text))
|
||||
|
||||
self.call_hooks([self.sid, 'CHGNAME',
|
||||
{'target': target, 'newgecos': text}])
|
||||
|
||||
def ping(self, source=None, target=None):
|
||||
"""Sends a PING to a target server. Periodic PINGs are sent to our uplink
|
||||
automatically by the Irc() internals; plugins shouldn't have to use this."""
|
||||
source = source or self.irc.sid
|
||||
target = target or self.irc.uplink
|
||||
if not (target is None or source is None):
|
||||
self._send(source, 'PING %s %s' % (source, target))
|
||||
|
||||
def numeric(self, source, numeric, target, text):
|
||||
"""Sends raw numerics from a server to a remote client."""
|
||||
# InspIRCd 2.0 syntax (undocumented):
|
||||
@ -327,92 +317,122 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
# :<sid> NUM <numeric source sid> <target uuid> <3 digit number> <params>
|
||||
# Take this into consideration if we ever target InspIRCd 2.2, even though m_spanningtree
|
||||
# does provide backwards compatibility for commands like this. -GLolol
|
||||
self._send(self.irc.sid, 'PUSH %s ::%s %s %s %s' % (target, source, numeric, target, text))
|
||||
self._send_with_prefix(self.sid, 'PUSH %s ::%s %s %s %s' % (target, source, numeric, target, text))
|
||||
|
||||
def away(self, source, text):
|
||||
"""Sends an AWAY message from a PyLink client. <text> can be an empty string
|
||||
to unset AWAY status."""
|
||||
if text:
|
||||
self._send(source, 'AWAY %s :%s' % (int(time.time()), text))
|
||||
self._send_with_prefix(source, 'AWAY %s :%s' % (int(time.time()), text))
|
||||
else:
|
||||
self._send(source, 'AWAY')
|
||||
self.irc.users[source].away = text
|
||||
self._send_with_prefix(source, 'AWAY')
|
||||
self.users[source].away = text
|
||||
|
||||
def spawnServer(self, name, sid=None, uplink=None, desc=None, endburst_delay=0):
|
||||
def spawn_server(self, name, sid=None, uplink=None, desc=None):
|
||||
"""
|
||||
Spawns a server off a PyLink server. desc (server description)
|
||||
defaults to the one in the config. uplink defaults to the main PyLink
|
||||
server, and sid (the server ID) is automatically generated if not
|
||||
given.
|
||||
|
||||
If endburst_delay is set greater than zero, the sending of ENDBURST
|
||||
will be delayed by the amount given. This can be used to prevent
|
||||
pseudoserver bursts from triggering IRCd join-flood preventions,
|
||||
and prevent connections from filling up the snomasks too much.
|
||||
Endburst delay can be tweaked by setting the _endburst_delay variable
|
||||
to a positive value before calling spawn_server(). This can be used to
|
||||
prevent PyLink bursts from filling up snomasks and triggering InspIRCd +j.
|
||||
"""
|
||||
# -> :0AL SERVER test.server * 1 0AM :some silly pseudoserver
|
||||
uplink = uplink or self.irc.sid
|
||||
uplink = uplink or self.sid
|
||||
name = name.lower()
|
||||
|
||||
# "desc" defaults to the configured server description.
|
||||
desc = desc or self.irc.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc']
|
||||
desc = desc or self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']
|
||||
|
||||
if sid is None: # No sid given; generate one!
|
||||
sid = self.sidgen.next_sid()
|
||||
|
||||
assert len(sid) == 3, "Incorrect SID length"
|
||||
if sid in self.irc.servers:
|
||||
if sid in self.servers:
|
||||
raise ValueError('A server with SID %r already exists!' % sid)
|
||||
for server in self.irc.servers.values():
|
||||
|
||||
for server in self.servers.values():
|
||||
if name == server.name:
|
||||
raise ValueError('A server named %r already exists!' % name)
|
||||
if not self.irc.isInternalServer(uplink):
|
||||
|
||||
if not self.is_internal_server(uplink):
|
||||
raise ValueError('Server %r is not a PyLink server!' % uplink)
|
||||
if not utils.isServerName(name):
|
||||
|
||||
if not self.is_server_name(name):
|
||||
raise ValueError('Invalid server name %r' % name)
|
||||
self._send(uplink, 'SERVER %s * 1 %s :%s' % (name, sid, desc))
|
||||
self.irc.servers[sid] = IrcServer(uplink, name, internal=True, desc=desc)
|
||||
|
||||
self.servers[sid] = Server(self, uplink, name, internal=True, desc=desc)
|
||||
self._send_with_prefix(uplink, 'SERVER %s * %s %s :%s' % (name, self.servers[sid].hopcount, sid, desc))
|
||||
|
||||
# Endburst delay clutter
|
||||
|
||||
def endburstf():
|
||||
# Delay ENDBURST by X seconds if requested.
|
||||
if self.irc.aborted.wait(endburst_delay):
|
||||
if self._aborted.wait(self._endburst_delay):
|
||||
# We managed to catch the abort flag before sending ENDBURST, so break
|
||||
log.debug('(%s) stopping endburstf() for %s as aborted was set', self.irc.name, sid)
|
||||
log.debug('(%s) stopping endburstf() for %s as aborted was set', self.name, sid)
|
||||
return
|
||||
self._send(sid, 'ENDBURST')
|
||||
self._send_with_prefix(sid, 'ENDBURST')
|
||||
|
||||
if endburst_delay:
|
||||
threading.Thread(target=endburstf).start()
|
||||
if self._endburst_delay:
|
||||
t = threading.Thread(target=endburstf, name="protocols/inspircd delayed ENDBURST thread for %s@%s" % (sid, self.name))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
else: # Else, send burst immediately
|
||||
self._send(sid, 'ENDBURST')
|
||||
self._send_with_prefix(sid, 'ENDBURST')
|
||||
return sid
|
||||
|
||||
def squit(self, source, target, text='No reason given'):
|
||||
"""SQUITs a PyLink server."""
|
||||
# -> :9PY SQUIT 9PZ :blah, blah
|
||||
self._send(source, 'SQUIT %s :%s' % (target, text))
|
||||
self.handle_squit(source, 'SQUIT', [target, text])
|
||||
def set_server_ban(self, source, duration, user='*', host='*', reason='User banned'):
|
||||
"""
|
||||
Sets a server ban.
|
||||
"""
|
||||
# <- :70M ADDLINE G *@10.9.8.7 midnight.local 1433704565 0 :gib reason pls kthx
|
||||
|
||||
assert not (user == host == '*'), "Refusing to set ridiculous ban on *@*"
|
||||
|
||||
# Per https://wiki.inspircd.org/InspIRCd_Spanning_Tree_1.2/ADDLINE, the setter argument can
|
||||
# be a max of 64 characters
|
||||
self._send_with_prefix(source, 'ADDLINE G %s@%s %s %s %s :%s' % (user, host, self.get_friendly_name(source)[:64],
|
||||
int(time.time()), duration, reason))
|
||||
|
||||
### Core / command handlers
|
||||
|
||||
def connect(self):
|
||||
def _post_disconnect(self):
|
||||
super()._post_disconnect()
|
||||
log.debug('(%s) _post_disconnect: clearing _modsupport entries. Last: %s', self.name, self._modsupport)
|
||||
self._modsupport.clear()
|
||||
|
||||
def post_connect(self):
|
||||
"""Initializes a connection to a server."""
|
||||
ts = self.irc.start_ts
|
||||
ts = self.start_ts
|
||||
|
||||
# Track the modules supported by the uplink.
|
||||
self.modsupport = []
|
||||
|
||||
f = self.irc.send
|
||||
f = self.send
|
||||
f('CAPAB START %s' % self.proto_ver)
|
||||
f('CAPAB CAPABILITIES :PROTOCOL=%s' % self.proto_ver)
|
||||
f('CAPAB END')
|
||||
|
||||
host = self.irc.serverdata["hostname"]
|
||||
host = self.serverdata["hostname"]
|
||||
f('SERVER {host} {Pass} 0 {sid} :{sdesc}'.format(host=host,
|
||||
Pass=self.irc.serverdata["sendpass"], sid=self.irc.sid,
|
||||
sdesc=self.irc.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc']))
|
||||
Pass=self.serverdata["sendpass"], sid=self.sid,
|
||||
sdesc=self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']))
|
||||
|
||||
self._send(self.irc.sid, 'BURST %s' % ts)
|
||||
self._send_with_prefix(self.sid, 'BURST %s' % ts)
|
||||
# InspIRCd sends VERSION data on link, instead of whenever requested by a client.
|
||||
self._send(self.irc.sid, 'VERSION :%s' % self.irc.version())
|
||||
self._send(self.irc.sid, 'ENDBURST')
|
||||
self._send_with_prefix(self.sid, 'VERSION :%s' % self.version())
|
||||
self._send_with_prefix(self.sid, 'ENDBURST')
|
||||
|
||||
# Extban definitions
|
||||
self.extbans_acting = {'quiet': 'm:', 'ban_nonick': 'N:', 'ban_blockcolor': 'c:',
|
||||
'ban_partmsgs': 'p:', 'ban_invites': 'A:', 'ban_blockcaps': 'B:',
|
||||
'ban_noctcp': 'C:', 'ban_nokicks': 'Q:', 'ban_stripcolor': 'S:',
|
||||
'ban_nonotice': 'T:'}
|
||||
self.extbans_matching = {'ban_inchannel': 'j:', 'ban_realname': 'r:', 'ban_server': 's:',
|
||||
'ban_certfp': 'z:', 'ban_opertype': 'O:', 'ban_account': 'R:',
|
||||
# Note: InspIRCd /helpop refers to this as an acting extban, but
|
||||
# it actually behaves as a matching one...
|
||||
'ban_unregistered_matching': 'U:'}
|
||||
|
||||
def handle_capab(self, source, command, args):
|
||||
"""
|
||||
@ -458,7 +478,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
name += '_insp'
|
||||
|
||||
# We don't care about mode prefixes; just the mode char.
|
||||
self.irc.cmodes[name] = char[-1]
|
||||
self.cmodes[name] = char[-1]
|
||||
|
||||
|
||||
elif args[0] == 'USERMODES':
|
||||
@ -472,7 +492,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
name, char = modepair.split('=')
|
||||
# Strip u_ prefixes to be consistent with other protocols.
|
||||
name = name.lstrip('u_')
|
||||
self.irc.umodes[name] = char
|
||||
self.umodes[name] = char
|
||||
|
||||
elif args[0] == 'CAPABILITIES':
|
||||
# <- CAPAB CAPABILITIES :NICKMAX=21 CHANMAX=64 MAXMODES=20
|
||||
@ -482,8 +502,8 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
# USERMODES=,,s,BHIRSWcghikorwx GLOBOPS=1 SVSPART=1
|
||||
|
||||
# First, turn the arguments into a dict
|
||||
caps = self.parseCapabilities(args[-1])
|
||||
log.debug("(%s) capabilities list: %s", self.irc.name, caps)
|
||||
caps = self.parse_isupport(args[-1])
|
||||
log.debug("(%s) capabilities list: %s", self.name, caps)
|
||||
|
||||
# Check the protocol version
|
||||
self.remote_proto_ver = protocol_version = int(caps['PROTOCOL'])
|
||||
@ -496,51 +516,50 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
elif protocol_version > self.max_proto_ver:
|
||||
log.warning("(%s) PyLink support for InspIRCd 2.2+ is experimental, "
|
||||
"and should not be relied upon for anything important.",
|
||||
self.irc.name)
|
||||
self.name)
|
||||
|
||||
# Store the max nick and channel lengths
|
||||
self.irc.maxnicklen = int(caps['NICKMAX'])
|
||||
self.irc.maxchanlen = int(caps['CHANMAX'])
|
||||
self.maxnicklen = int(caps['NICKMAX'])
|
||||
self.maxchanlen = int(caps['CHANMAX'])
|
||||
|
||||
# Modes are divided into A, B, C, and D classes
|
||||
# See http://www.irc.org/tech_docs/005.html
|
||||
|
||||
# FIXME: Find a neater way to assign/store this.
|
||||
self.irc.cmodes['*A'], self.irc.cmodes['*B'], self.irc.cmodes['*C'], self.irc.cmodes['*D'] \
|
||||
self.cmodes['*A'], self.cmodes['*B'], self.cmodes['*C'], self.cmodes['*D'] \
|
||||
= caps['CHANMODES'].split(',')
|
||||
self.irc.umodes['*A'], self.irc.umodes['*B'], self.irc.umodes['*C'], self.irc.umodes['*D'] \
|
||||
self.umodes['*A'], self.umodes['*B'], self.umodes['*C'], self.umodes['*D'] \
|
||||
= caps['USERMODES'].split(',')
|
||||
|
||||
# Separate the prefixes field (e.g. "(Yqaohv)!~&@%+") into a
|
||||
# dict mapping mode characters to mode prefixes.
|
||||
self.irc.prefixmodes = self.parsePrefixes(caps['PREFIX'])
|
||||
log.debug('(%s) self.irc.prefixmodes set to %r', self.irc.name,
|
||||
self.irc.prefixmodes)
|
||||
self.prefixmodes = self.parse_isupport_prefixes(caps['PREFIX'])
|
||||
log.debug('(%s) self.prefixmodes set to %r', self.name,
|
||||
self.prefixmodes)
|
||||
|
||||
# Finally, set the irc.connected (protocol negotiation complete)
|
||||
# state to True.
|
||||
self.irc.connected.set()
|
||||
elif args[0] == 'MODSUPPORT':
|
||||
# <- CAPAB MODSUPPORT :m_alltime.so m_check.so m_chghost.so m_chgident.so m_chgname.so m_fullversion.so m_gecosban.so m_knock.so m_muteban.so m_nicklock.so m_nopartmsg.so m_opmoderated.so m_sajoin.so m_sanick.so m_sapart.so m_serverban.so m_services_account.so m_showwhois.so m_silence.so m_swhois.so m_uninvite.so m_watch.so
|
||||
self.modsupport = args[-1].split()
|
||||
self._modsupport |= set(args[-1].split())
|
||||
|
||||
def handle_ping(self, source, command, args):
|
||||
"""Handles incoming PING commands, so we don't time out."""
|
||||
# <- :70M PING 70M 0AL
|
||||
# -> :0AL PONG 0AL 70M
|
||||
if self.irc.isInternalServer(args[1]):
|
||||
self._send(args[1], 'PONG %s %s' % (args[1], source), queue=False)
|
||||
if len(args) >= 2:
|
||||
self._send_with_prefix(args[1], 'PONG %s %s' % (args[1], source), queue=False)
|
||||
else:
|
||||
self._send_with_prefix(self.sid, 'PONG %s' % source, queue=False)
|
||||
|
||||
def handle_fjoin(self, servernumeric, command, args):
|
||||
"""Handles incoming FJOIN commands (InspIRCd equivalent of JOIN/SJOIN)."""
|
||||
# :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :o,1SRAABIT4 v,1IOAAF53R <...>
|
||||
channel = self.irc.toLower(args[0])
|
||||
chandata = self.irc.channels[channel].deepcopy()
|
||||
channel = args[0]
|
||||
chandata = self._channels[channel].deepcopy()
|
||||
# InspIRCd sends each channel's users in the form of 'modeprefix(es),UID'
|
||||
userlist = args[-1].split()
|
||||
|
||||
modestring = args[2:-1] or args[2]
|
||||
parsedmodes = self.irc.parseModes(channel, modestring)
|
||||
parsedmodes = self.parse_modes(channel, modestring)
|
||||
namelist = []
|
||||
|
||||
# Keep track of other modes that are added due to prefix modes being joined too.
|
||||
@ -550,18 +569,18 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
modeprefix, user = user.split(',', 1)
|
||||
|
||||
# Don't crash when we get an invalid UID.
|
||||
if user not in self.irc.users:
|
||||
if user not in self.users:
|
||||
log.debug('(%s) handle_fjoin: tried to introduce user %s not in our user list, ignoring...',
|
||||
self.irc.name, user)
|
||||
self.name, user)
|
||||
continue
|
||||
|
||||
namelist.append(user)
|
||||
self.irc.users[user].channels.add(channel)
|
||||
self.users[user].channels.add(channel)
|
||||
|
||||
# Only save mode changes if the remote has lower TS than us.
|
||||
changedmodes |= {('+%s' % mode, user) for mode in modeprefix}
|
||||
|
||||
self.irc.channels[channel].users.add(user)
|
||||
self._channels[channel].users.add(user)
|
||||
|
||||
# Statekeeping with timestamps. Note: some service packages (Anope 1.8) send a trailing
|
||||
# 'd' after the timestamp, which we should strip out to prevent int() from erroring.
|
||||
@ -570,7 +589,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
# <- :3AX FJOIN #monitor 1485462109d + :,3AXAAAAAK
|
||||
their_ts = int(''.join(char for char in args[1] if char.isdigit()))
|
||||
|
||||
our_ts = self.irc.channels[channel].ts
|
||||
our_ts = self._channels[channel].ts
|
||||
self.updateTS(servernumeric, channel, their_ts, changedmodes)
|
||||
|
||||
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts,
|
||||
@ -580,37 +599,37 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
"""Handles incoming UID commands (user introduction)."""
|
||||
# :70M UID 70MAAAAAB 1429934638 GL 0::1 hidden-7j810p.9mdf.lrek.0000.0000.IP gl 0::1 1429934638 +Wioswx +ACGKNOQXacfgklnoqvx :realname
|
||||
uid, ts, nick, realhost, host, ident, ip = args[0:7]
|
||||
self.check_nick_collision(nick)
|
||||
|
||||
ts = int(ts)
|
||||
|
||||
self._check_nick_collision(nick)
|
||||
realname = args[-1]
|
||||
self.irc.users[uid] = userobj = IrcUser(nick, ts, uid, numeric, ident, host, realname, realhost, ip)
|
||||
self.users[uid] = userobj = User(self, nick, ts, uid, numeric, ident, host, realname, realhost, ip)
|
||||
|
||||
parsedmodes = self.irc.parseModes(uid, [args[8], args[9]])
|
||||
self.irc.applyModes(uid, parsedmodes)
|
||||
parsedmodes = self.parse_modes(uid, [args[8], args[9]])
|
||||
self.apply_modes(uid, parsedmodes)
|
||||
|
||||
if (self.irc.umodes.get('servprotect'), None) in userobj.modes:
|
||||
# Services are usually given a "Network Service" WHOIS, so
|
||||
# set that as the opertype.
|
||||
self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': 'Network Service'}])
|
||||
self._check_oper_status_change(uid, parsedmodes)
|
||||
|
||||
self.irc.servers[numeric].users.add(uid)
|
||||
self.servers[numeric].users.add(uid)
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
|
||||
|
||||
def handle_server(self, numeric, command, args):
|
||||
"""Handles incoming SERVER commands (introduction of servers)."""
|
||||
|
||||
# Initial SERVER command on connect.
|
||||
if self.irc.uplink is None:
|
||||
if self.uplink is None:
|
||||
# <- SERVER whatever.net abcdefgh 0 10X :some server description
|
||||
servername = args[0].lower()
|
||||
numeric = args[3]
|
||||
|
||||
if args[1] != self.irc.serverdata['recvpass']:
|
||||
if args[1] != self.serverdata['recvpass']:
|
||||
# Check if recvpass is correct
|
||||
raise ProtocolError('Error: recvpass from uplink server %s does not match configuration!' % servername)
|
||||
raise ProtocolError('recvpass from uplink server %s does not match configuration!' % servername)
|
||||
|
||||
sdesc = args[-1]
|
||||
self.irc.servers[numeric] = IrcServer(None, servername, desc=sdesc)
|
||||
self.irc.uplink = numeric
|
||||
self.servers[numeric] = Server(self, None, servername, desc=sdesc)
|
||||
self.uplink = numeric
|
||||
return
|
||||
|
||||
# Other server introductions.
|
||||
@ -618,81 +637,55 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
servername = args[0].lower()
|
||||
sid = args[3]
|
||||
sdesc = args[-1]
|
||||
self.irc.servers[sid] = IrcServer(numeric, servername, desc=sdesc)
|
||||
self.servers[sid] = Server(self, numeric, servername, desc=sdesc)
|
||||
|
||||
return {'name': servername, 'sid': args[3], 'text': sdesc}
|
||||
|
||||
def handle_fmode(self, numeric, command, args):
|
||||
"""Handles the FMODE command, used for channel mode changes."""
|
||||
# <- :70MAAAAAA FMODE #chat 1433653462 +hhT 70MAAAAAA 70MAAAAAD
|
||||
channel = self.irc.toLower(args[0])
|
||||
oldobj = self.irc.channels[channel].deepcopy()
|
||||
channel = args[0]
|
||||
oldobj = self._channels[channel].deepcopy()
|
||||
modes = args[2:]
|
||||
changedmodes = self.irc.parseModes(channel, modes)
|
||||
self.irc.applyModes(channel, changedmodes)
|
||||
changedmodes = self.parse_modes(channel, modes)
|
||||
self.apply_modes(channel, changedmodes)
|
||||
ts = int(args[1])
|
||||
return {'target': channel, 'modes': changedmodes, 'ts': ts,
|
||||
'channeldata': oldobj}
|
||||
|
||||
def handle_mode(self, numeric, command, args):
|
||||
"""Handles incoming user mode changes."""
|
||||
# In InspIRCd, MODE is used for setting user modes and
|
||||
# FMODE is used for channel modes:
|
||||
# <- :70MAAAAAA MODE 70MAAAAAA -i+xc
|
||||
target = args[0]
|
||||
modestrings = args[1:]
|
||||
changedmodes = self.irc.parseModes(target, modestrings)
|
||||
self.irc.applyModes(target, changedmodes)
|
||||
return {'target': target, 'modes': changedmodes}
|
||||
|
||||
def handle_idle(self, numeric, command, args):
|
||||
def handle_idle(self, source, command, args):
|
||||
"""
|
||||
Handles the IDLE command, sent between servers in remote WHOIS queries.
|
||||
"""
|
||||
# <- :70MAAAAAA IDLE 1MLAAAAIG
|
||||
# -> :1MLAAAAIG IDLE 70MAAAAAA 1433036797 319
|
||||
sourceuser = numeric
|
||||
targetuser = args[0]
|
||||
|
||||
# HACK: make PyLink handle the entire WHOIS request.
|
||||
# This works by silently ignoring the idle time request, and sending our WHOIS data as
|
||||
# raw numerics instead.
|
||||
# The rationale behind this is that PyLink cannot accurately track signon and idle times for
|
||||
# things like relay users, without forwarding WHOIS requests between servers in a way the
|
||||
# hooks system is really not optimized to do. However, no IDLE response means that no WHOIS
|
||||
# data is ever sent back to the calling user, so this workaround is probably the best
|
||||
# solution (aside from faking values). -GLolol
|
||||
return {'target': args[0], 'parse_as': 'WHOIS'}
|
||||
if self.serverdata.get('force_whois_extensions', True):
|
||||
return {'target': args[0], 'parse_as': 'WHOIS'}
|
||||
|
||||
# Allow hiding the startup time if set to do so (if both idle and signon time is 0, InspIRCd omits
|
||||
# showing this line).
|
||||
target = args[0]
|
||||
start_time = self.start_ts if (conf.conf['pylink'].get('whois_show_startup_time', True) and
|
||||
self.get_service_bot(target)) else 0
|
||||
|
||||
# First arg = source, second = signon time, third = idle time
|
||||
self._send_with_prefix(target, 'IDLE %s %s 0' % (source, start_time))
|
||||
|
||||
def handle_ftopic(self, numeric, command, args):
|
||||
"""Handles incoming FTOPIC (sets topic on burst)."""
|
||||
# <- :70M FTOPIC #channel 1434510754 GLo|o|!GLolol@escape.the.dreamland.ca :Some channel topic
|
||||
channel = self.irc.toLower(args[0])
|
||||
channel = args[0]
|
||||
ts = args[1]
|
||||
setter = args[2]
|
||||
topic = args[-1]
|
||||
self.irc.channels[channel].topic = topic
|
||||
self.irc.channels[channel].topicset = True
|
||||
self._channels[channel].topic = topic
|
||||
self._channels[channel].topicset = True
|
||||
return {'channel': channel, 'setter': setter, 'ts': ts, 'text': topic}
|
||||
|
||||
# SVSTOPIC is used by InspIRCd module m_topiclock - its arguments are the same as FTOPIC
|
||||
handle_svstopic = handle_ftopic
|
||||
|
||||
def handle_invite(self, numeric, command, args):
|
||||
"""Handles incoming INVITEs."""
|
||||
# <- :70MAAAAAC INVITE 0ALAAAAAA #blah 0
|
||||
target = args[0]
|
||||
channel = self.irc.toLower(args[1])
|
||||
# We don't actually need to process this; just send the hook so plugins can use it
|
||||
return {'target': target, 'channel': channel}
|
||||
|
||||
def handle_knock(self, numeric, command, args):
|
||||
"""Handles channel KNOCKs."""
|
||||
# <- :70MAAAAAA ENCAP * KNOCK #blah :abcdefg
|
||||
channel = self.irc.toLower(args[0])
|
||||
text = args[1]
|
||||
return {'channel': channel, 'text': text}
|
||||
|
||||
def handle_opertype(self, target, command, args):
|
||||
"""Handles incoming OPERTYPE, which is used to denote an oper up.
|
||||
|
||||
@ -706,33 +699,36 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
|
||||
# Set umode +o on the target.
|
||||
omode = [('+o', None)]
|
||||
self.irc.applyModes(target, omode)
|
||||
self.apply_modes(target, omode)
|
||||
|
||||
# Call the CLIENT_OPERED hook that protocols use. The MODE hook
|
||||
# payload is returned below.
|
||||
self.irc.callHooks([target, 'CLIENT_OPERED', {'text': opertype}])
|
||||
self.call_hooks([target, 'CLIENT_OPERED', {'text': opertype}])
|
||||
return {'target': target, 'modes': omode}
|
||||
|
||||
def handle_fident(self, numeric, command, args):
|
||||
"""Handles FIDENT, used for denoting ident changes."""
|
||||
# <- :70MAAAAAB FIDENT test
|
||||
self.irc.users[numeric].ident = newident = args[0]
|
||||
self.users[numeric].ident = newident = args[0]
|
||||
return {'target': numeric, 'newident': newident}
|
||||
|
||||
def handle_fhost(self, numeric, command, args):
|
||||
"""Handles FHOST, used for denoting hostname changes."""
|
||||
# <- :70MAAAAAB FHOST some.host
|
||||
self.irc.users[numeric].host = newhost = args[0]
|
||||
self.users[numeric].host = newhost = args[0]
|
||||
return {'target': numeric, 'newhost': newhost}
|
||||
|
||||
def handle_fname(self, numeric, command, args):
|
||||
"""Handles FNAME, used for denoting real name/gecos changes."""
|
||||
# <- :70MAAAAAB FNAME :afdsafasf
|
||||
self.irc.users[numeric].realname = newgecos = args[0]
|
||||
self.users[numeric].realname = newgecos = args[0]
|
||||
return {'target': numeric, 'newgecos': newgecos}
|
||||
|
||||
def handle_endburst(self, numeric, command, args):
|
||||
"""ENDBURST handler; sends a hook with empty contents."""
|
||||
self.servers[numeric].has_eob = True
|
||||
if numeric == self.uplink:
|
||||
self.connected.set()
|
||||
return {}
|
||||
|
||||
def handle_away(self, numeric, command, args):
|
||||
@ -740,10 +736,10 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
# <- :1MLAAAAIG AWAY 1439371390 :Auto-away
|
||||
try:
|
||||
ts = args[0]
|
||||
self.irc.users[numeric].away = text = args[1]
|
||||
self.users[numeric].away = text = args[1]
|
||||
return {'text': text, 'ts': ts}
|
||||
except IndexError: # User is unsetting away status
|
||||
self.irc.users[numeric].away = ''
|
||||
self.users[numeric].away = ''
|
||||
return {'text': ''}
|
||||
|
||||
def handle_rsquit(self, numeric, command, args):
|
||||
@ -764,16 +760,16 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
# If we receive such a remote SQUIT, just forward it as a regular
|
||||
# SQUIT, in order to be consistent with other IRCds which make SQUITs
|
||||
# implicit.
|
||||
target = self._getSid(args[0])
|
||||
if self.irc.isInternalServer(target):
|
||||
target = self._get_SID(args[0])
|
||||
if self.is_internal_server(target):
|
||||
# The target has to be one of our servers in order to work...
|
||||
uplink = self.irc.servers[target].uplink
|
||||
reason = 'Requested by %s' % self.irc.getHostmask(numeric)
|
||||
self._send(uplink, 'SQUIT %s :%s' % (target, reason))
|
||||
uplink = self.servers[target].uplink
|
||||
reason = 'Requested by %s' % self.get_hostmask(numeric)
|
||||
self._send_with_prefix(uplink, 'SQUIT %s :%s' % (target, reason))
|
||||
return self.handle_squit(numeric, 'SQUIT', [target, reason])
|
||||
else:
|
||||
log.debug("(%s) Got RSQUIT for '%s', which is either invalid or not "
|
||||
"a server of ours!", self.irc.name, args[0])
|
||||
"a server of ours!", self.name, args[0])
|
||||
|
||||
def handle_metadata(self, numeric, command, args):
|
||||
"""
|
||||
@ -782,51 +778,52 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
"""
|
||||
uid = args[0]
|
||||
|
||||
if args[1] == 'accountname' and uid in self.irc.users:
|
||||
if args[1] == 'accountname' and uid in self.users:
|
||||
# <- :00A METADATA 1MLAAAJET accountname :
|
||||
# <- :00A METADATA 1MLAAAJET accountname :tester
|
||||
# Sets the services login name of the client.
|
||||
|
||||
self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': args[-1]}])
|
||||
self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': args[-1]}])
|
||||
elif args[1] == 'modules' and numeric == self.uplink:
|
||||
# Note: only handle METADATA from our uplink; otherwise leaf servers unloading modules
|
||||
# while shutting down will corrupt the state.
|
||||
# <- :70M METADATA * modules :-m_chghost.so
|
||||
# <- :70M METADATA * modules :+m_chghost.so
|
||||
for module in args[-1].split():
|
||||
if module.startswith('-'):
|
||||
log.debug('(%s) Removing module %s', self.name, module[1:])
|
||||
self._modsupport.discard(module[1:])
|
||||
elif module.startswith('+'):
|
||||
log.debug('(%s) Adding module %s', self.name, module[1:])
|
||||
self._modsupport.add(module[1:])
|
||||
else:
|
||||
log.warning('(%s) Got unknown METADATA modules string: %r', self.name, args[-1])
|
||||
|
||||
def handle_version(self, numeric, command, args):
|
||||
"""
|
||||
Stub VERSION handler (does nothing) to override the one in ts6_common.
|
||||
"""
|
||||
|
||||
def handle_kill(self, source, command, args):
|
||||
"""Handles incoming KILLs."""
|
||||
killed = args[0]
|
||||
# Depending on whether the IRCd sends explicit QUIT messages for
|
||||
# killed clients, the user may or may not have automatically been
|
||||
# removed from our user list.
|
||||
# If not, we have to assume that KILL = QUIT and remove them
|
||||
# ourselves.
|
||||
data = self.irc.users.get(killed)
|
||||
if data:
|
||||
self.removeClient(killed)
|
||||
return {'target': killed, 'text': args[1], 'userdata': data}
|
||||
|
||||
def handle_sakick(self, source, command, args):
|
||||
"""Handles forced kicks (SAKICK)."""
|
||||
# <- :1MLAAAAAD ENCAP 0AL SAKICK #test 0ALAAAAAB :test
|
||||
# ENCAP -> SAKICK args: ['#test', '0ALAAAAAB', 'test']
|
||||
|
||||
target = args[1]
|
||||
channel = self.irc.toLower(args[0])
|
||||
channel = args[0]
|
||||
try:
|
||||
reason = args[2]
|
||||
except IndexError:
|
||||
# Kick reason is optional, strange...
|
||||
reason = self.irc.getFriendlyName(source)
|
||||
reason = self.get_friendly_name(source)
|
||||
|
||||
if not self.irc.isInternalClient(target):
|
||||
log.warning("(%s) Got SAKICK for client that not one of ours: %s", self.irc.name, target)
|
||||
if not self.is_internal_client(target):
|
||||
log.warning("(%s) Got SAKICK for client that not one of ours: %s", self.name, target)
|
||||
return
|
||||
else:
|
||||
# Like RSQUIT, SAKICK requires that the receiving server acknowledge that a kick has
|
||||
# happened. This comes from the server hosting the target client.
|
||||
server = self.irc.getServer(target)
|
||||
server = self.get_server(target)
|
||||
|
||||
self.kick(server, channel, target, reason)
|
||||
return {'channel': channel, 'target': target, 'text': reason}
|
||||
@ -838,6 +835,6 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
|
||||
# XXX: We override notice() here because that abstraction doesn't allow messages from servers.
|
||||
timestring = '%s (%s)' % (time.strftime('%Y-%m-%d %H:%M:%S'), int(time.time()))
|
||||
self._send(self.irc.sid, 'NOTICE %s :System time is %s on %s' % (source, timestring, self.irc.hostname()))
|
||||
self._send_with_prefix(self.sid, 'NOTICE %s :System time is %s on %s' % (source, timestring, self.hostname()))
|
||||
|
||||
Class = InspIRCdProtocol
|
||||
|
@ -3,19 +3,342 @@ ircs2s_common.py: Common base protocol class with functions shared by TS6 and P1
|
||||
"""
|
||||
|
||||
import time
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
from pylinkirc.classes import Protocol
|
||||
from pylinkirc.classes import IRCNetwork, ProtocolError
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc import utils
|
||||
from pylinkirc import utils, conf
|
||||
|
||||
class IRCS2SProtocol(Protocol):
|
||||
class IncrementalUIDGenerator():
|
||||
"""
|
||||
Incremental UID Generator module, adapted from InspIRCd source:
|
||||
https://github.com/inspircd/inspircd/blob/f449c6b296ab/src/server.cpp#L85-L156
|
||||
"""
|
||||
|
||||
def __init__(self, sid):
|
||||
if not (hasattr(self, 'allowedchars') and hasattr(self, 'length')):
|
||||
raise RuntimeError("Allowed characters list not defined. Subclass "
|
||||
"%s by defining self.allowedchars and self.length "
|
||||
"and then calling super().__init__()." % self.__class__.__name__)
|
||||
self.uidchars = [self.allowedchars[0]]*self.length
|
||||
self.sid = str(sid)
|
||||
|
||||
def increment(self, pos=None):
|
||||
"""
|
||||
Increments the UID generator to the next available UID.
|
||||
"""
|
||||
# Position starts at 1 less than the UID length.
|
||||
if pos is None:
|
||||
pos = self.length - 1
|
||||
|
||||
# If we're at the last character in the list of allowed ones, reset
|
||||
# and increment the next level above.
|
||||
if self.uidchars[pos] == self.allowedchars[-1]:
|
||||
self.uidchars[pos] = self.allowedchars[0]
|
||||
self.increment(pos-1)
|
||||
else:
|
||||
# Find what position in the allowed characters list we're currently
|
||||
# on, and add one.
|
||||
idx = self.allowedchars.find(self.uidchars[pos])
|
||||
self.uidchars[pos] = self.allowedchars[idx+1]
|
||||
|
||||
def next_uid(self):
|
||||
"""
|
||||
Returns the next unused UID for the server.
|
||||
"""
|
||||
uid = self.sid + ''.join(self.uidchars)
|
||||
self.increment()
|
||||
return uid
|
||||
|
||||
class IRCCommonProtocol(IRCNetwork):
|
||||
|
||||
COMMON_PREFIXMODES = [('h', 'halfop'), ('a', 'admin'), ('q', 'owner'), ('y', 'owner')]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._caps = {}
|
||||
self._use_builtin_005_handling = False # Disabled by default for greater security
|
||||
|
||||
def post_connect(self):
|
||||
self._caps.clear()
|
||||
|
||||
def validate_server_conf(self):
|
||||
"""Validates that the server block given contains the required keys."""
|
||||
for k in self.conf_keys:
|
||||
log.debug('(%s) Checking presence of conf key %r', self.name, k)
|
||||
conf.validate(k in self.serverdata,
|
||||
"Missing option %r in server block for network %s."
|
||||
% (k, self.name))
|
||||
|
||||
port = self.serverdata['port']
|
||||
conf.validate(isinstance(port, int) and 0 < port < 65535,
|
||||
"Invalid port %r for network %s"
|
||||
% (port, self.name))
|
||||
|
||||
# TODO: these wrappers really need to be standardized
|
||||
def _get_SID(self, sname):
|
||||
"""Returns the SID of a server with the given name, if present."""
|
||||
name = sname.lower()
|
||||
|
||||
if name in self.servers:
|
||||
return name
|
||||
|
||||
for k, v in self.servers.items():
|
||||
if v.name.lower() == name:
|
||||
return k
|
||||
else:
|
||||
return sname # Fall back to given text instead of None
|
||||
|
||||
def _get_UID(self, target):
|
||||
"""Converts a nick argument to its matching UID. This differs from irc.nick_to_uid()
|
||||
in that it returns the original text instead of None, if no matching nick is found."""
|
||||
|
||||
if target in self.users:
|
||||
return target
|
||||
|
||||
target = self.nick_to_uid(target) or target
|
||||
return target
|
||||
|
||||
@staticmethod
|
||||
def parse_args(args):
|
||||
"""
|
||||
Parses a string or list of of RFC1459-style arguments, where ":" may
|
||||
be used for multi-word arguments that last until the end of a line.
|
||||
"""
|
||||
if isinstance(args, str):
|
||||
args = args.split(' ')
|
||||
|
||||
real_args = []
|
||||
for idx, arg in enumerate(args):
|
||||
if arg.startswith(':') and idx != 0:
|
||||
# ":" is used to begin multi-word arguments that last until the end of the message.
|
||||
# Use list splicing here to join them into one argument, and then add it to our list of args.
|
||||
joined_arg = ' '.join(args[idx:])[1:] # Cut off the leading : as well
|
||||
real_args.append(joined_arg)
|
||||
break
|
||||
real_args.append(arg)
|
||||
|
||||
return real_args
|
||||
|
||||
@classmethod
|
||||
def parse_prefixed_args(cls, args):
|
||||
"""Similar to parse_args(), but stripping leading colons from the first argument
|
||||
of a line (usually the sender field)."""
|
||||
args = cls.parse_args(args)
|
||||
args[0] = args[0].split(':', 1)[1]
|
||||
return args
|
||||
|
||||
def _squit(self, numeric, command, args):
|
||||
"""Handles incoming SQUITs."""
|
||||
|
||||
split_server = self._get_SID(args[0])
|
||||
|
||||
# Normally we'd only need to check for our SID as the SQUIT target, but Nefarious
|
||||
# actually uses the uplink server as the SQUIT target.
|
||||
# <- ABAAE SQ nefarious.midnight.vpn 0 :test
|
||||
if split_server in (self.sid, self.uplink):
|
||||
raise ProtocolError('SQUIT received: (reason: %s)' % args[-1])
|
||||
|
||||
affected_users = []
|
||||
affected_nicks = defaultdict(list)
|
||||
log.debug('(%s) Splitting server %s (reason: %s)', self.name, split_server, args[-1])
|
||||
|
||||
if split_server not in self.servers:
|
||||
log.warning("(%s) Tried to split a server (%s) that didn't exist!", self.name, split_server)
|
||||
return
|
||||
|
||||
# Prevent RuntimeError: dictionary changed size during iteration
|
||||
old_servers = self.servers.copy()
|
||||
old_channels = self._channels.copy()
|
||||
|
||||
# Cycle through our list of servers. If any server's uplink is the one that is being SQUIT,
|
||||
# remove them and all their users too.
|
||||
for sid, data in old_servers.items():
|
||||
if data.uplink == split_server:
|
||||
log.debug('Server %s also hosts server %s, removing those users too...', split_server, sid)
|
||||
# Recursively run SQUIT on any other hubs this server may have been connected to.
|
||||
args = self._squit(sid, 'SQUIT', [sid, "0",
|
||||
"PyLink: Automatically splitting leaf servers of %s" % sid])
|
||||
affected_users += args['users']
|
||||
|
||||
for user in self.servers[split_server].users.copy():
|
||||
affected_users.append(user)
|
||||
nick = self.users[user].nick
|
||||
|
||||
# Nicks affected is channel specific for SQUIT:. This makes Clientbot's SQUIT relaying
|
||||
# much easier to implement.
|
||||
for name, cdata in old_channels.items():
|
||||
if user in cdata.users:
|
||||
affected_nicks[name].append(nick)
|
||||
|
||||
log.debug('Removing client %s (%s)', user, nick)
|
||||
self._remove_client(user)
|
||||
|
||||
serverdata = self.servers[split_server]
|
||||
sname = serverdata.name
|
||||
uplink = serverdata.uplink
|
||||
|
||||
del self.servers[split_server]
|
||||
log.debug('(%s) Netsplit affected users: %s', self.name, affected_users)
|
||||
|
||||
return {'target': split_server, 'users': affected_users, 'name': sname,
|
||||
'uplink': uplink, 'nicks': affected_nicks, 'serverdata': serverdata,
|
||||
'channeldata': old_channels}
|
||||
|
||||
@staticmethod
|
||||
def parse_isupport(args, fallback=''):
|
||||
"""
|
||||
Parses a string of capabilities in the 005 / RPL_ISUPPORT format.
|
||||
"""
|
||||
|
||||
if isinstance(args, str):
|
||||
args = args.split(' ')
|
||||
|
||||
caps = {}
|
||||
for cap in args:
|
||||
try:
|
||||
# Try to split it as a KEY=VALUE pair.
|
||||
key, value = cap.split('=', 1)
|
||||
except ValueError:
|
||||
key = cap
|
||||
value = fallback
|
||||
caps[key] = value
|
||||
|
||||
return caps
|
||||
|
||||
@staticmethod
|
||||
def parse_isupport_prefixes(args):
|
||||
"""
|
||||
Separates prefixes field like "(qaohv)~&@%+" into a dict mapping mode characters to mode
|
||||
prefixes.
|
||||
"""
|
||||
prefixsearch = re.search(r'\(([A-Za-z]+)\)(.*)', args)
|
||||
return dict(zip(prefixsearch.group(1), prefixsearch.group(2)))
|
||||
|
||||
def handle_away(self, source, command, args):
|
||||
"""Handles incoming AWAY messages."""
|
||||
# TS6:
|
||||
# <- :6ELAAAAAB AWAY :Auto-away
|
||||
# <- :6ELAAAAAB AWAY
|
||||
# P10:
|
||||
# <- ABAAA A :blah
|
||||
# <- ABAAA A
|
||||
if source not in self.users:
|
||||
return
|
||||
|
||||
try:
|
||||
self.users[source].away = text = args[0]
|
||||
except IndexError: # User is unsetting away status
|
||||
self.users[source].away = text = ''
|
||||
return {'text': text}
|
||||
|
||||
def handle_error(self, numeric, command, args):
|
||||
"""Handles ERROR messages - these mean that our uplink has disconnected us!"""
|
||||
raise ProtocolError('Received an ERROR, disconnecting!')
|
||||
|
||||
def handle_pong(self, source, command, args):
|
||||
"""Handles incoming PONG commands."""
|
||||
if source == self.uplink:
|
||||
self.lastping = time.time()
|
||||
|
||||
def handle_005(self, source, command, args):
|
||||
"""
|
||||
Handles 005 / RPL_ISUPPORT. This is used by at least Clientbot and ngIRCd (for server negotiation).
|
||||
"""
|
||||
# ngIRCd:
|
||||
# <- :ngircd.midnight.local 005 pylink-devel.int NETWORK=ngircd-test :is my network name
|
||||
# <- :ngircd.midnight.local 005 pylink-devel.int RFC2812 IRCD=ngIRCd CHARSET=UTF-8 CASEMAPPING=ascii PREFIX=(qaohv)~&@%+ CHANTYPES=#&+ CHANMODES=beI,k,l,imMnOPQRstVz CHANLIMIT=#&+:10 :are supported on this server
|
||||
# <- :ngircd.midnight.local 005 pylink-devel.int CHANNELLEN=50 NICKLEN=21 TOPICLEN=490 AWAYLEN=127 KICKLEN=400 MODES=5 MAXLIST=beI:50 EXCEPTS=e INVEX=I PENALTY :are supported on this server
|
||||
|
||||
# Regular clientbot, connecting to InspIRCd:
|
||||
# <- :millennium.overdrivenetworks.com 005 ice AWAYLEN=200 CALLERID=g CASEMAPPING=rfc1459 CHANMODES=IXbegw,k,FJLfjl,ACKMNOPQRSTUcimnprstz CHANNELLEN=64 CHANTYPES=# CHARSET=ascii ELIST=MU ESILENCE EXCEPTS=e EXTBAN=,ACNOQRSTUcmprsuz FNC INVEX=I :are supported by this server
|
||||
# <- :millennium.overdrivenetworks.com 005 ice KICKLEN=255 MAP MAXBANS=60 MAXCHANNELS=30 MAXPARA=32 MAXTARGETS=20 MODES=20 NAMESX NETWORK=OVERdrive-IRC NICKLEN=21 OVERRIDE PREFIX=(Yqaohv)*~&@%+ SILENCE=32 :are supported by this server
|
||||
# <- :millennium.overdrivenetworks.com 005 ice SSL=[::]:6697 STARTTLS STATUSMSG=*~&@%+ TOPICLEN=307 UHNAMES USERIP VBANLIST WALLCHOPS WALLVOICES WATCH=32 :are supported by this server
|
||||
|
||||
if not self._use_builtin_005_handling:
|
||||
log.warning("(%s) Got spurious 005 message from %s: %r", self.name, source, args)
|
||||
return
|
||||
|
||||
newcaps = self.parse_isupport(args[1:-1])
|
||||
self._caps.update(newcaps)
|
||||
log.debug('(%s) handle_005: self._caps is %s', self.name, self._caps)
|
||||
|
||||
if 'CHANMODES' in newcaps:
|
||||
self.cmodes['*A'], self.cmodes['*B'], self.cmodes['*C'], self.cmodes['*D'] = \
|
||||
newcaps['CHANMODES'].split(',')
|
||||
log.debug('(%s) handle_005: cmodes: %s', self.name, self.cmodes)
|
||||
|
||||
if 'USERMODES' in newcaps:
|
||||
self.umodes['*A'], self.umodes['*B'], self.umodes['*C'], self.umodes['*D'] = \
|
||||
newcaps['USERMODES'].split(',')
|
||||
log.debug('(%s) handle_005: umodes: %s', self.name, self.umodes)
|
||||
|
||||
if 'CASEMAPPING' in newcaps:
|
||||
self.casemapping = newcaps.get('CASEMAPPING', self.casemapping)
|
||||
log.debug('(%s) handle_005: casemapping set to %s', self.name, self.casemapping)
|
||||
|
||||
if 'PREFIX' in newcaps:
|
||||
self.prefixmodes = prefixmodes = self.parse_isupport_prefixes(newcaps['PREFIX'])
|
||||
log.debug('(%s) handle_005: prefix modes set to %s', self.name, self.prefixmodes)
|
||||
|
||||
# Autodetect common prefix mode names.
|
||||
for char, modename in self.COMMON_PREFIXMODES:
|
||||
# Don't overwrite existing named mode definitions.
|
||||
if char in self.prefixmodes and modename not in self.cmodes:
|
||||
self.cmodes[modename] = char
|
||||
log.debug('(%s) handle_005: autodetecting mode %s (%s) as %s', self.name,
|
||||
char, self.prefixmodes[char], modename)
|
||||
|
||||
# https://defs.ircdocs.horse/defs/isupport.html
|
||||
if 'EXCEPTS' in newcaps:
|
||||
# Handle EXCEPTS=e or EXCEPTS fields
|
||||
self.cmodes['banexception'] = newcaps.get('EXCEPTS') or 'e'
|
||||
log.debug('(%s) handle_005: got cmode banexception=%r', self.name, self.cmodes['banexception'])
|
||||
|
||||
if 'INVEX' in newcaps:
|
||||
# Handle INVEX=I, INVEX fields
|
||||
self.cmodes['invex'] = newcaps.get('INVEX') or 'I'
|
||||
log.debug('(%s) handle_005: got cmode invex=%r', self.name, self.cmodes['invex'])
|
||||
|
||||
if 'NICKLEN' in newcaps:
|
||||
# Handle NICKLEN=number
|
||||
assert newcaps['NICKLEN'], "Got NICKLEN tag with no content?"
|
||||
self.maxnicklen = int(newcaps['NICKLEN'])
|
||||
log.debug('(%s) handle_005: got %r for maxnicklen', self.name, self.maxnicklen)
|
||||
|
||||
if 'DEAF' in newcaps:
|
||||
# Handle DEAF=D, DEAF fields
|
||||
self.umodes['deaf'] = newcaps.get('DEAF') or 'D'
|
||||
log.debug('(%s) handle_005: got umode deaf=%r', self.name, self.umodes['deaf'])
|
||||
|
||||
if 'CALLERID' in newcaps:
|
||||
# Handle CALLERID=g, CALLERID fields
|
||||
self.umodes['callerid'] = newcaps.get('CALLERID') or 'g'
|
||||
log.debug('(%s) handle_005: got umode callerid=%r', self.name, self.umodes['callerid'])
|
||||
|
||||
if 'STATUSMSG' in newcaps:
|
||||
# Note: This assumes that all available prefixes can be used in STATUSMSG too.
|
||||
# Even though this isn't always true, I don't see the point in making things
|
||||
# any more complicated.
|
||||
self.protocol_caps |= {'has-statusmsg'}
|
||||
|
||||
def _send_with_prefix(self, source, msg, **kwargs):
|
||||
"""Sends a RFC1459-style raw command from the given sender."""
|
||||
self.send(':%s %s' % (self._expandPUID(source), msg), **kwargs)
|
||||
|
||||
class IRCS2SProtocol(IRCCommonProtocol):
|
||||
COMMAND_TOKENS = {}
|
||||
|
||||
def __init__(self, irc):
|
||||
super().__init__(irc)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.protocol_caps = {'can-spawn-clients', 'has-ts', 'can-host-relay',
|
||||
'can-track-servers'}
|
||||
|
||||
# Alias
|
||||
self.handle_squit = self._squit
|
||||
|
||||
def handle_events(self, data):
|
||||
"""Event handler for RFC1459-like protocols.
|
||||
|
||||
@ -28,38 +351,38 @@ class IRCS2SProtocol(Protocol):
|
||||
the SID of the uplink server.
|
||||
"""
|
||||
data = data.split(" ")
|
||||
args = self.parseArgs(data)
|
||||
args = self.parse_args(data)
|
||||
|
||||
sender = args[0]
|
||||
sender = sender.lstrip(':')
|
||||
|
||||
# If the sender isn't in numeric format, try to convert it automatically.
|
||||
sender_sid = self._getSid(sender)
|
||||
sender_uid = self._getUid(sender)
|
||||
sender_sid = self._get_SID(sender)
|
||||
sender_uid = self._get_UID(sender)
|
||||
|
||||
if sender_sid in self.irc.servers:
|
||||
if sender_sid in self.servers:
|
||||
# Sender is a server (converting from name to SID gave a valid result).
|
||||
sender = sender_sid
|
||||
elif sender_uid in self.irc.users:
|
||||
elif sender_uid in self.users:
|
||||
# Sender is a user (converting from name to UID gave a valid result).
|
||||
sender = sender_uid
|
||||
else:
|
||||
elif not (args[0].startswith(':')):
|
||||
# No sender prefix; treat as coming from uplink IRCd.
|
||||
sender = self.irc.uplink
|
||||
sender = self.uplink
|
||||
args.insert(0, sender)
|
||||
|
||||
raw_command = args[1].upper()
|
||||
args = args[2:]
|
||||
|
||||
log.debug('(%s) Found message sender as %s', self.irc.name, sender)
|
||||
log.debug('(%s) Found message sender as %s, raw_command=%r, args=%r', self.name, sender, raw_command, args)
|
||||
|
||||
# For P10, convert the command token into a regular command, if present.
|
||||
command = self.COMMAND_TOKENS.get(raw_command, raw_command)
|
||||
if command != raw_command:
|
||||
log.debug('(%s) Translating token %s to command %s', self.irc.name, raw_command, command)
|
||||
log.debug('(%s) Translating token %s to command %s', self.name, raw_command, command)
|
||||
|
||||
if self.irc.isInternalClient(sender) or self.irc.isInternalServer(sender):
|
||||
log.warning("(%s) Received command %s being routed the wrong way!", self.irc.name, command)
|
||||
if self.is_internal_client(sender) or self.is_internal_server(sender):
|
||||
log.warning("(%s) Received command %s being routed the wrong way!", self.name, command)
|
||||
return
|
||||
|
||||
if command == 'ENCAP':
|
||||
@ -67,7 +390,7 @@ class IRCS2SProtocol(Protocol):
|
||||
# <- :00A ENCAP * SU 42XAAAAAC :GLolol
|
||||
command = args[1]
|
||||
args = args[2:]
|
||||
log.debug("(%s) Rewriting incoming ENCAP to command %s (args: %s)", self.irc.name, command, args)
|
||||
log.debug("(%s) Rewriting incoming ENCAP to command %s (args: %s)", self.name, command, args)
|
||||
|
||||
try:
|
||||
func = getattr(self, 'handle_'+command.lower())
|
||||
@ -78,6 +401,264 @@ class IRCS2SProtocol(Protocol):
|
||||
if parsed_args is not None:
|
||||
return [sender, command, parsed_args]
|
||||
|
||||
def invite(self, source, target, channel):
|
||||
"""Sends an INVITE from a PyLink client.."""
|
||||
if not self.is_internal_client(source):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
|
||||
self._send_with_prefix(source, 'INVITE %s %s' % (self._expandPUID(target), channel))
|
||||
|
||||
def kick(self, numeric, channel, target, reason=None):
|
||||
"""Sends kicks from a PyLink client/server."""
|
||||
|
||||
if (not self.is_internal_client(numeric)) and \
|
||||
(not self.is_internal_server(numeric)):
|
||||
raise LookupError('No such PyLink client/server exists.')
|
||||
|
||||
if not reason:
|
||||
reason = 'No reason given'
|
||||
|
||||
# Mangle kick targets for IRCds that require it.
|
||||
real_target = self._expandPUID(target)
|
||||
|
||||
self._send_with_prefix(numeric, 'KICK %s %s :%s' % (channel, real_target, reason))
|
||||
|
||||
# We can pretend the target left by its own will; all we really care about
|
||||
# is that the target gets removed from the channel userlist, and calling
|
||||
# handle_part() does that just fine.
|
||||
self.handle_part(target, 'KICK', [channel])
|
||||
|
||||
def numeric(self, source, numeric, target, text):
|
||||
"""Sends raw numerics from a server to a remote client. This is used for WHOIS replies."""
|
||||
# Mangle the target for IRCds that require it.
|
||||
target = self._expandPUID(target)
|
||||
|
||||
self._send_with_prefix(source, '%s %s %s' % (numeric, target, text))
|
||||
|
||||
def part(self, client, channel, reason=None):
|
||||
"""Sends a part from a PyLink client."""
|
||||
if not self.is_internal_client(client):
|
||||
log.error('(%s) Error trying to part %r from %r (no such client exists)', self.name, client, channel)
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
msg = "PART %s" % channel
|
||||
if reason:
|
||||
msg += " :%s" % reason
|
||||
self._send_with_prefix(client, msg)
|
||||
self.handle_part(client, 'PART', [channel])
|
||||
|
||||
def _ping_uplink(self):
|
||||
"""Sends a PING to the uplink.
|
||||
|
||||
This is mostly used by PyLink internals to check whether the remote link is up."""
|
||||
if self.sid and self.connected.is_set():
|
||||
self._send_with_prefix(self.sid, 'PING %s' % self._expandPUID(self.sid))
|
||||
|
||||
def quit(self, numeric, reason):
|
||||
"""Quits a PyLink client."""
|
||||
if self.is_internal_client(numeric):
|
||||
self._send_with_prefix(numeric, "QUIT :%s" % reason)
|
||||
self._remove_client(numeric)
|
||||
else:
|
||||
raise LookupError("No such PyLink client exists.")
|
||||
|
||||
def message(self, numeric, target, text):
|
||||
"""Sends a PRIVMSG from a PyLink client."""
|
||||
if not self.is_internal_client(numeric):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
|
||||
# Mangle message targets for IRCds that require it.
|
||||
target = self._expandPUID(target)
|
||||
|
||||
self._send_with_prefix(numeric, 'PRIVMSG %s :%s' % (target, text))
|
||||
|
||||
def notice(self, numeric, target, text):
|
||||
"""Sends a NOTICE from a PyLink client or server."""
|
||||
if (not self.is_internal_client(numeric)) and \
|
||||
(not self.is_internal_server(numeric)):
|
||||
raise LookupError('No such PyLink client/server exists.')
|
||||
|
||||
# Mangle message targets for IRCds that require it.
|
||||
target = self._expandPUID(target)
|
||||
|
||||
self._send_with_prefix(numeric, 'NOTICE %s :%s' % (target, text))
|
||||
|
||||
def squit(self, source, target, text='No reason given'):
|
||||
"""SQUITs a PyLink server."""
|
||||
# -> SQUIT 9PZ :blah, blah
|
||||
log.debug('(%s) squit: source=%s, target=%s', self.name, source, target)
|
||||
self._send_with_prefix(source, 'SQUIT %s :%s' % (self._expandPUID(target), text))
|
||||
self.handle_squit(source, 'SQUIT', [target, text])
|
||||
|
||||
def topic(self, source, target, text):
|
||||
"""Sends a TOPIC change from a PyLink client or server."""
|
||||
if (not self.is_internal_client(source)) and (not self.is_internal_server(source)):
|
||||
raise LookupError('No such PyLink client/server exists.')
|
||||
|
||||
self._send_with_prefix(source, 'TOPIC %s :%s' % (target, text))
|
||||
self._channels[target].topic = text
|
||||
self._channels[target].topicset = True
|
||||
topic_burst = topic
|
||||
|
||||
def handle_invite(self, numeric, command, args):
|
||||
"""Handles incoming INVITEs."""
|
||||
# TS6:
|
||||
# <- :70MAAAAAC INVITE 0ALAAAAAA #blah 12345
|
||||
# P10:
|
||||
# <- ABAAA I PyLink-devel #services 1460948992
|
||||
# Note that the target is a nickname, not a numeric.
|
||||
|
||||
target = self._get_UID(args[0])
|
||||
channel = args[1]
|
||||
|
||||
curtime = int(time.time())
|
||||
try:
|
||||
ts = int(args[2])
|
||||
except IndexError:
|
||||
ts = curtime
|
||||
|
||||
ts = ts or curtime # Treat 0 timestamps (e.g. inspircd) as the current time.
|
||||
|
||||
return {'target': target, 'channel': channel, 'ts': ts}
|
||||
|
||||
def handle_kick(self, source, command, args):
|
||||
"""Handles incoming KICKs."""
|
||||
# :70MAAAAAA KICK #test 70MAAAAAA :some reason
|
||||
channel = args[0]
|
||||
kicked = self._get_UID(args[1])
|
||||
|
||||
try:
|
||||
reason = args[2]
|
||||
except IndexError:
|
||||
reason = ''
|
||||
|
||||
log.debug('(%s) Removing kick target %s from %s', self.name, kicked, channel)
|
||||
self.handle_part(kicked, 'KICK', [channel, reason])
|
||||
return {'channel': channel, 'target': kicked, 'text': reason}
|
||||
|
||||
def handle_kill(self, source, command, args):
|
||||
"""Handles incoming KILLs."""
|
||||
killed = self._get_UID(args[0])
|
||||
# Depending on whether the IRCd sends explicit QUIT messages for
|
||||
# killed clients, the user may or may not have automatically been
|
||||
# removed from our user list.
|
||||
# If not, we have to assume that KILL = QUIT and remove them
|
||||
# ourselves.
|
||||
data = self.users.get(killed)
|
||||
if data:
|
||||
self._remove_client(killed)
|
||||
|
||||
# TS6-style kills look something like this:
|
||||
# <- :GL KILL 38QAAAAAA :hidden-1C620195!GL (test)
|
||||
# What we actually want is to format a pretty kill message, in the form
|
||||
# "Killed (killername (reason))".
|
||||
|
||||
if '!' in args[1].split(" ", 1)[0]:
|
||||
try:
|
||||
# Get the nick or server name of the caller.
|
||||
killer = self.get_friendly_name(source)
|
||||
except KeyError:
|
||||
# Killer was... neither? We must have aliens or something. Fallback
|
||||
# to the given "UID".
|
||||
killer = source
|
||||
|
||||
# Get the reason, which is enclosed in brackets.
|
||||
killmsg = ' '.join(args[1].split(" ")[1:])[1:-1]
|
||||
if not killmsg:
|
||||
log.warning('(%s) Failed to extract kill reason: %r', irc.name, args)
|
||||
killmsg = '<No reason given>'
|
||||
else:
|
||||
# We already have a preformatted kill, so just pass it on as is.
|
||||
# XXX: this does create a convoluted kill string if we want to forward kills
|
||||
# over relay.
|
||||
# InspIRCd:
|
||||
# <- :1MLAAAAA1 KILL 0ALAAAAAC :Killed (GL (test))
|
||||
# ngIRCd:
|
||||
# <- :GL KILL PyLink-devel :KILLed by GL: ?
|
||||
killmsg = args[1]
|
||||
|
||||
return {'target': killed, 'text': killmsg, 'userdata': data}
|
||||
|
||||
def _check_cloak_change(self, uid):
|
||||
return
|
||||
|
||||
def _check_umode_away_change(self, uid):
|
||||
# Handle away status changes based on umode +a
|
||||
awaymode = self.umodes.get('away')
|
||||
if uid in self.users and awaymode:
|
||||
u = self.users[uid]
|
||||
old_away_status = u.away
|
||||
|
||||
# Check whether the user is marked away, and send a hook update only if the status has changed.
|
||||
away_status = (awaymode, None) in u.modes
|
||||
if away_status != bool(old_away_status):
|
||||
# This sets a dummy away reason of "Away" because no actual text is provided.
|
||||
self.call_hooks([uid, 'AWAY', {'text': 'Away' if away_status else ''}])
|
||||
|
||||
def _check_oper_status_change(self, uid, modes):
|
||||
if uid in self.users:
|
||||
u = self.users[uid]
|
||||
if 'servprotect' in self.umodes and (self.umodes['servprotect'], None) in u.modes:
|
||||
opertype = 'Network Service'
|
||||
elif 'netadmin' in self.umodes and (self.umodes['netadmin'], None) in u.modes:
|
||||
opertype = 'Network Administrator'
|
||||
elif 'admin' in self.umodes and (self.umodes['admin'], None) in u.modes:
|
||||
opertype = 'Server Administrator'
|
||||
else:
|
||||
opertype = 'IRC Operator'
|
||||
|
||||
if ('+o', None) in modes:
|
||||
self.call_hooks([uid, 'CLIENT_OPERED', {'text': opertype}])
|
||||
|
||||
def handle_mode(self, source, command, args):
|
||||
"""Handles mode changes."""
|
||||
# InspIRCd:
|
||||
# <- :70MAAAAAA MODE 70MAAAAAA -i+xc
|
||||
|
||||
# P10:
|
||||
# <- ABAAA M GL -w
|
||||
# <- ABAAA M #test +v ABAAB 1460747615
|
||||
# <- ABAAA OM #test +h ABAAA
|
||||
target = self._get_UID(args[0])
|
||||
if self.is_channel(target):
|
||||
channeldata = self._channels[target].deepcopy()
|
||||
else:
|
||||
channeldata = None
|
||||
|
||||
modestrings = args[1:]
|
||||
changedmodes = self.parse_modes(target, modestrings)
|
||||
self.apply_modes(target, changedmodes)
|
||||
|
||||
if target in self.users:
|
||||
# Target was a user. Check for any cloak and away status changes.
|
||||
self._check_cloak_change(target)
|
||||
self._check_umode_away_change(target)
|
||||
self._check_oper_status_change(target, changedmodes)
|
||||
|
||||
return {'target': target, 'modes': changedmodes, 'channeldata': channeldata}
|
||||
|
||||
def handle_part(self, source, command, args):
|
||||
"""Handles incoming PART commands."""
|
||||
channels = args[0].split(',')
|
||||
|
||||
for channel in channels.copy():
|
||||
if channel not in self._channels or source not in self._channels[channel].users:
|
||||
# Ignore channels the user isn't on, and remove them from any hook payloads.
|
||||
channels.remove(channel)
|
||||
|
||||
self._channels[channel].remove_user(source)
|
||||
try:
|
||||
self.users[source].channels.discard(channel)
|
||||
except KeyError:
|
||||
log.debug("(%s) handle_part: KeyError trying to remove %r from %r's channel list?", self.name, channel, source)
|
||||
|
||||
try:
|
||||
reason = args[1]
|
||||
except IndexError:
|
||||
reason = ''
|
||||
|
||||
if channels:
|
||||
return {'channels': channels, 'text': reason}
|
||||
|
||||
def handle_privmsg(self, source, command, args):
|
||||
"""Handles incoming PRIVMSG/NOTICE."""
|
||||
# TS6:
|
||||
@ -86,91 +667,91 @@ class IRCS2SProtocol(Protocol):
|
||||
# P10:
|
||||
# <- ABAAA P AyAAA :privmsg text
|
||||
# <- ABAAA O AyAAA :notice text
|
||||
target = self._getUid(args[0])
|
||||
raw_target = args[0]
|
||||
server_check = None
|
||||
if '@' in raw_target and not self.is_channel(raw_target.lstrip(''.join(self.prefixmodes.values()))):
|
||||
log.debug('(%s) Processing user@server message with target %s',
|
||||
self.name, raw_target)
|
||||
raw_target, server_check = raw_target.split('@', 1)
|
||||
|
||||
if not self.is_server_name(server_check):
|
||||
log.warning('(%s) Got user@server message with invalid server '
|
||||
'name %r (full target: %r)', self.name, server_check,
|
||||
args[0])
|
||||
return
|
||||
|
||||
target = self._get_UID(raw_target)
|
||||
|
||||
if server_check is not None:
|
||||
not_found = False
|
||||
if target not in self.users:
|
||||
# Most IRCds don't check locally if the target nick actually exists.
|
||||
# If it doesn't, send an error back.
|
||||
not_found = True
|
||||
else:
|
||||
# I guess we can technically leave this up to the IRCd to do the right
|
||||
# checks here, but maybe that ruins the point of this 'security feature'
|
||||
# in the first place.
|
||||
log.debug('(%s) Checking if target %s/%s exists on server %s',
|
||||
self.name, target, raw_target, server_check)
|
||||
sid = self._get_SID(server_check)
|
||||
|
||||
if not sid:
|
||||
log.debug('(%s) Failed user@server server check: %s does not exist.',
|
||||
self.name, server_check)
|
||||
not_found = True
|
||||
elif sid != self.get_server(target):
|
||||
log.debug("(%s) Got user@server message for %s/%s, but they "
|
||||
"aren't on the server %s/%s. (full target: %r)",
|
||||
self.name, target, raw_target, server_check, sid,
|
||||
args[0])
|
||||
not_found = True
|
||||
|
||||
if not_found:
|
||||
self.numeric(self.sid, 401, source, '%s :No such nick' %
|
||||
args[0])
|
||||
return
|
||||
|
||||
# Coerse =#channel from Charybdis op moderated +z to @#channel.
|
||||
if target.startswith('='):
|
||||
target = '@' + target[1:]
|
||||
|
||||
# We use lowercase channels internally, but uppercase UIDs.
|
||||
# Strip the target of leading prefix modes (for targets like @#channel)
|
||||
# before checking whether it's actually a channel.
|
||||
split_channel = target.split('#', 1)
|
||||
if len(split_channel) >= 2 and utils.isChannel('#' + split_channel[1]):
|
||||
# Note: don't mess with the case of the channel prefix, or ~#channel
|
||||
# messages will break on RFC1459 casemapping networks (it becomes ^#channel
|
||||
# instead).
|
||||
target = '#'.join((split_channel[0], self.irc.toLower(split_channel[1])))
|
||||
log.debug('(%s) Normalizing channel target %s to %s', self.irc.name, args[0], target)
|
||||
|
||||
return {'target': target, 'text': args[1]}
|
||||
|
||||
handle_notice = handle_privmsg
|
||||
|
||||
def check_nick_collision(self, nick):
|
||||
"""
|
||||
Nick collision checker.
|
||||
"""
|
||||
uid = self.irc.nickToUid(nick)
|
||||
# If there is a nick collision, we simply alert plugins. Relay will purposely try to
|
||||
# lose fights and tag nicks instead, while other plugins can choose how to handle this.
|
||||
if uid:
|
||||
log.info('(%s) Nick collision on %s/%s, forwarding this to plugins', self.irc.name,
|
||||
uid, nick)
|
||||
self.irc.callHooks([self.irc.sid, 'SAVE', {'target': uid}])
|
||||
|
||||
def handle_kill(self, source, command, args):
|
||||
"""Handles incoming KILLs."""
|
||||
killed = args[0]
|
||||
# Depending on whether the IRCd sends explicit QUIT messages for
|
||||
# killed clients, the user may or may not have automatically been
|
||||
# removed from our user list.
|
||||
# If not, we have to assume that KILL = QUIT and remove them
|
||||
# ourselves.
|
||||
data = self.irc.users.get(killed)
|
||||
if data:
|
||||
self.removeClient(killed)
|
||||
|
||||
# TS6-style kills look something like this:
|
||||
# <- :GL KILL 38QAAAAAA :hidden-1C620195!GL (test)
|
||||
# What we actually want is to format a pretty kill message, in the form
|
||||
# "Killed (killername (reason))".
|
||||
|
||||
try:
|
||||
# Get the nick or server name of the caller.
|
||||
killer = self.irc.getFriendlyName(source)
|
||||
except KeyError:
|
||||
# Killer was... neither? We must have aliens or something. Fallback
|
||||
# to the given "UID".
|
||||
killer = source
|
||||
|
||||
# Get the reason, which is enclosed in brackets.
|
||||
reason = ' '.join(args[1].split(" ")[1:])
|
||||
|
||||
killmsg = "Killed (%s %s)" % (killer, reason)
|
||||
|
||||
return {'target': killed, 'text': killmsg, 'userdata': data}
|
||||
|
||||
def handle_squit(self, numeric, command, args):
|
||||
"""Handles incoming SQUITs."""
|
||||
return self._squit(numeric, command, args)
|
||||
|
||||
def handle_away(self, numeric, command, args):
|
||||
"""Handles incoming AWAY messages."""
|
||||
def handle_quit(self, numeric, command, args):
|
||||
"""Handles incoming QUIT commands."""
|
||||
# TS6:
|
||||
# <- :6ELAAAAAB AWAY :Auto-away
|
||||
# <- :1SRAAGB4T QUIT :Quit: quit message goes here
|
||||
# P10:
|
||||
# <- ABAAA A :blah
|
||||
# <- ABAAA A
|
||||
try:
|
||||
self.irc.users[numeric].away = text = args[0]
|
||||
except IndexError: # User is unsetting away status
|
||||
self.irc.users[numeric].away = text = ''
|
||||
return {'text': text}
|
||||
# <- ABAAB Q :Killed (GL_ (bangbang))
|
||||
self._remove_client(numeric)
|
||||
return {'text': args[0]}
|
||||
|
||||
def handle_version(self, numeric, command, args):
|
||||
"""Handles requests for the PyLink server version."""
|
||||
return {} # See coremods/handlers.py for how this hook is used
|
||||
def handle_stats(self, numeric, command, args):
|
||||
"""Handles the IRC STATS command."""
|
||||
# IRCds are mostly consistent with this syntax, with the caller being the source,
|
||||
# the stats type as arg 0, and the target server (SID or hostname) as arg 1
|
||||
# <- :42XAAAAAB STATS c :7PY
|
||||
return {'stats_type': args[0], 'target': self._get_SID(args[1])}
|
||||
|
||||
def handle_topic(self, numeric, command, args):
|
||||
"""Handles incoming TOPIC changes from clients."""
|
||||
# <- :70MAAAAAA TOPIC #test :test
|
||||
channel = args[0]
|
||||
topic = args[1]
|
||||
|
||||
oldtopic = self._channels[channel].topic
|
||||
self._channels[channel].topic = topic
|
||||
self._channels[channel].topicset = True
|
||||
|
||||
return {'channel': channel, 'setter': numeric, 'text': topic,
|
||||
'oldtopic': oldtopic}
|
||||
|
||||
def handle_time(self, numeric, command, args):
|
||||
"""Handles incoming /TIME requests."""
|
||||
return {'target': args[0]}
|
||||
|
||||
def handle_whois(self, numeric, command, args):
|
||||
"""Handles incoming WHOIS commands.."""
|
||||
@ -184,22 +765,8 @@ class IRCS2SProtocol(Protocol):
|
||||
# WHOIS commands received are for us, since we don't host any real servers
|
||||
# to route it to.
|
||||
|
||||
return {'target': self._getUid(args[-1])}
|
||||
return {'target': self._get_UID(args[-1])}
|
||||
|
||||
def handle_quit(self, numeric, command, args):
|
||||
"""Handles incoming QUIT commands."""
|
||||
# TS6:
|
||||
# <- :1SRAAGB4T QUIT :Quit: quit message goes here
|
||||
# P10:
|
||||
# <- ABAAB Q :Killed (GL_ (bangbang))
|
||||
self.removeClient(numeric)
|
||||
return {'text': args[0]}
|
||||
|
||||
def handle_time(self, numeric, command, args):
|
||||
"""Handles incoming /TIME requests."""
|
||||
return {'target': args[0]}
|
||||
|
||||
def handle_pong(self, source, command, args):
|
||||
"""Handles incoming PONG commands."""
|
||||
if source == self.irc.uplink:
|
||||
self.irc.lastping = time.time()
|
||||
def handle_version(self, numeric, command, args):
|
||||
"""Handles requests for the PyLink server version."""
|
||||
return {} # See coremods/handlers.py for how this hook is used
|
||||
|
@ -6,11 +6,11 @@ from pylinkirc.log import log
|
||||
from pylinkirc.protocols.p10 import *
|
||||
|
||||
class NefariousProtocol(P10Protocol):
|
||||
def __init__(self, irc):
|
||||
super().__init__(irc)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
log.warning("(%s) protocols/nefarious.py has been renamed to protocols/p10.py, which "
|
||||
"now also supports other IRCu variants. Please update your configuration, "
|
||||
"as this migration stub will be removed in a future version.",
|
||||
self.irc.name)
|
||||
self.name)
|
||||
|
||||
Class = NefariousProtocol
|
||||
|
549
protocols/ngircd.py
Normal file
549
protocols/ngircd.py
Normal file
@ -0,0 +1,549 @@
|
||||
"""
|
||||
ngircd.py: PyLink protocol module for ngIRCd.
|
||||
"""
|
||||
##
|
||||
# Server protocol docs for ngIRCd can be found at:
|
||||
# https://github.com/ngircd/ngircd/blob/master/doc/Protocol.txt
|
||||
# and https://tools.ietf.org/html/rfc2813
|
||||
##
|
||||
|
||||
import time
|
||||
import re
|
||||
|
||||
from pylinkirc import utils, conf, __version__
|
||||
from pylinkirc.classes import *
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.protocols.ircs2s_common import *
|
||||
|
||||
class NgIRCdProtocol(IRCS2SProtocol):
|
||||
def __init__(self, irc):
|
||||
super().__init__(irc)
|
||||
|
||||
self.conf_keys -= {'sid', 'sidrange'}
|
||||
self.casemapping = 'ascii' # This is the default; it's actually set on server negotiation
|
||||
self.hook_map = {'NJOIN': 'JOIN'}
|
||||
|
||||
# Track whether we've received end-of-burst from the uplink.
|
||||
self.has_eob = False
|
||||
|
||||
self._caps = {}
|
||||
self._use_builtin_005_handling = True
|
||||
|
||||
# ngIRCd has no TS tracking.
|
||||
self.protocol_caps.discard('has-ts')
|
||||
|
||||
# Slash in nicks is problematic; while it works for basic things like JOIN and messages,
|
||||
# attempts to set user modes fail.
|
||||
self.protocol_caps |= {'slash-in-hosts', 'underscore-in-hosts'}
|
||||
|
||||
### Commands
|
||||
|
||||
def post_connect(self):
|
||||
self.send('PASS %s 0210-IRC+ PyLink|%s:CHLMoX' % (self.serverdata['sendpass'], __version__))
|
||||
|
||||
# Note: RFC 2813 mandates another server token value after the hopcount (1), but ngIRCd
|
||||
# doesn't follow that behaviour per https://github.com/ngircd/ngircd/issues/224
|
||||
self.send("SERVER %s 1 :%s" % (self.serverdata['hostname'],
|
||||
self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']))
|
||||
|
||||
self._uidgen = PUIDGenerator('PUID')
|
||||
|
||||
# The first "SID" this generator should return is 2, because server token 1 is implied to be
|
||||
# the main PyLink server. RFC2813 has no official definition of SIDs, but rather uses
|
||||
# integer tokens in the SERVER and NICK (user introduction) commands to keep track of which
|
||||
# user exists on which server. Why did they do it this way? Who knows!
|
||||
self._sidgen = PUIDGenerator('PSID', start=1)
|
||||
self.sid = self._sidgen.next_sid(prefix=self.serverdata['hostname'])
|
||||
|
||||
self._caps.clear()
|
||||
|
||||
self.cmodes.update({
|
||||
'banexception': 'e', 'invex': 'I', 'regmoderated': 'M', 'nonick': 'N',
|
||||
'operonly': 'O', 'permanent': 'P', 'nokick': 'Q', 'registered': 'r',
|
||||
'regonly': 'R', 'noinvite': 'V', 'sslonly': 'z'
|
||||
})
|
||||
|
||||
self.umodes.update({
|
||||
'away': 'a', 'deaf': 'b', 'bot': 'B', 'sno_clientconnections': 'c',
|
||||
'deaf_commonchan': 'C', 'floodexempt': 'f', 'hidechans': 'I',
|
||||
'servprotect': 'q', 'restricted': 'r', 'registered': 'R', 'cloak': 'x'
|
||||
})
|
||||
|
||||
def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(),
|
||||
server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator',
|
||||
manipulatable=False):
|
||||
"""
|
||||
Spawns a new client with the given options.
|
||||
|
||||
Note: No nick collision / valid nickname checks are done here; it is
|
||||
up to plugins to make sure they don't introduce anything invalid.
|
||||
|
||||
Note 2: IP and realhost are ignored because ngIRCd does not send them.
|
||||
"""
|
||||
server = server or self.sid
|
||||
assert '@' in server, "Need PSID for spawn_client, not pure server name!"
|
||||
if not self.is_internal_server(server):
|
||||
raise ValueError('Server %r is not a PyLink server!' % server)
|
||||
|
||||
realname = realname or conf.conf['pylink']['realname']
|
||||
|
||||
uid = self._uidgen.next_uid(prefix=nick)
|
||||
userobj = self.users[uid] = User(self, nick, ts or int(time.time()), uid, server,
|
||||
ident=ident, host=host, realname=realname,
|
||||
manipulatable=manipulatable, opertype=opertype,
|
||||
realhost=host)
|
||||
|
||||
self.apply_modes(uid, modes)
|
||||
self.servers[server].users.add(uid)
|
||||
|
||||
# Grab our server token; this is used instead of server name to denote where the client is.
|
||||
server_token = server.rsplit('@')[-1]
|
||||
# <- :ngircd.midnight.local NICK GL 1 ~gl localhost 1 +io :realname
|
||||
self._send_with_prefix(server, 'NICK %s %s %s %s %s %s :%s' % (nick, self.servers[server].hopcount,
|
||||
ident, host, server_token, self.join_modes(modes), realname))
|
||||
return userobj
|
||||
|
||||
def spawn_server(self, name, sid=None, uplink=None, desc=None):
|
||||
"""
|
||||
Spawns a server off a PyLink server.
|
||||
|
||||
* desc (server description) defaults to the one in the config.
|
||||
* uplink defaults to the main PyLink server.
|
||||
* SID is set equal to the server name for ngIRCd, as server UIDs are not used.
|
||||
"""
|
||||
uplink = uplink or self.sid
|
||||
assert uplink in self.servers, "Unknown uplink %r?" % uplink
|
||||
name = name.lower()
|
||||
sid = self._sidgen.next_sid(prefix=name)
|
||||
|
||||
desc = desc or self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']
|
||||
|
||||
if sid in self.servers:
|
||||
raise ValueError('A server named %r already exists!' % sid)
|
||||
|
||||
if not self.is_internal_server(uplink):
|
||||
raise ValueError('Server %r is not a PyLink server!' % uplink)
|
||||
|
||||
if not self.is_server_name(name):
|
||||
raise ValueError('Invalid server name %r' % name)
|
||||
|
||||
# https://tools.ietf.org/html/rfc2813#section-4.1.2
|
||||
# We need to store a server token to introduce clients on the right server. Since this is just
|
||||
# a number, we can simply use the counter in our PSID generator for this.
|
||||
server_token = sid.rsplit('@')[-1]
|
||||
self.servers[sid] = Server(self, uplink, name, internal=True, desc=desc)
|
||||
self._send_with_prefix(uplink, 'SERVER %s %s %s :%s' % (name, self.servers[sid].hopcount, server_token, desc))
|
||||
return sid
|
||||
|
||||
def away(self, source, text):
|
||||
"""Sends an AWAY message from a PyLink client. If the text is empty, away status is unset."""
|
||||
if not self.is_internal_client(source):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
|
||||
# Away status is denoted on ngIRCd with umode +a.
|
||||
modes = self.users[source].modes
|
||||
if text and (('a', None) not in modes):
|
||||
# Set umode +a if it isn't already set
|
||||
self.mode(source, source, [('+a', None)])
|
||||
elif ('a', None) in modes:
|
||||
# Ditto, only unset the mode if it *was* set.
|
||||
self.mode(source, source, [('-a', None)])
|
||||
self.users[source].away = text
|
||||
|
||||
def join(self, client, channel):
|
||||
|
||||
if not self.is_internal_client(client):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
|
||||
self._send_with_prefix(client, "JOIN %s" % channel)
|
||||
self._channels[channel].users.add(client)
|
||||
self.users[client].channels.add(channel)
|
||||
|
||||
def kill(self, source, target, reason):
|
||||
"""Sends a kill from a PyLink client/server."""
|
||||
if (not self.is_internal_client(source)) and \
|
||||
(not self.is_internal_server(source)):
|
||||
raise LookupError('No such PyLink client/server exists.')
|
||||
|
||||
# Follow ngIRCd's formatting of the kill messages for the most part
|
||||
self._send_with_prefix(source, 'KILL %s :KILLed by %s: %s' % (self._expandPUID(target),
|
||||
self.get_friendly_name(source), reason))
|
||||
|
||||
# Implicitly remove our own client if one was the target.
|
||||
if self.is_internal_client(target):
|
||||
self._remove_client(target)
|
||||
|
||||
def knock(self, numeric, target, text):
|
||||
raise NotImplementedError('KNOCK is not supported on ngIRCd.')
|
||||
|
||||
def mode(self, source, target, modes, ts=None):
|
||||
"""Sends mode changes from a PyLink client/server. The TS option is not used on ngIRCd."""
|
||||
|
||||
if (not self.is_internal_client(source)) and \
|
||||
(not self.is_internal_server(source)):
|
||||
raise LookupError('No such PyLink client/server %r exists' % source)
|
||||
|
||||
self.apply_modes(target, modes)
|
||||
modes = list(modes) # Work around TypeError in the expand PUID section
|
||||
|
||||
if self.is_channel(target):
|
||||
msgprefix = ':%s MODE %s ' % (self._expandPUID(source), target)
|
||||
bufsize = self.S2S_BUFSIZE - len(msgprefix)
|
||||
|
||||
# Expand PUIDs when sending outgoing prefix modes.
|
||||
for idx, mode in enumerate(modes):
|
||||
if mode[0][-1] in self.prefixmodes:
|
||||
log.debug('(%s) mode: expanding PUID of mode %s', self.name, str(mode))
|
||||
modes[idx] = (mode[0], self._expandPUID(mode[1]))
|
||||
|
||||
for modestr in self.wrap_modes(modes, bufsize, max_modes_per_msg=12):
|
||||
self.send(msgprefix + modestr)
|
||||
else:
|
||||
joinedmodes = self.join_modes(modes)
|
||||
self._send_with_prefix(source, 'MODE %s %s' % (self._expandPUID(target), joinedmodes))
|
||||
|
||||
def nick(self, source, newnick):
|
||||
"""Changes the nick of a PyLink client."""
|
||||
if not self.is_internal_client(source):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
|
||||
self._send_with_prefix(source, 'NICK %s' % newnick)
|
||||
|
||||
self.users[source].nick = newnick
|
||||
|
||||
# Update the nick TS for consistency with other protocols (it isn't actually used in S2S)
|
||||
self.users[source].ts = int(time.time())
|
||||
|
||||
def sjoin(self, server, channel, users, ts=None, modes=set()):
|
||||
"""Sends an SJOIN for a group of users to a channel.
|
||||
|
||||
The sender should always be a Server ID (SID). TS is optional, and defaults
|
||||
to the one we've stored in the channel state if not given.
|
||||
<users> is a list of (prefix mode, UID) pairs:
|
||||
|
||||
Example uses:
|
||||
sjoin('100', '#test', [('', 'user0@0'), ('o', user1@1'), ('v', 'someone@2')])
|
||||
sjoin(self.sid, '#test', [('o', self.pseudoclient.uid)])
|
||||
"""
|
||||
|
||||
server = server or self.sid
|
||||
if not server:
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
log.debug('(%s) sjoin: got %r for users', self.name, users)
|
||||
|
||||
njoin_prefix = ':%s NJOIN %s :' % (self._expandPUID(server), channel)
|
||||
# Format the user list into strings such as @user1, +user2, user3, etc.
|
||||
nicks_to_send = []
|
||||
for userpair in users:
|
||||
prefixes, uid = userpair
|
||||
|
||||
if uid not in self.users:
|
||||
log.warning('(%s) Trying to NJOIN missing user %s?', self.name, uid)
|
||||
continue
|
||||
elif uid in self._channels[channel].users:
|
||||
# Don't rejoin users already in the channel, this causes errors with ngIRCd.
|
||||
continue
|
||||
|
||||
self._channels[channel].users.add(uid)
|
||||
self.users[uid].channels.add(channel)
|
||||
|
||||
self.apply_modes(channel, (('+%s' % prefix, uid) for prefix in userpair[0]))
|
||||
|
||||
nicks_to_send.append(''.join(self.prefixmodes[modechar] for modechar in userpair[0]) + \
|
||||
self._expandPUID(userpair[1]))
|
||||
|
||||
if nicks_to_send:
|
||||
# Use 13 args max per line: this is equal to the max of 15 minus the command name and target channel.
|
||||
for message in utils.wrap_arguments(njoin_prefix, nicks_to_send, self.S2S_BUFSIZE, separator=',', max_args_per_line=13):
|
||||
self.send(message)
|
||||
|
||||
if modes:
|
||||
# Burst modes separately if there are any.
|
||||
log.debug("(%s) sjoin: bursting modes %r for channel %r now", self.name, modes, channel)
|
||||
self.mode(server, channel, modes)
|
||||
|
||||
def set_server_ban(self, source, duration, user='*', host='*', reason='User banned'):
|
||||
"""
|
||||
Sets a server ban.
|
||||
"""
|
||||
# <- :GL GLINE *!*@bad.user 3d :test
|
||||
assert not (user == host == '*'), "Refusing to set ridiculous ban on *@*"
|
||||
self._send_with_prefix(source, 'GLINE *!%s@%s %s :%s' % (user, host, duration, reason))
|
||||
|
||||
def update_client(self, target, field, text):
|
||||
"""Updates the ident, host, or realname of any connected client."""
|
||||
field = field.upper()
|
||||
|
||||
if field not in ('IDENT', 'HOST', 'REALNAME', 'GECOS'):
|
||||
raise NotImplementedError("Changing field %r of a client is "
|
||||
"unsupported by this protocol." % field)
|
||||
|
||||
real_target = self._expandPUID(target)
|
||||
if field == 'IDENT':
|
||||
self.users[target].ident = text
|
||||
self._send_with_prefix(self.sid, 'METADATA %s user :%s' % (real_target, text))
|
||||
|
||||
if not self.is_internal_client(target):
|
||||
# If the target wasn't one of our clients, send a hook payload for other plugins to listen to.
|
||||
self.call_hooks([self.sid, 'CHGIDENT', {'target': target, 'newident': text}])
|
||||
|
||||
elif field == 'HOST':
|
||||
self.users[target].host = text
|
||||
|
||||
if self.is_internal_client(target):
|
||||
# For our own clients, replace the real host.
|
||||
self._send_with_prefix(self.sid, 'METADATA %s host :%s' % (real_target, text))
|
||||
else:
|
||||
# For others, update the cloaked host and force a umode +x.
|
||||
self._send_with_prefix(self.sid, 'METADATA %s cloakhost :%s' % (real_target, text))
|
||||
|
||||
if ('x', None) not in self.users[target].modes:
|
||||
log.debug('(%s) Forcing umode +x on %r as part of cloak setting', self.name, target)
|
||||
self.mode(self.sid, target, [('+x', None)])
|
||||
|
||||
self.call_hooks([self.sid, 'CHGHOST', {'target': target, 'newhost': text}])
|
||||
|
||||
elif field in ('REALNAME', 'GECOS'):
|
||||
self.users[target].realname = text
|
||||
self._send_with_prefix(self.sid, 'METADATA %s info :%s' % (real_target, text))
|
||||
|
||||
if not self.is_internal_client(target):
|
||||
self.call_hooks([self.sid, 'CHGNAME', {'target': target, 'newgecos': text}])
|
||||
|
||||
### Handlers
|
||||
|
||||
def handle_376(self, source, command, args):
|
||||
# 376 is used to denote end of server negotiation - we send our info back at this point.
|
||||
# <- :ngircd.midnight.local 005 pylink-devel.int NETWORK=ngircd-test :is my network name
|
||||
# <- :ngircd.midnight.local 005 pylink-devel.int RFC2812 IRCD=ngIRCd CHARSET=UTF-8 CASEMAPPING=ascii PREFIX=(qaohv)~&@%+ CHANTYPES=#&+ CHANMODES=beI,k,l,imMnOPQRstVz CHANLIMIT=#&+:10 :are supported on this server
|
||||
# <- :ngircd.midnight.local 005 pylink-devel.int CHANNELLEN=50 NICKLEN=21 TOPICLEN=490 AWAYLEN=127 KICKLEN=400 MODES=5 MAXLIST=beI:50 EXCEPTS=e INVEX=I PENALTY :are supported on this server
|
||||
def f(numeric, msg):
|
||||
self._send_with_prefix(self.sid, '%s %s %s' % (numeric, self.uplink, msg))
|
||||
f('005', 'NETWORK=%s :is my network name' % self.get_full_network_name())
|
||||
f('005', 'RFC2812 IRCD=PyLink CHARSET=UTF-8 CASEMAPPING=%s PREFIX=%s CHANTYPES=# '
|
||||
'CHANMODES=%s,%s,%s,%s :are supported on this server' % (self.casemapping, self._caps['PREFIX'],
|
||||
self.cmodes['*A'], self.cmodes['*B'], self.cmodes['*C'], self.cmodes['*D']))
|
||||
f('005', 'CHANNELLEN NICKLEN=%s EXCEPTS=E INVEX=I :are supported on this server' % self.maxnicklen)
|
||||
|
||||
# 376 (end of MOTD) marks the end of extended server negotiation per
|
||||
# https://github.com/ngircd/ngircd/blob/master/doc/Protocol.txt#L103-L112
|
||||
f('376', ":End of server negotiation, happy PyLink'ing!")
|
||||
|
||||
def handle_chaninfo(self, source, command, args):
|
||||
# https://github.com/ngircd/ngircd/blob/722afc1b810cef74dbd2738d71866176fd974ec2/doc/Protocol.txt#L146-L159
|
||||
# CHANINFO has 3 styles depending on the amount of information applicable to a channel:
|
||||
# CHANINFO <channel> +<modes>
|
||||
# CHANINFO <channel> +<modes> <topic>
|
||||
# CHANINFO <channel> +<modes> <key> <limit> <topic>
|
||||
# If there is no key, the key is "*". If there is no limit, the limit is "0".
|
||||
|
||||
channel = args[0]
|
||||
# Get rid of +l and +k in the initial parsing; we handle that later by looking at the CHANINFO arguments
|
||||
modes = self.parse_modes(channel, args[1].replace('l', '').replace('k', ''))
|
||||
|
||||
if len(args) >= 3:
|
||||
topic = args[-1]
|
||||
if topic:
|
||||
log.debug('(%s) handle_chaninfo: setting topic for %s to %r', self.name, channel, topic)
|
||||
self._channels[channel].topic = topic
|
||||
self._channels[channel].topicset = True
|
||||
|
||||
if len(args) >= 5:
|
||||
key = args[2]
|
||||
limit = args[3]
|
||||
if key != '*':
|
||||
modes.append(('+k', key))
|
||||
if limit != '0':
|
||||
modes.append(('+l', limit))
|
||||
|
||||
self.apply_modes(channel, modes)
|
||||
|
||||
def handle_join(self, source, command, args):
|
||||
# RFC 2813 is odd to say the least... https://tools.ietf.org/html/rfc2813#section-4.2.1
|
||||
# Basically, we expect messages of the forms:
|
||||
# <- :GL JOIN #test\x07o
|
||||
# <- :GL JOIN #moretest
|
||||
for chanpair in args[0].split(','):
|
||||
# Normalize channel case.
|
||||
try:
|
||||
channel, status = chanpair.split('\x07', 1)
|
||||
if status in 'ov':
|
||||
self.apply_modes(channel, [('+' + status, source)])
|
||||
except ValueError:
|
||||
channel = chanpair
|
||||
|
||||
c = self._channels[channel]
|
||||
|
||||
self.users[source].channels.add(channel)
|
||||
self._channels[channel].users.add(source)
|
||||
|
||||
# Call hooks manually, because one JOIN command have multiple channels.
|
||||
self.call_hooks([source, command, {'channel': channel, 'users': [source], 'modes': c.modes}])
|
||||
|
||||
def handle_kill(self, source, command, args):
|
||||
"""Handles incoming KILLs."""
|
||||
|
||||
# ngIRCd sends QUIT after KILL for its own clients, so we shouldn't process this by itself
|
||||
# unless we're the target.
|
||||
killed = self._get_UID(args[0])
|
||||
if self.is_internal_client(killed):
|
||||
return super().handle_kill(source, command, args)
|
||||
else:
|
||||
log.debug("(%s) Ignoring KILL to %r as it isn't meant for us; we should see a QUIT soon",
|
||||
self.name, killed)
|
||||
|
||||
def _check_cloak_change(self, target):
|
||||
u = self.users[target]
|
||||
old_host = u.host
|
||||
|
||||
if ('x', None) in u.modes and u.cloaked_host:
|
||||
u.host = u.cloaked_host
|
||||
elif u.realhost:
|
||||
u.host = u.realhost
|
||||
|
||||
# Something changed, so send a CHGHOST hook
|
||||
if old_host != u.host:
|
||||
self.call_hooks([target, 'CHGHOST', {'target': target, 'newhost': u.host}])
|
||||
|
||||
def handle_metadata(self, source, command, args):
|
||||
"""Handles various user metadata for ngIRCd (cloaked host, account name, etc.)"""
|
||||
# <- :ngircd.midnight.local METADATA GL cloakhost :hidden-3a2a739e.ngircd.midnight.local
|
||||
target = self._get_UID(args[0])
|
||||
|
||||
if target not in self.users:
|
||||
log.warning("(%s) Ignoring METADATA to missing user %r?", self.name, target)
|
||||
return
|
||||
|
||||
datatype = args[1]
|
||||
u = self.users[target]
|
||||
|
||||
if datatype == 'cloakhost': # Set cloaked host
|
||||
u.cloaked_host = args[-1]
|
||||
self._check_cloak_change(target)
|
||||
|
||||
elif datatype == 'host': # Host changing. This actually sets the "real host" that ngIRCd stores
|
||||
u.realhost = args[-1]
|
||||
self._check_cloak_change(target)
|
||||
|
||||
elif datatype == 'user': # Ident changing
|
||||
u.ident = args[-1]
|
||||
self.call_hooks([target, 'CHGIDENT', {'target': target, 'newident': args[-1]}])
|
||||
|
||||
elif datatype == 'info': # Realname changing
|
||||
u.realname = args[-1]
|
||||
self.call_hooks([target, 'CHGNAME', {'target': target, 'newgecos': args[-1]}])
|
||||
|
||||
elif datatype == 'accountname': # Services account
|
||||
self.call_hooks([target, 'CLIENT_SERVICES_LOGIN', {'text': args[-1]}])
|
||||
|
||||
def handle_nick(self, source, command, args):
|
||||
"""
|
||||
Handles the NICK command, used for server introductions and nick changes.
|
||||
"""
|
||||
if len(args) >= 2:
|
||||
# User introduction:
|
||||
# <- :ngircd.midnight.local NICK GL 1 ~gl localhost 1 +io :realname
|
||||
nick = args[0]
|
||||
assert source in self.servers, "Server %r tried to introduce nick %r but isn't in the servers index?" % (source, nick)
|
||||
self._check_nick_collision(nick)
|
||||
|
||||
ident = args[2]
|
||||
host = args[3]
|
||||
uid = self._uidgen.next_uid(prefix=nick)
|
||||
realname = args[-1]
|
||||
|
||||
ts = int(time.time())
|
||||
self.users[uid] = User(self, nick, ts, uid, source, ident=ident, host=host,
|
||||
realname=realname, realhost=host)
|
||||
parsedmodes = self.parse_modes(uid, [args[5]])
|
||||
self.apply_modes(uid, parsedmodes)
|
||||
|
||||
# Add the nick to the list of users on its server; this is used for SQUIT tracking
|
||||
self.servers[source].users.add(uid)
|
||||
|
||||
# Check away status and cloaked host changes
|
||||
self._check_umode_away_change(uid)
|
||||
self._check_cloak_change(uid)
|
||||
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': host, 'host': host, 'ident': ident,
|
||||
'parse_as': 'UID', 'ip': '0.0.0.0'}
|
||||
else:
|
||||
# Nick changes:
|
||||
# <- :GL NICK :GL_
|
||||
oldnick = self.users[source].nick
|
||||
newnick = self.users[source].nick = args[0]
|
||||
return {'newnick': newnick, 'oldnick': oldnick}
|
||||
|
||||
def handle_njoin(self, source, command, args):
|
||||
# <- :ngircd.midnight.local NJOIN #test :tester,@%GL
|
||||
|
||||
channel = args[0]
|
||||
chandata = self._channels[channel].deepcopy()
|
||||
namelist = []
|
||||
|
||||
# Reverse the modechar->modeprefix mapping for quicker lookup
|
||||
prefixchars = {v: k for k, v in self.prefixmodes.items()}
|
||||
for userpair in args[1].split(','):
|
||||
# Some regex magic to split the prefix from the nick.
|
||||
r = re.search(r'([%s]*)(.*)' % ''.join(self.prefixmodes.values()), userpair)
|
||||
user = self._get_UID(r.group(2))
|
||||
modeprefix = r.group(1)
|
||||
|
||||
if modeprefix:
|
||||
modes = {('+%s' % prefixchars[mode], user) for mode in modeprefix}
|
||||
self.apply_modes(channel, modes)
|
||||
namelist.append(user)
|
||||
|
||||
# Final bits of state tracking. (I hate having to do this everywhere...)
|
||||
self.users[user].channels.add(channel)
|
||||
self._channels[channel].users.add(user)
|
||||
|
||||
return {'channel': channel, 'users': namelist, 'modes': [], 'channeldata': chandata}
|
||||
|
||||
def handle_pass(self, source, command, args):
|
||||
"""
|
||||
Handles phase one of the ngIRCd login process (password auth and version info).
|
||||
"""
|
||||
# PASS is step one of server introduction, and is used to send the server info and password.
|
||||
# <- :ngircd.midnight.local PASS xyzpassword 0210-IRC+ ngIRCd|24~3-gbc728f92:CHLMSXZ PZ
|
||||
recvpass = args[0]
|
||||
if recvpass != self.serverdata['recvpass']:
|
||||
raise ProtocolError("RECVPASS from uplink does not match configuration!")
|
||||
|
||||
assert 'IRC+' in args[1], "Linking to non-ngIRCd server using this protocol module is not supported"
|
||||
|
||||
def handle_ping(self, source, command, args):
|
||||
"""
|
||||
Handles incoming PINGs (and implicit end of burst).
|
||||
"""
|
||||
self._send_with_prefix(self.sid, 'PONG %s :%s' % (self._expandPUID(self.sid), args[-1]), queue=False)
|
||||
|
||||
if not self.servers[source].has_eob:
|
||||
# Treat the first PING we receive as end of burst.
|
||||
self.servers[source].has_eob = True
|
||||
|
||||
if source == self.uplink:
|
||||
self.connected.set()
|
||||
|
||||
# Return the endburst hook.
|
||||
return {'parse_as': 'ENDBURST'}
|
||||
|
||||
def handle_server(self, source, command, args):
|
||||
"""
|
||||
Handles the SERVER command.
|
||||
"""
|
||||
# <- :ngircd.midnight.local SERVER ngircd.midnight.local 1 :ngIRCd dev server
|
||||
servername = args[0].lower()
|
||||
serverdesc = args[-1]
|
||||
|
||||
# The uplink should be set to None for the uplink; otherwise, set it equal to the sender server.
|
||||
self.servers[servername] = Server(self, source if source != servername else None, servername, desc=serverdesc)
|
||||
|
||||
if self.uplink is None:
|
||||
self.uplink = servername
|
||||
log.debug('(%s) Got %s as uplink', self.name, servername)
|
||||
else:
|
||||
# Only send the SERVER hook if this isn't the initial connection.
|
||||
return {'name': servername, 'sid': None, 'text': serverdesc}
|
||||
|
||||
Class = NgIRCdProtocol
|
701
protocols/p10.py
701
protocols/p10.py
File diff suppressed because it is too large
Load Diff
@ -1,97 +0,0 @@
|
||||
import time
|
||||
|
||||
from pylinkirc import utils, conf
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.classes import *
|
||||
from pylinkirc.protocols.ts6 import *
|
||||
|
||||
class RatboxProtocol(TS6Protocol):
|
||||
|
||||
def __init__(self, irc):
|
||||
super().__init__(irc)
|
||||
# Don't require EUID for Ratbox
|
||||
self.required_caps.discard('EUID')
|
||||
|
||||
self.hook_map['LOGIN'] = 'CLIENT_SERVICES_LOGIN'
|
||||
self.protocol_caps -= {'slash-in-hosts'}
|
||||
|
||||
def connect(self):
|
||||
"""Initializes a connection to a server."""
|
||||
super().connect()
|
||||
|
||||
# Note: +r, +e, and +I support will be negotiated on link
|
||||
self.irc.cmodes = {'op': 'o', 'secret': 's', 'private': 'p', 'noextmsg': 'n', 'moderated': 'm',
|
||||
'inviteonly': 'i', 'topiclock': 't', 'limit': 'l', 'ban': 'b', 'voice': 'v',
|
||||
'key': 'k', 'sslonly': 'S', 'noknock': 'p',
|
||||
'*A': 'beI',
|
||||
'*B': 'k',
|
||||
'*C': 'l',
|
||||
'*D': 'imnpstrS'}
|
||||
|
||||
self.irc.umodes = {
|
||||
'invisible': 'i', 'callerid': 'g', 'oper': 'o', 'admin': 'a', 'sno_botfloods': 'b',
|
||||
'sno_clientconnections': 'c', 'sno_extclientconnections': 'C', 'sno_debug': 'd',
|
||||
'sno_fullauthblock': 'f', 'sno_skill': 'k', 'locops': 'l',
|
||||
'sno_rejectedclients': 'r', 'snomask': 's', 'sno_badclientconnections': 'u',
|
||||
'wallops': 'w', 'sno_serverconnects': 'x', 'sno_stats': 'y',
|
||||
'operwall': 'z', 'sno_operspy': 'Z', 'deaf': 'D', 'servprotect': 'S',
|
||||
# Now, map all the ABCD type modes:
|
||||
'*A': '', '*B': '', '*C': '', '*D': 'igoabcCdfklrsuwxyzZD'
|
||||
}
|
||||
|
||||
def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(),
|
||||
server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None,
|
||||
manipulatable=False):
|
||||
"""
|
||||
Spawns a new client with the given options.
|
||||
|
||||
Note: No nick collision / valid nickname checks are done here; it is
|
||||
up to plugins to make sure they don't introduce anything invalid.
|
||||
"""
|
||||
|
||||
# parameters: nickname, hopcount, nickTS, umodes, username, visible hostname, IP address,
|
||||
# UID, gecos
|
||||
|
||||
server = server or self.irc.sid
|
||||
if not self.irc.isInternalServer(server):
|
||||
raise ValueError('Server %r is not a PyLink server!' % server)
|
||||
|
||||
uid = self.uidgen[server].next_uid()
|
||||
|
||||
ts = ts or int(time.time())
|
||||
realname = realname or conf.conf['bot']['realname']
|
||||
raw_modes = self.irc.joinModes(modes)
|
||||
|
||||
orig_realhost = realhost
|
||||
realhost = realhost or host
|
||||
|
||||
u = self.irc.users[uid] = IrcUser(nick, ts, uid, server, ident=ident, host=host, realname=realname,
|
||||
realhost=realhost, ip=ip, manipulatable=manipulatable)
|
||||
self.irc.applyModes(uid, modes)
|
||||
self.irc.servers[server].users.add(uid)
|
||||
self._send(server, "UID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} "
|
||||
":{realname}".format(ts=ts, host=host,
|
||||
nick=nick, ident=ident, uid=uid,
|
||||
modes=raw_modes, ip=ip, realname=realname))
|
||||
|
||||
if orig_realhost:
|
||||
# If real host is specified, send it using ENCAP REALHOST
|
||||
self._send(uid, "ENCAP * REALHOST %s" % orig_realhost)
|
||||
|
||||
return u
|
||||
|
||||
def updateClient(self, target, field, text):
|
||||
"""updateClient() stub for ratbox."""
|
||||
raise NotImplementedError("User data changing is not supported on ircd-ratbox.")
|
||||
|
||||
def handle_realhost(self, uid, command, args):
|
||||
"""Handles real host propagation."""
|
||||
log.debug('(%s) Got REALHOST %s for %s', args[0], uid)
|
||||
self.irc.users[uid].realhost = args[0]
|
||||
|
||||
def handle_login(self, uid, command, args):
|
||||
"""Handles login propagation on burst."""
|
||||
self.irc.users[uid].services_account = args[0]
|
||||
return {'text': args[0]}
|
||||
|
||||
Class = RatboxProtocol
|
534
protocols/ts6.py
534
protocols/ts6.py
@ -10,24 +10,36 @@ from pylinkirc.classes import *
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.protocols.ts6_common import *
|
||||
|
||||
S2S_BUFSIZE = 510
|
||||
|
||||
class TS6Protocol(TS6BaseProtocol):
|
||||
def __init__(self, irc):
|
||||
super().__init__(irc)
|
||||
self.protocol_caps |= {'slash-in-hosts'}
|
||||
|
||||
SUPPORTED_IRCDS = ('charybdis', 'elemental', 'chatircd', 'ratbox')
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._ircd = self.serverdata.get('ircd', 'elemental' if self.serverdata.get('use_elemental_modes')
|
||||
else 'charybdis')
|
||||
self._ircd = self._ircd.lower()
|
||||
if self._ircd not in self.SUPPORTED_IRCDS:
|
||||
log.warning("(%s) Unsupported IRCd %r; falling back to 'charybdis' instead", self.name, target_ircd)
|
||||
self._ircd = 'charybdis'
|
||||
|
||||
self._can_chghost = False
|
||||
if self._ircd in ('charybdis', 'elemental', 'chatircd'):
|
||||
# Charybdis and derivatives allow slashes in hosts. Ratbox does not.
|
||||
self.protocol_caps |= {'slash-in-hosts'}
|
||||
self._can_chghost = True
|
||||
|
||||
self.casemapping = 'rfc1459'
|
||||
self.hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE',
|
||||
'EUID': 'UID', 'RSFNC': 'SVSNICK', 'ETB': 'TOPIC'}
|
||||
'EUID': 'UID', 'RSFNC': 'SVSNICK', 'ETB': 'TOPIC',
|
||||
# ENCAP LOGIN is used on burst for EUID-less servers
|
||||
'LOGIN': 'CLIENT_SERVICES_LOGIN'}
|
||||
|
||||
# Track whether we've received end-of-burst from the uplink.
|
||||
self.has_eob = False
|
||||
|
||||
self.required_caps = {'EUID', 'SAVE', 'TB', 'ENCAP', 'QS', 'CHW'}
|
||||
self.required_caps = {'TB', 'ENCAP', 'QS', 'CHW'}
|
||||
|
||||
### OUTGOING COMMANDS
|
||||
|
||||
def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(),
|
||||
def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(),
|
||||
server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator',
|
||||
manipulatable=False):
|
||||
"""
|
||||
@ -36,9 +48,9 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
Note: No nick collision / valid nickname checks are done here; it is
|
||||
up to plugins to make sure they don't introduce anything invalid.
|
||||
"""
|
||||
server = server or self.irc.sid
|
||||
server = server or self.sid
|
||||
|
||||
if not self.irc.isInternalServer(server):
|
||||
if not self.is_internal_server(server):
|
||||
raise ValueError('Server %r is not a PyLink server!' % server)
|
||||
|
||||
uid = self.uidgen[server].next_uid()
|
||||
@ -47,34 +59,47 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
# parameters: nickname, hopcount, nickTS, umodes, username,
|
||||
# visible hostname, IP address, UID, real hostname, account name, gecos
|
||||
ts = ts or int(time.time())
|
||||
realname = realname or conf.conf['bot']['realname']
|
||||
realhost = realhost or host
|
||||
raw_modes = self.irc.joinModes(modes)
|
||||
u = self.irc.users[uid] = IrcUser(nick, ts, uid, server, ident=ident, host=host, realname=realname,
|
||||
realhost=realhost, ip=ip, manipulatable=manipulatable, opertype=opertype)
|
||||
realname = realname or conf.conf['pylink']['realname']
|
||||
raw_modes = self.join_modes(modes)
|
||||
u = self.users[uid] = User(self, nick, ts, uid, server, ident=ident, host=host,
|
||||
realname=realname, realhost=realhost or host, ip=ip,
|
||||
manipulatable=manipulatable, opertype=opertype)
|
||||
|
||||
self.irc.applyModes(uid, modes)
|
||||
self.irc.servers[server].users.add(uid)
|
||||
self.apply_modes(uid, modes)
|
||||
self.servers[server].users.add(uid)
|
||||
|
||||
self._send(server, "EUID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} "
|
||||
"{realhost} * :{realname}".format(ts=ts, host=host,
|
||||
nick=nick, ident=ident, uid=uid,
|
||||
modes=raw_modes, ip=ip, realname=realname,
|
||||
realhost=realhost))
|
||||
if 'EUID' in self._caps:
|
||||
# charybdis-style EUID
|
||||
self._send_with_prefix(server, "EUID {nick} {hopcount} {ts} {modes} {ident} {host} {ip} {uid} "
|
||||
"{realhost} * :{realname}".format(ts=ts, host=host,
|
||||
nick=nick, ident=ident, uid=uid,
|
||||
modes=raw_modes, ip=ip, realname=realname,
|
||||
realhost=realhost or host,
|
||||
hopcount=self.servers[server].hopcount))
|
||||
else:
|
||||
# Basic ratbox UID
|
||||
self._send_with_prefix(server, "UID {nick} {hopcount} {ts} {modes} {ident} {host} {ip} {uid} "
|
||||
":{realname}".format(ts=ts, host=host,
|
||||
nick=nick, ident=ident, uid=uid,
|
||||
modes=raw_modes, ip=ip, realname=realname,
|
||||
hopcount=self.servers[server].hopcount))
|
||||
|
||||
if realhost:
|
||||
# If real host is specified, send it using ENCAP REALHOST
|
||||
self._send_with_prefix(uid, "ENCAP * REALHOST %s" % realhost)
|
||||
|
||||
return u
|
||||
|
||||
def join(self, client, channel):
|
||||
"""Joins a PyLink client to a channel."""
|
||||
channel = self.irc.toLower(channel)
|
||||
# JOIN:
|
||||
# parameters: channelTS, channel, '+' (a plus sign)
|
||||
if not self.irc.isInternalClient(client):
|
||||
log.error('(%s) Error trying to join %r to %r (no such client exists)', self.irc.name, client, channel)
|
||||
if not self.is_internal_client(client):
|
||||
log.error('(%s) Error trying to join %r to %r (no such client exists)', self.name, client, channel)
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
self._send(client, "JOIN {ts} {channel} +".format(ts=self.irc.channels[channel].ts, channel=channel))
|
||||
self.irc.channels[channel].users.add(client)
|
||||
self.irc.users[client].channels.add(channel)
|
||||
self._send_with_prefix(client, "JOIN {ts} {channel} +".format(ts=self._channels[channel].ts, channel=channel))
|
||||
self._channels[channel].users.add(client)
|
||||
self.users[client].channels.add(channel)
|
||||
|
||||
def sjoin(self, server, channel, users, ts=None, modes=set()):
|
||||
"""Sends an SJOIN for a group of users to a channel.
|
||||
@ -85,7 +110,7 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
|
||||
Example uses:
|
||||
sjoin('100', '#test', [('', '100AAABBC'), ('o', 100AAABBB'), ('v', '100AAADDD')])
|
||||
sjoin(self.irc.sid, '#test', [('o', self.irc.pseudoclient.uid)])
|
||||
sjoin(self.sid, '#test', [('o', self.pseudoclient.uid)])
|
||||
"""
|
||||
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L821
|
||||
# parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist
|
||||
@ -96,34 +121,33 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
# their status ('@+', '@', '+' or ''), for example:
|
||||
# '@+1JJAAAAAB +2JJAAAA4C 1JJAAAADS'. All users must be behind the source server
|
||||
# so it is not possible to use this message to force users to join a channel.
|
||||
channel = self.irc.toLower(channel)
|
||||
server = server or self.irc.sid
|
||||
server = server or self.sid
|
||||
assert users, "sjoin: No users sent?"
|
||||
log.debug('(%s) sjoin: got %r for users', self.irc.name, users)
|
||||
log.debug('(%s) sjoin: got %r for users', self.name, users)
|
||||
if not server:
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
|
||||
modes = set(modes or self.irc.channels[channel].modes)
|
||||
orig_ts = self.irc.channels[channel].ts
|
||||
modes = set(modes or self._channels[channel].modes)
|
||||
orig_ts = self._channels[channel].ts
|
||||
ts = ts or orig_ts
|
||||
|
||||
# Get all the ban modes in a separate list. These are bursted using a separate BMASK
|
||||
# command.
|
||||
banmodes = {k: [] for k in self.irc.cmodes['*A']}
|
||||
banmodes = {k: [] for k in self.cmodes['*A']}
|
||||
regularmodes = []
|
||||
log.debug('(%s) Unfiltered SJOIN modes: %s', self.irc.name, modes)
|
||||
log.debug('(%s) Unfiltered SJOIN modes: %s', self.name, modes)
|
||||
for mode in modes:
|
||||
modechar = mode[0][-1]
|
||||
if modechar in self.irc.cmodes['*A']:
|
||||
if modechar in self.cmodes['*A']:
|
||||
# Mode character is one of 'beIq'
|
||||
if (modechar, mode[1]) in self.irc.channels[channel].modes:
|
||||
if (modechar, mode[1]) in self._channels[channel].modes:
|
||||
# Don't reset modes that are already set.
|
||||
continue
|
||||
|
||||
banmodes[modechar].append(mode[1])
|
||||
else:
|
||||
regularmodes.append(mode)
|
||||
log.debug('(%s) Filtered SJOIN modes to be regular modes: %s, banmodes: %s', self.irc.name, regularmodes, banmodes)
|
||||
log.debug('(%s) Filtered SJOIN modes to be regular modes: %s, banmodes: %s', self.name, regularmodes, banmodes)
|
||||
|
||||
changedmodes = modes
|
||||
while users[:12]:
|
||||
@ -135,22 +159,22 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
prefixes, user = userpair
|
||||
prefixchars = ''
|
||||
for prefix in prefixes:
|
||||
pr = self.irc.prefixmodes.get(prefix)
|
||||
pr = self.prefixmodes.get(prefix)
|
||||
if pr:
|
||||
prefixchars += pr
|
||||
changedmodes.add(('+%s' % prefix, user))
|
||||
namelist.append(prefixchars+user)
|
||||
uids.append(user)
|
||||
try:
|
||||
self.irc.users[user].channels.add(channel)
|
||||
self.users[user].channels.add(channel)
|
||||
except KeyError: # Not initialized yet?
|
||||
log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user)
|
||||
log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.name, channel, user)
|
||||
users = users[12:]
|
||||
namelist = ' '.join(namelist)
|
||||
self._send(server, "SJOIN {ts} {channel} {modes} :{users}".format(
|
||||
self._send_with_prefix(server, "SJOIN {ts} {channel} {modes} :{users}".format(
|
||||
ts=ts, users=namelist, channel=channel,
|
||||
modes=self.irc.joinModes(regularmodes)))
|
||||
self.irc.channels[channel].users.update(uids)
|
||||
modes=self.join_modes(regularmodes)))
|
||||
self._channels[channel].users.update(uids)
|
||||
|
||||
# Now, burst bans.
|
||||
# <- :42X BMASK 1424222769 #dev b :*!test@*.isp.net *!badident@*
|
||||
@ -158,12 +182,12 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
# Max 15-3 = 12 bans per line to prevent cut off. (TS6 allows a max of 15 parameters per
|
||||
# line)
|
||||
if bans:
|
||||
log.debug('(%s) sjoin: bursting mode %s with bans %s, ts:%s', self.irc.name, bmode, bans, ts)
|
||||
log.debug('(%s) sjoin: bursting mode %s with bans %s, ts:%s', self.name, bmode, bans, ts)
|
||||
msgprefix = ':{sid} BMASK {ts} {channel} {bmode} :'.format(sid=server, ts=ts,
|
||||
channel=channel, bmode=bmode)
|
||||
# Actually, we cut off at 17 arguments/line, since the prefix and command name don't count.
|
||||
for msg in utils.wrapArguments(msgprefix, bans, S2S_BUFSIZE, max_args_per_line=17):
|
||||
self.irc.send(msg)
|
||||
for msg in utils.wrap_arguments(msgprefix, bans, self.S2S_BUFSIZE, max_args_per_line=17):
|
||||
self.send(msg)
|
||||
|
||||
self.updateTS(server, channel, ts, changedmodes)
|
||||
|
||||
@ -172,155 +196,188 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
# c <- :0UYAAAAAA TMODE 0 #a +o 0T4AAAAAC
|
||||
# u <- :0UYAAAAAA MODE 0UYAAAAAA :-Facdefklnou
|
||||
|
||||
if (not self.irc.isInternalClient(numeric)) and \
|
||||
(not self.irc.isInternalServer(numeric)):
|
||||
if (not self.is_internal_client(numeric)) and \
|
||||
(not self.is_internal_server(numeric)):
|
||||
raise LookupError('No such PyLink client/server exists.')
|
||||
|
||||
self.irc.applyModes(target, modes)
|
||||
self.apply_modes(target, modes)
|
||||
modes = list(modes)
|
||||
|
||||
if utils.isChannel(target):
|
||||
ts = ts or self.irc.channels[self.irc.toLower(target)].ts
|
||||
if self.is_channel(target):
|
||||
ts = ts or self._channels[target].ts
|
||||
# TMODE:
|
||||
# parameters: channelTS, channel, cmode changes, opt. cmode parameters...
|
||||
|
||||
# On output, at most ten cmode parameters should be sent; if there are more,
|
||||
# multiple TMODE messages should be sent.
|
||||
msgprefix = ':%s TMODE %s %s ' % (numeric, ts, target)
|
||||
bufsize = S2S_BUFSIZE - len(msgprefix)
|
||||
bufsize = self.S2S_BUFSIZE - len(msgprefix)
|
||||
|
||||
for modestr in self.irc.wrapModes(modes, bufsize, max_modes_per_msg=10):
|
||||
self.irc.send(msgprefix + modestr)
|
||||
for modestr in self.wrap_modes(modes, bufsize, max_modes_per_msg=10):
|
||||
self.send(msgprefix + modestr)
|
||||
else:
|
||||
joinedmodes = self.irc.joinModes(modes)
|
||||
self._send(numeric, 'MODE %s %s' % (target, joinedmodes))
|
||||
joinedmodes = self.join_modes(modes)
|
||||
self._send_with_prefix(numeric, 'MODE %s %s' % (target, joinedmodes))
|
||||
|
||||
def topicBurst(self, numeric, target, text):
|
||||
def topic_burst(self, numeric, target, text):
|
||||
"""Sends a topic change from a PyLink server. This is usually used on burst."""
|
||||
if not self.irc.isInternalServer(numeric):
|
||||
if not self.is_internal_server(numeric):
|
||||
raise LookupError('No such PyLink server exists.')
|
||||
# TB
|
||||
# capab: TB
|
||||
# source: server
|
||||
# propagation: broadcast
|
||||
# parameters: channel, topicTS, opt. topic setter, topic
|
||||
ts = self.irc.channels[target].ts
|
||||
servername = self.irc.servers[numeric].name
|
||||
self._send(numeric, 'TB %s %s %s :%s' % (target, ts, servername, text))
|
||||
self.irc.channels[target].topic = text
|
||||
self.irc.channels[target].topicset = True
|
||||
ts = self._channels[target].ts
|
||||
servername = self.servers[numeric].name
|
||||
self._send_with_prefix(numeric, 'TB %s %s %s :%s' % (target, ts, servername, text))
|
||||
self._channels[target].topic = text
|
||||
self._channels[target].topicset = True
|
||||
|
||||
def invite(self, numeric, target, channel):
|
||||
"""Sends an INVITE from a PyLink client.."""
|
||||
if not self.irc.isInternalClient(numeric):
|
||||
if not self.is_internal_client(numeric):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
self._send(numeric, 'INVITE %s %s %s' % (target, channel, self.irc.channels[channel].ts))
|
||||
self._send_with_prefix(numeric, 'INVITE %s %s %s' % (target, channel, self._channels[channel].ts))
|
||||
|
||||
def knock(self, numeric, target, text):
|
||||
"""Sends a KNOCK from a PyLink client."""
|
||||
if 'KNOCK' not in self.irc.caps:
|
||||
if 'KNOCK' not in self._caps:
|
||||
log.debug('(%s) knock: Dropping KNOCK to %r since the IRCd '
|
||||
'doesn\'t support it.', self.irc.name, target)
|
||||
'doesn\'t support it.', self.name, target)
|
||||
return
|
||||
if not self.irc.isInternalClient(numeric):
|
||||
if not self.is_internal_client(numeric):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
# No text value is supported here; drop it.
|
||||
self._send(numeric, 'KNOCK %s' % target)
|
||||
self._send_with_prefix(numeric, 'KNOCK %s' % target)
|
||||
|
||||
def updateClient(self, target, field, text):
|
||||
def set_server_ban(self, source, duration, user='*', host='*', reason='User banned'):
|
||||
"""
|
||||
Sets a server ban.
|
||||
"""
|
||||
# source: user
|
||||
# parameters: target server mask, duration, user mask, host mask, reason
|
||||
assert not (user == host == '*'), "Refusing to set ridiculous ban on *@*"
|
||||
|
||||
if not source in self.users:
|
||||
log.debug('(%s) Forcing KLINE sender to %s as TS6 does not allow KLINEs from servers', self.name, self.pseudoclient.uid)
|
||||
source = self.pseudoclient.uid
|
||||
|
||||
self._send_with_prefix(source, 'ENCAP * KLINE %s %s %s :%s' % (duration, user, host, reason))
|
||||
|
||||
def update_client(self, target, field, text):
|
||||
"""Updates the hostname of any connected client."""
|
||||
field = field.upper()
|
||||
if field == 'HOST':
|
||||
self.irc.users[target].host = text
|
||||
self._send(self.irc.sid, 'CHGHOST %s :%s' % (target, text))
|
||||
if not self.irc.isInternalClient(target):
|
||||
if field == 'HOST' and self._can_chghost:
|
||||
self.users[target].host = text
|
||||
self._send_with_prefix(self.sid, 'CHGHOST %s :%s' % (target, text))
|
||||
if not self.is_internal_client(target):
|
||||
# If the target isn't one of our clients, send hook payload
|
||||
# for other plugins to listen to.
|
||||
self.irc.callHooks([self.irc.sid, 'CHGHOST',
|
||||
self.call_hooks([self.sid, 'CHGHOST',
|
||||
{'target': target, 'newhost': text}])
|
||||
else:
|
||||
raise NotImplementedError("Changing field %r of a client is "
|
||||
"unsupported by this protocol." % field)
|
||||
|
||||
def ping(self, source=None, target=None):
|
||||
"""Sends a PING to a target server. Periodic PINGs are sent to our uplink
|
||||
automatically by the Irc() internals; plugins shouldn't have to use this."""
|
||||
source = source or self.irc.sid
|
||||
if source is None:
|
||||
return
|
||||
if target is not None:
|
||||
self._send(source, 'PING %s %s' % (source, target))
|
||||
else:
|
||||
self._send(source, 'PING %s' % source)
|
||||
"unsupported by this IRCd." % field)
|
||||
|
||||
### Core / handlers
|
||||
|
||||
def connect(self):
|
||||
def post_connect(self):
|
||||
"""Initializes a connection to a server."""
|
||||
ts = self.irc.start_ts
|
||||
self.has_eob = False
|
||||
ts = self.start_ts
|
||||
|
||||
f = self.irc.send
|
||||
f = self.send
|
||||
|
||||
# Base TS6 mode set from ratbox.
|
||||
self.cmodes.update({'sslonly': 'S', 'noknock': 'p',
|
||||
'*A': 'beI',
|
||||
'*B': 'k',
|
||||
'*C': 'l',
|
||||
'*D': 'imnpstrS'})
|
||||
|
||||
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L80
|
||||
chary_cmodes = { # TS6 generic modes (note that +p is noknock instead of private):
|
||||
'op': 'o', 'voice': 'v', 'ban': 'b', 'key': 'k', 'limit':
|
||||
'l', 'moderated': 'm', 'noextmsg': 'n', 'noknock': 'p',
|
||||
'secret': 's', 'topiclock': 't', 'inviteonly': 'i',
|
||||
'private': 'p',
|
||||
# charybdis-specific modes:
|
||||
'quiet': 'q', 'redirect': 'f', 'freetarget': 'F',
|
||||
'joinflood': 'j', 'largebanlist': 'L', 'permanent': 'P',
|
||||
'noforwards': 'Q', 'stripcolor': 'c', 'allowinvite':
|
||||
'g', 'opmoderated': 'z', 'noctcp': 'C',
|
||||
# charybdis-specific modes provided by EXTENSIONS
|
||||
'operonly': 'O', 'adminonly': 'A', 'sslonly': 'S',
|
||||
'nonotice': 'T',
|
||||
# Now, map all the ABCD type modes:
|
||||
'*A': 'beIq', '*B': 'k', '*C': 'lfj', '*D': 'mnprstFLPQcgzCOAST'}
|
||||
if self._ircd in ('charybdis', 'elemental', 'chatircd'):
|
||||
self.cmodes.update({
|
||||
'quiet': 'q', 'redirect': 'f', 'freetarget': 'F',
|
||||
'joinflood': 'j', 'largebanlist': 'L', 'permanent': 'P',
|
||||
'noforwards': 'Q', 'stripcolor': 'c', 'allowinvite':
|
||||
'g', 'opmoderated': 'z', 'noctcp': 'C', 'ssl': 'Z',
|
||||
# charybdis modes provided by extensions
|
||||
'operonly': 'O', 'adminonly': 'A', 'sslonly': 'S',
|
||||
'nonotice': 'T',
|
||||
'*A': 'beIq', '*B': 'k', '*C': 'lfj', '*D': 'mnprstFLPQcgzCZOAST'
|
||||
})
|
||||
self.umodes.update({
|
||||
'deaf': 'D', 'servprotect': 'S', 'admin': 'a',
|
||||
'invisible': 'i', 'oper': 'o', 'wallops': 'w',
|
||||
'snomask': 's', 'noforward': 'Q', 'regdeaf': 'R',
|
||||
'callerid': 'g', 'operwall': 'z', 'locops': 'l',
|
||||
'cloak': 'x', 'override': 'p',
|
||||
'*A': '', '*B': '', '*C': '', '*D': 'DSaiowsQRgzlxp'
|
||||
})
|
||||
|
||||
if self.irc.serverdata.get('use_owner'):
|
||||
chary_cmodes['owner'] = 'y'
|
||||
self.irc.prefixmodes['y'] = '~'
|
||||
if self.irc.serverdata.get('use_admin'):
|
||||
chary_cmodes['admin'] = 'a'
|
||||
self.irc.prefixmodes['a'] = '!'
|
||||
if self.irc.serverdata.get('use_halfop'):
|
||||
chary_cmodes['halfop'] = 'h'
|
||||
self.irc.prefixmodes['h'] = '%'
|
||||
# Charybdis extbans
|
||||
self.extbans_matching = {'ban_all_registered': '$a', 'ban_inchannel': '$c:', 'ban_account': '$a:',
|
||||
'ban_all_opers': '$o', 'ban_realname': '$r:', 'ban_server': '$s:',
|
||||
'ban_banshare': '$j:', 'ban_extgecos': '$x:', 'ban_all_ssl': '$z'}
|
||||
elif self._ircd == 'ratbox':
|
||||
self.umodes.update({
|
||||
'callerid': 'g', 'admin': 'a', 'sno_botfloods': 'b',
|
||||
'sno_clientconnections': 'c', 'sno_extclientconnections': 'C', 'sno_debug': 'd',
|
||||
'sno_fullauthblock': 'f', 'sno_skill': 'k', 'locops': 'l', 'sno_rejectedclients': 'r',
|
||||
'snomask': 's', 'sno_badclientconnections': 'u', 'sno_serverconnects': 'x',
|
||||
'sno_stats': 'y', 'operwall': 'z', 'sno_operspy': 'Z', 'deaf': 'D', 'servprotect': 'S',
|
||||
'*A': '', '*B': '', '*C': '', '*D': 'igoabcCdfklrsuwxyzZD'
|
||||
})
|
||||
|
||||
self.irc.cmodes = chary_cmodes
|
||||
|
||||
# Define supported user modes
|
||||
chary_umodes = {'deaf': 'D', 'servprotect': 'S', 'admin': 'a',
|
||||
'invisible': 'i', 'oper': 'o', 'wallops': 'w',
|
||||
'snomask': 's', 'noforward': 'Q', 'regdeaf': 'R',
|
||||
'callerid': 'g', 'operwall': 'z', 'locops': 'l',
|
||||
'cloak': 'x', 'override': 'p',
|
||||
# Now, map all the ABCD type modes:
|
||||
'*A': '', '*B': '', '*C': '', '*D': 'DSaiowsQRgzlxp'}
|
||||
self.irc.umodes = chary_umodes
|
||||
# TODO: make these more flexible...
|
||||
if self.serverdata.get('use_owner'):
|
||||
self.cmodes['owner'] = 'y'
|
||||
self.prefixmodes['y'] = '~'
|
||||
if self.serverdata.get('use_admin'):
|
||||
self.cmodes['admin'] = 'a'
|
||||
self.prefixmodes['a'] = '!' if self._ircd != 'chatircd' else '&'
|
||||
if self.serverdata.get('use_halfop'):
|
||||
self.cmodes['halfop'] = 'h'
|
||||
self.prefixmodes['h'] = '%'
|
||||
|
||||
# Toggles support of shadowircd/elemental-ircd specific channel modes:
|
||||
# +T (no notice), +u (hidden ban list), +E (no kicks), +J (blocks kickrejoin),
|
||||
# +K (no repeat messages), +d (no nick changes), and user modes:
|
||||
# +B (bot), +C (blocks CTCP), +D (deaf), +V (no invites), +I (hides channel list)
|
||||
if self.irc.serverdata.get('use_elemental_modes'):
|
||||
# +B (bot), +C (blocks CTCP), +V (no invites), +I (hides channel list)
|
||||
if self._ircd == 'elemental':
|
||||
elemental_cmodes = {'hiddenbans': 'u', 'nokick': 'E',
|
||||
'kicknorejoin': 'J', 'repeat': 'K', 'nonick': 'd',
|
||||
'blockcaps': 'G'}
|
||||
self.irc.cmodes.update(elemental_cmodes)
|
||||
self.irc.cmodes['*D'] += ''.join(elemental_cmodes.values())
|
||||
self.cmodes.update(elemental_cmodes)
|
||||
self.cmodes['*D'] += ''.join(elemental_cmodes.values())
|
||||
|
||||
elemental_umodes = {'noctcp': 'C', 'deaf': 'D', 'bot': 'B', 'noinvite': 'V',
|
||||
'hidechans': 'I'}
|
||||
self.irc.umodes.update(elemental_umodes)
|
||||
self.irc.umodes['*D'] += ''.join(elemental_umodes.values())
|
||||
elemental_umodes = {'noctcp': 'C', 'bot': 'B', 'noinvite': 'V', 'hidechans': 'I'}
|
||||
self.umodes.update(elemental_umodes)
|
||||
self.umodes['*D'] += ''.join(elemental_umodes.values())
|
||||
|
||||
elif self._ircd == 'chatircd':
|
||||
chatircd_cmodes = {'netadminonly': 'N'}
|
||||
self.cmodes.update(chatircd_cmodes)
|
||||
self.cmodes['*D'] += ''.join(chatircd_cmodes.values())
|
||||
|
||||
chatircd_umodes = {'netadmin': 'n', 'bot': 'B', 'callerid_sslonly': 't'}
|
||||
self.umodes.update(chatircd_umodes)
|
||||
self.umodes['*D'] += ''.join(chatircd_umodes.values())
|
||||
|
||||
# Add definitions for all the inverted versions of the extbans.
|
||||
if self.extbans_matching:
|
||||
for k, v in self.extbans_matching.copy().items():
|
||||
if k == 'ban_all_registered':
|
||||
newk = 'ban_unregistered'
|
||||
else:
|
||||
newk = k.replace('_all_', '_').replace('ban_', 'ban_not_')
|
||||
self.extbans_matching[newk] = '$~' + v[1:]
|
||||
|
||||
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L55
|
||||
f('PASS %s TS 6 %s' % (self.irc.serverdata["sendpass"], self.irc.sid))
|
||||
f('PASS %s TS 6 %s' % (self.serverdata["sendpass"], self.sid))
|
||||
|
||||
# We request the following capabilities (for charybdis):
|
||||
# We request the following capabilities:
|
||||
|
||||
# QS: SQUIT doesn't send recursive quits for each users; required
|
||||
# by charybdis (Source: https://github.com/grawity/irc-docs/blob/master/server/ts-capab.txt)
|
||||
@ -331,20 +388,21 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
# KNOCK: support for /knock
|
||||
# SAVE: support for SAVE (forces user to UID in nick collision)
|
||||
# SERVICES: adds mode +r (only registered users can join a channel)
|
||||
# TB: topic burst command; we send this in topicBurst
|
||||
# TB: topic burst command; we send this in topic_burst
|
||||
# EUID: extended UID command, which includes real hostname + account data info,
|
||||
# and allows sending CHGHOST without ENCAP.
|
||||
# RSFNC: states that we support RSFNC (forced nick changed attempts). XXX: With atheme services,
|
||||
# does this actually do anything?
|
||||
# EOPMOD: supports ETB (extended TOPIC burst) and =#channel messages for opmoderated +z
|
||||
f('CAPAB :QS ENCAP EX CHW IE KNOCK SAVE SERVICES TB EUID RSFNC EOPMOD SAVETS_100')
|
||||
# KLN: supports remote KLINEs
|
||||
f('CAPAB :QS ENCAP EX CHW IE KNOCK SAVE SERVICES TB EUID RSFNC EOPMOD SAVETS_100 KLN')
|
||||
|
||||
f('SERVER %s 0 :%s' % (self.irc.serverdata["hostname"],
|
||||
self.irc.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc']))
|
||||
f('SERVER %s 0 :%s' % (self.serverdata["hostname"],
|
||||
self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']))
|
||||
|
||||
# Finally, end all the initialization with a PING - that's Charybdis'
|
||||
# way of saying end-of-burst :)
|
||||
self.ping()
|
||||
self._ping_uplink()
|
||||
|
||||
def handle_pass(self, numeric, command, args):
|
||||
"""
|
||||
@ -353,7 +411,7 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
"""
|
||||
# <- PASS $somepassword TS 6 :42X
|
||||
|
||||
if args[0] != self.irc.serverdata['recvpass']:
|
||||
if args[0] != self.serverdata['recvpass']:
|
||||
# Check if recvpass is correct
|
||||
raise ProtocolError('Recvpass from uplink server %s does not match configuration!' % servername)
|
||||
|
||||
@ -361,12 +419,12 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
raise ProtocolError("Remote protocol version is too old! Is this even TS6?")
|
||||
|
||||
numeric = args[-1]
|
||||
log.debug('(%s) Found uplink SID as %r', self.irc.name, numeric)
|
||||
log.debug('(%s) Found uplink SID as %r', self.name, numeric)
|
||||
|
||||
# Server name and SID are sent in different messages, so we fill this
|
||||
# with dummy information until we get the actual sid.
|
||||
self.irc.servers[numeric] = IrcServer(None, '')
|
||||
self.irc.uplink = numeric
|
||||
self.servers[numeric] = Server(self, None, '')
|
||||
self.uplink = numeric
|
||||
|
||||
def handle_capab(self, numeric, command, args):
|
||||
"""
|
||||
@ -375,21 +433,18 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
# We only get a list of keywords here. Charybdis obviously assumes that
|
||||
# we know what modes it supports (indeed, this is a standard list).
|
||||
# <- CAPAB :BAN CHW CLUSTER ENCAP EOPMOD EUID EX IE KLN KNOCK MLOCK QS RSFNC SAVE SERVICES TB UNKLN
|
||||
self.irc.caps = caps = args[0].split()
|
||||
self._caps = caps = args[0].split()
|
||||
|
||||
for required_cap in self.required_caps:
|
||||
if required_cap not in caps:
|
||||
raise ProtocolError('%s not found in TS6 capabilities list; this is required! (got %r)' % (required_cap, caps))
|
||||
|
||||
if 'EX' in caps:
|
||||
self.irc.cmodes['banexception'] = 'e'
|
||||
self.cmodes['banexception'] = 'e'
|
||||
if 'IE' in caps:
|
||||
self.irc.cmodes['invex'] = 'I'
|
||||
self.cmodes['invex'] = 'I'
|
||||
if 'SERVICES' in caps:
|
||||
self.irc.cmodes['regonly'] = 'r'
|
||||
|
||||
log.debug('(%s) self.irc.connected set!', self.irc.name)
|
||||
self.irc.connected.set()
|
||||
self.cmodes['regonly'] = 'r'
|
||||
|
||||
def handle_ping(self, source, command, args):
|
||||
"""Handles incoming PING commands."""
|
||||
@ -406,14 +461,18 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
try:
|
||||
destination = args[1]
|
||||
except IndexError:
|
||||
destination = self.irc.sid
|
||||
if self.irc.isInternalServer(destination):
|
||||
self._send(destination, 'PONG %s %s' % (destination, source), queue=False)
|
||||
destination = self.sid
|
||||
if self.is_internal_server(destination):
|
||||
self._send_with_prefix(destination, 'PONG %s %s' % (destination, source), queue=False)
|
||||
|
||||
if destination == self.irc.sid and not self.has_eob:
|
||||
# Charybdis' idea of endburst is just sending a PING. No, really!
|
||||
if not self.servers[source].has_eob:
|
||||
# TS6 endburst is just sending a PING to the other server.
|
||||
# https://github.com/charybdis-ircd/charybdis/blob/dc336d1/modules/core/m_server.c#L484-L485
|
||||
self.has_eob = True
|
||||
self.servers[source].has_eob = True
|
||||
|
||||
if source == self.uplink:
|
||||
log.debug('(%s) self.connected set!', self.name)
|
||||
self.connected.set()
|
||||
|
||||
# Return the endburst hook.
|
||||
return {'parse_as': 'ENDBURST'}
|
||||
@ -422,18 +481,18 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
"""Handles incoming SJOIN commands."""
|
||||
# parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist
|
||||
# <- :0UY SJOIN 1451041566 #channel +nt :@0UYAAAAAB
|
||||
channel = self.irc.toLower(args[1])
|
||||
chandata = self.irc.channels[channel].deepcopy()
|
||||
channel = args[1]
|
||||
chandata = self._channels[channel].deepcopy()
|
||||
userlist = args[-1].split()
|
||||
|
||||
modestring = args[2:-1] or args[2]
|
||||
parsedmodes = self.irc.parseModes(channel, modestring)
|
||||
parsedmodes = self.parse_modes(channel, modestring)
|
||||
namelist = []
|
||||
|
||||
# Keep track of other modes that are added due to prefix modes being joined too.
|
||||
changedmodes = set(parsedmodes)
|
||||
|
||||
log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel)
|
||||
log.debug('(%s) handle_sjoin: got userlist %r for %r', self.name, userlist, channel)
|
||||
for userpair in userlist:
|
||||
# charybdis sends this in the form "@+UID1, +UID2, UID3, @UID4"
|
||||
r = re.search(r'([^\d]*)(.*)', userpair)
|
||||
@ -441,30 +500,30 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
modeprefix = r.group(1) or ''
|
||||
finalprefix = ''
|
||||
assert user, 'Failed to get the UID from %r; our regex needs updating?' % userpair
|
||||
log.debug('(%s) handle_sjoin: got modeprefix %r for user %r', self.irc.name, modeprefix, user)
|
||||
log.debug('(%s) handle_sjoin: got modeprefix %r for user %r', self.name, modeprefix, user)
|
||||
|
||||
# Don't crash when we get an invalid UID.
|
||||
if user not in self.irc.users:
|
||||
if user not in self.users:
|
||||
log.debug('(%s) handle_sjoin: tried to introduce user %s not in our user list, ignoring...',
|
||||
self.irc.name, user)
|
||||
self.name, user)
|
||||
continue
|
||||
|
||||
for m in modeprefix:
|
||||
# Iterate over the mapping of prefix chars to prefixes, and
|
||||
# find the characters that match.
|
||||
for char, prefix in self.irc.prefixmodes.items():
|
||||
for char, prefix in self.prefixmodes.items():
|
||||
if m == prefix:
|
||||
finalprefix += char
|
||||
namelist.append(user)
|
||||
self.irc.users[user].channels.add(channel)
|
||||
self.users[user].channels.add(channel)
|
||||
|
||||
# Only save mode changes if the remote has lower TS than us.
|
||||
changedmodes |= {('+%s' % mode, user) for mode in finalprefix}
|
||||
self.irc.channels[channel].users.add(user)
|
||||
self._channels[channel].users.add(user)
|
||||
|
||||
# Statekeeping with timestamps
|
||||
their_ts = int(args[0])
|
||||
our_ts = self.irc.channels[channel].ts
|
||||
our_ts = self._channels[channel].ts
|
||||
self.updateTS(servernumeric, channel, their_ts, changedmodes)
|
||||
|
||||
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts,
|
||||
@ -477,57 +536,56 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
ts = int(args[0])
|
||||
if args[0] == '0':
|
||||
# /join 0; part the user from all channels
|
||||
oldchans = self.irc.users[numeric].channels.copy()
|
||||
oldchans = self.users[numeric].channels.copy()
|
||||
log.debug('(%s) Got /join 0 from %r, channel list is %r',
|
||||
self.irc.name, numeric, oldchans)
|
||||
self.name, numeric, oldchans)
|
||||
for channel in oldchans:
|
||||
self.irc.channels[channel].users.discard(numeric)
|
||||
self.irc.users[numeric].channels.discard(channel)
|
||||
self._channels[channel].users.discard(numeric)
|
||||
self.users[numeric].channels.discard(channel)
|
||||
return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'}
|
||||
else:
|
||||
channel = self.irc.toLower(args[1])
|
||||
channel = args[1]
|
||||
self.updateTS(numeric, channel, ts)
|
||||
|
||||
self.irc.users[numeric].channels.add(channel)
|
||||
self.irc.channels[channel].users.add(numeric)
|
||||
self.users[numeric].channels.add(channel)
|
||||
self._channels[channel].users.add(numeric)
|
||||
|
||||
# We send users and modes here because SJOIN and JOIN both use one hook,
|
||||
# for simplicity's sake (with plugins).
|
||||
return {'channel': channel, 'users': [numeric], 'modes':
|
||||
self.irc.channels[channel].modes, 'ts': ts}
|
||||
self._channels[channel].modes, 'ts': ts}
|
||||
|
||||
def handle_euid(self, numeric, command, args):
|
||||
"""Handles incoming EUID commands (user introduction)."""
|
||||
# <- :42X EUID GL 1 1437505322 +ailoswz ~gl 127.0.0.1 127.0.0.1 42XAAAAAB * * :realname
|
||||
nick = args[0]
|
||||
self.check_nick_collision(nick)
|
||||
self._check_nick_collision(nick)
|
||||
ts, modes, ident, host, ip, uid, realhost, accountname, realname = args[2:11]
|
||||
ts = int(ts)
|
||||
if realhost == '*':
|
||||
realhost = host
|
||||
|
||||
log.debug('(%s) handle_euid got args: nick=%s ts=%s uid=%s ident=%s '
|
||||
'host=%s realname=%s realhost=%s ip=%s', self.irc.name, nick, ts, uid,
|
||||
'host=%s realname=%s realhost=%s ip=%s', self.name, nick, ts, uid,
|
||||
ident, host, realname, realhost, ip)
|
||||
assert ts != 0, "Bad TS 0 for user %s" % uid
|
||||
|
||||
if ip == '0': # IP was invalid; something used for services.
|
||||
ip = '0.0.0.0'
|
||||
|
||||
self.irc.users[uid] = IrcUser(nick, ts, uid, numeric, ident, host, realname, realhost, ip)
|
||||
self.users[uid] = User(self, nick, ts, uid, numeric, ident, host, realname, realhost, ip)
|
||||
|
||||
parsedmodes = self.irc.parseModes(uid, [modes])
|
||||
parsedmodes = self.parse_modes(uid, [modes])
|
||||
log.debug('Applying modes %s for %s', parsedmodes, uid)
|
||||
self.irc.applyModes(uid, parsedmodes)
|
||||
self.irc.servers[numeric].users.add(uid)
|
||||
self.apply_modes(uid, parsedmodes)
|
||||
self.servers[numeric].users.add(uid)
|
||||
|
||||
# Call the OPERED UP hook if +o is being added to the mode list.
|
||||
if ('+o', None) in parsedmodes:
|
||||
otype = 'Server Administrator' if ('+a', None) in parsedmodes else 'IRC Operator'
|
||||
self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': otype}])
|
||||
self._check_oper_status_change(uid, parsedmodes)
|
||||
|
||||
# Set the accountname if present
|
||||
if accountname != "*":
|
||||
self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}])
|
||||
self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}])
|
||||
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
|
||||
|
||||
@ -545,123 +603,85 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
euid_args.insert(8, '*')
|
||||
|
||||
# Copy the visible hostname to the real hostname, as this data isn't sent yet.
|
||||
# TODO: handle encap realhost / encap login
|
||||
euid_args.insert(8, args[5])
|
||||
|
||||
return self.handle_euid(numeric, command, euid_args)
|
||||
|
||||
def handle_sid(self, numeric, command, args):
|
||||
"""Handles incoming server introductions."""
|
||||
# parameters: server name, hopcount, sid, server description
|
||||
servername = args[0].lower()
|
||||
sid = args[2]
|
||||
sdesc = args[-1]
|
||||
self.irc.servers[sid] = IrcServer(numeric, servername, desc=sdesc)
|
||||
return {'name': servername, 'sid': sid, 'text': sdesc}
|
||||
|
||||
def handle_server(self, numeric, command, args):
|
||||
"""
|
||||
Handles 1) incoming legacy (no SID) server introductions,
|
||||
2) Sending server data in initial connection.
|
||||
"""
|
||||
if numeric == self.irc.uplink and not self.irc.servers[numeric].name:
|
||||
if numeric == self.uplink and not self.servers[numeric].name:
|
||||
# <- SERVER charybdis.midnight.vpn 1 :charybdis test server
|
||||
sname = args[0].lower()
|
||||
|
||||
log.debug('(%s) Found uplink server name as %r', self.irc.name, sname)
|
||||
self.irc.servers[numeric].name = sname
|
||||
self.irc.servers[numeric].desc = args[-1]
|
||||
log.debug('(%s) Found uplink server name as %r', self.name, sname)
|
||||
self.servers[numeric].name = sname
|
||||
self.servers[numeric].desc = args[-1]
|
||||
|
||||
# According to the TS6 protocol documentation, we should send SVINFO
|
||||
# when we get our uplink's SERVER command.
|
||||
self.irc.send('SVINFO 6 6 0 :%s' % int(time.time()))
|
||||
self.send('SVINFO 6 6 0 :%s' % int(time.time()))
|
||||
|
||||
return
|
||||
|
||||
# <- :services.int SERVER a.bc 2 :(H) [GL] a
|
||||
servername = args[0].lower()
|
||||
sdesc = args[-1]
|
||||
self.irc.servers[servername] = IrcServer(numeric, servername, desc=sdesc)
|
||||
return {'name': servername, 'sid': None, 'text': sdesc}
|
||||
return super().handle_server(numeric, command, args)
|
||||
|
||||
def handle_tmode(self, numeric, command, args):
|
||||
"""Handles incoming TMODE commands (channel mode change)."""
|
||||
# <- :42XAAAAAB TMODE 1437450768 #test -c+lkC 3 agte4
|
||||
# <- :0UYAAAAAD TMODE 0 #a +h 0UYAAAAAD
|
||||
channel = self.irc.toLower(args[1])
|
||||
oldobj = self.irc.channels[channel].deepcopy()
|
||||
channel = args[1]
|
||||
oldobj = self._channels[channel].deepcopy()
|
||||
modes = args[2:]
|
||||
changedmodes = self.irc.parseModes(channel, modes)
|
||||
self.irc.applyModes(channel, changedmodes)
|
||||
changedmodes = self.parse_modes(channel, modes)
|
||||
self.apply_modes(channel, changedmodes)
|
||||
ts = int(args[0])
|
||||
return {'target': channel, 'modes': changedmodes, 'ts': ts,
|
||||
'channeldata': oldobj}
|
||||
|
||||
def handle_mode(self, numeric, command, args):
|
||||
"""Handles incoming user mode changes."""
|
||||
# <- :70MAAAAAA MODE 70MAAAAAA -i+xc
|
||||
target = args[0]
|
||||
modestrings = args[1:]
|
||||
changedmodes = self.irc.parseModes(target, modestrings)
|
||||
self.irc.applyModes(target, changedmodes)
|
||||
# Call the OPERED UP hook if +o is being set.
|
||||
if ('+o', None) in changedmodes:
|
||||
otype = 'Server Administrator' if ('a', None) in self.irc.users[target].modes else 'IRC Operator'
|
||||
self.irc.callHooks([target, 'CLIENT_OPERED', {'text': otype}])
|
||||
return {'target': target, 'modes': changedmodes}
|
||||
|
||||
def handle_tb(self, numeric, command, args):
|
||||
"""Handles incoming topic burst (TB) commands."""
|
||||
# <- :42X TB #chat 1467427448 GL!~gl@127.0.0.1 :test
|
||||
channel = self.irc.toLower(args[0])
|
||||
channel = args[0]
|
||||
ts = args[1]
|
||||
setter = args[2]
|
||||
topic = args[-1]
|
||||
self.irc.channels[channel].topic = topic
|
||||
self.irc.channels[channel].topicset = True
|
||||
self._channels[channel].topic = topic
|
||||
self._channels[channel].topicset = True
|
||||
return {'channel': channel, 'setter': setter, 'ts': ts, 'text': topic}
|
||||
|
||||
def handle_etb(self, numeric, command, args):
|
||||
"""Handles extended topic burst (ETB)."""
|
||||
# <- :00AAAAAAC ETB 0 #test 1470021157 GL :test | abcd
|
||||
# Same as TB, with extra TS and extensions arguments.
|
||||
channel = self.irc.toLower(args[1])
|
||||
channel = args[1]
|
||||
ts = args[2]
|
||||
setter = args[3]
|
||||
topic = args[-1]
|
||||
self.irc.channels[channel].topic = topic
|
||||
self.irc.channels[channel].topicset = True
|
||||
self._channels[channel].topic = topic
|
||||
self._channels[channel].topicset = True
|
||||
return {'channel': channel, 'setter': setter, 'ts': ts, 'text': topic}
|
||||
|
||||
def handle_invite(self, numeric, command, args):
|
||||
"""Handles incoming INVITEs."""
|
||||
# <- :70MAAAAAC INVITE 0ALAAAAAA #blah 12345
|
||||
target = args[0]
|
||||
channel = self.irc.toLower(args[1])
|
||||
try:
|
||||
ts = int(args[2])
|
||||
except IndexError:
|
||||
ts = int(time.time())
|
||||
# We don't actually need to process this; it's just something plugins/hooks can use
|
||||
return {'target': target, 'channel': channel, 'ts': ts}
|
||||
|
||||
def handle_chghost(self, numeric, command, args):
|
||||
"""Handles incoming CHGHOST commands."""
|
||||
target = self._getUid(args[0])
|
||||
self.irc.users[target].host = newhost = args[1]
|
||||
target = self._get_UID(args[0])
|
||||
self.users[target].host = newhost = args[1]
|
||||
return {'target': target, 'newhost': newhost}
|
||||
|
||||
def handle_bmask(self, numeric, command, args):
|
||||
"""Handles incoming BMASK commands (ban propagation on burst)."""
|
||||
# <- :42X BMASK 1424222769 #dev b :*!test@*.isp.net *!badident@*
|
||||
# This is used for propagating bans, not TMODE!
|
||||
channel = self.irc.toLower(args[1])
|
||||
channel = args[1]
|
||||
mode = args[2]
|
||||
ts = int(args[0])
|
||||
modes = []
|
||||
for ban in args[-1].split():
|
||||
modes.append(('+%s' % mode, ban))
|
||||
self.irc.applyModes(channel, modes)
|
||||
self.apply_modes(channel, modes)
|
||||
return {'target': channel, 'modes': modes, 'ts': ts}
|
||||
|
||||
def handle_472(self, numeric, command, args):
|
||||
@ -682,7 +702,7 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
log.warning('(%s) User %r attempted to set channel mode %r, but the '
|
||||
'extension providing it isn\'t loaded! To prevent possible'
|
||||
' desyncs, try adding the line "loadmodule "extensions/%s.so";" to '
|
||||
'your IRCd configuration.', self.irc.name, setter, badmode,
|
||||
'your IRCd configuration.', self.name, setter, badmode,
|
||||
charlist[badmode])
|
||||
|
||||
def handle_su(self, numeric, command, args):
|
||||
@ -697,7 +717,7 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
account = '' # No account name means a logout
|
||||
|
||||
uid = args[0]
|
||||
self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}])
|
||||
self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}])
|
||||
|
||||
def handle_rsfnc(self, numeric, command, args):
|
||||
"""
|
||||
@ -706,4 +726,14 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
# <- :00A ENCAP somenet.relay RSFNC 801AAAAAB Guest75038 1468299643 :1468299675
|
||||
return {'target': args[0], 'newnick': args[1]}
|
||||
|
||||
def handle_realhost(self, uid, command, args):
|
||||
"""Handles real host propagation."""
|
||||
log.debug('(%s) Got REALHOST %s for %s', args[0], uid)
|
||||
self.users[uid].realhost = args[0]
|
||||
|
||||
def handle_login(self, uid, command, args):
|
||||
"""Handles login propagation on burst."""
|
||||
self.users[uid].services_account = args[0]
|
||||
return {'text': args[0]}
|
||||
|
||||
Class = TS6Protocol
|
||||
|
@ -56,7 +56,6 @@ class TS6SIDGenerator():
|
||||
self.output[idx] = self.allowedchars[idx][0]
|
||||
next(self.iters[idx])
|
||||
|
||||
|
||||
def increment(self, pos=2):
|
||||
"""
|
||||
Increments the SID generator to the next available SID.
|
||||
@ -85,7 +84,7 @@ class TS6SIDGenerator():
|
||||
sid = ''.join(self.output)
|
||||
return sid
|
||||
|
||||
class TS6UIDGenerator(utils.IncrementalUIDGenerator):
|
||||
class TS6UIDGenerator(IncrementalUIDGenerator):
|
||||
"""Implements an incremental TS6 UID Generator."""
|
||||
|
||||
def __init__(self, sid):
|
||||
@ -99,65 +98,27 @@ class TS6UIDGenerator(utils.IncrementalUIDGenerator):
|
||||
super().__init__(sid)
|
||||
|
||||
class TS6BaseProtocol(IRCS2SProtocol):
|
||||
|
||||
def __init__(self, irc):
|
||||
super().__init__(irc)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Dictionary of UID generators (one for each server).
|
||||
self.uidgen = structures.KeyedDefaultdict(TS6UIDGenerator)
|
||||
|
||||
# SID generator for TS6.
|
||||
self.sidgen = TS6SIDGenerator(irc)
|
||||
self.sidgen = TS6SIDGenerator(self)
|
||||
|
||||
def _send(self, source, msg, **kwargs):
|
||||
"""Sends a TS6-style raw command from a source numeric to the self.irc connection given."""
|
||||
self.irc.send(':%s %s' % (source, msg), **kwargs)
|
||||
|
||||
def _expandPUID(self, uid):
|
||||
"""
|
||||
Returns the outgoing nick for the given UID. In the base ts6_common implementation,
|
||||
this does nothing, but other modules subclassing this can override it.
|
||||
For example, this can be used to turn PUIDs (used to store legacy, UID-less users)
|
||||
to actual nicks in outgoing messages, so that a remote IRCd can understand it.
|
||||
"""
|
||||
return uid
|
||||
# Most TS6 variations (unreal, inspircd, charybdis) support this. For
|
||||
# pure TS6, we also require the CHW capability which explicitly declares
|
||||
# support.
|
||||
self.protocol_caps |= {'has-statusmsg'}
|
||||
|
||||
### OUTGOING COMMANDS
|
||||
|
||||
def numeric(self, source, numeric, target, text):
|
||||
"""Sends raw numerics from a server to a remote client, used for WHOIS
|
||||
replies."""
|
||||
# Mangle the target for IRCds that require it.
|
||||
target = self._expandPUID(target)
|
||||
|
||||
self._send(source, '%s %s %s' % (numeric, target, text))
|
||||
|
||||
def kick(self, numeric, channel, target, reason=None):
|
||||
"""Sends kicks from a PyLink client/server."""
|
||||
|
||||
if (not self.irc.isInternalClient(numeric)) and \
|
||||
(not self.irc.isInternalServer(numeric)):
|
||||
raise LookupError('No such PyLink client/server exists.')
|
||||
|
||||
channel = self.irc.toLower(channel)
|
||||
if not reason:
|
||||
reason = 'No reason given'
|
||||
|
||||
# Mangle kick targets for IRCds that require it.
|
||||
real_target = self._expandPUID(target)
|
||||
|
||||
self._send(numeric, 'KICK %s %s :%s' % (channel, real_target, reason))
|
||||
|
||||
# We can pretend the target left by its own will; all we really care about
|
||||
# is that the target gets removed from the channel userlist, and calling
|
||||
# handle_part() does that just fine.
|
||||
self.handle_part(target, 'KICK', [channel])
|
||||
|
||||
def kill(self, numeric, target, reason):
|
||||
"""Sends a kill from a PyLink client/server."""
|
||||
|
||||
if (not self.irc.isInternalClient(numeric)) and \
|
||||
(not self.irc.isInternalServer(numeric)):
|
||||
if (not self.is_internal_client(numeric)) and \
|
||||
(not self.is_internal_server(numeric)):
|
||||
raise LookupError('No such PyLink client/server exists.')
|
||||
|
||||
# From TS6 docs:
|
||||
@ -168,155 +129,98 @@ class TS6BaseProtocol(IRCS2SProtocol):
|
||||
# the kill followed by a space and a parenthesized reason. To avoid overflow,
|
||||
# it is recommended not to add anything to the path.
|
||||
|
||||
assert target in self.irc.users, "Unknown target %r for kill()!" % target
|
||||
assert target in self.users, "Unknown target %r for kill()!" % target
|
||||
|
||||
if numeric in self.irc.users:
|
||||
if numeric in self.users:
|
||||
# Killer was an user. Follow examples of setting the path to be "killer.host!killer.nick".
|
||||
userobj = self.irc.users[numeric]
|
||||
userobj = self.users[numeric]
|
||||
killpath = '%s!%s' % (userobj.host, userobj.nick)
|
||||
elif numeric in self.irc.servers:
|
||||
elif numeric in self.servers:
|
||||
# Sender was a server; killpath is just its name.
|
||||
killpath = self.irc.servers[numeric].name
|
||||
killpath = self.servers[numeric].name
|
||||
else:
|
||||
# Invalid sender?! This shouldn't happen, but make the killpath our server name anyways.
|
||||
log.warning('(%s) Invalid sender %s for kill(); using our server name instead.',
|
||||
self.irc.name, numeric)
|
||||
killpath = self.irc.servers[self.irc.sid].name
|
||||
self.name, numeric)
|
||||
killpath = self.servers[self.sid].name
|
||||
|
||||
self._send(numeric, 'KILL %s :%s (%s)' % (target, killpath, reason))
|
||||
self.removeClient(target)
|
||||
self._send_with_prefix(numeric, 'KILL %s :%s (%s)' % (target, killpath, reason))
|
||||
self._remove_client(target)
|
||||
|
||||
def nick(self, numeric, newnick):
|
||||
"""Changes the nick of a PyLink client."""
|
||||
if not self.irc.isInternalClient(numeric):
|
||||
if not self.is_internal_client(numeric):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
|
||||
self._send(numeric, 'NICK %s %s' % (newnick, int(time.time())))
|
||||
self._send_with_prefix(numeric, 'NICK %s %s' % (newnick, int(time.time())))
|
||||
|
||||
self.irc.users[numeric].nick = newnick
|
||||
self.users[numeric].nick = newnick
|
||||
|
||||
# Update the NICK TS.
|
||||
self.irc.users[numeric].ts = int(time.time())
|
||||
self.users[numeric].ts = int(time.time())
|
||||
|
||||
def part(self, client, channel, reason=None):
|
||||
"""Sends a part from a PyLink client."""
|
||||
channel = self.irc.toLower(channel)
|
||||
if not self.irc.isInternalClient(client):
|
||||
log.error('(%s) Error trying to part %r from %r (no such client exists)', self.irc.name, client, channel)
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
msg = "PART %s" % channel
|
||||
if reason:
|
||||
msg += " :%s" % reason
|
||||
self._send(client, msg)
|
||||
self.handle_part(client, 'PART', [channel])
|
||||
|
||||
def quit(self, numeric, reason):
|
||||
"""Quits a PyLink client."""
|
||||
if self.irc.isInternalClient(numeric):
|
||||
self._send(numeric, "QUIT :%s" % reason)
|
||||
self.removeClient(numeric)
|
||||
else:
|
||||
raise LookupError("No such PyLink client exists.")
|
||||
|
||||
def message(self, numeric, target, text):
|
||||
"""Sends a PRIVMSG from a PyLink client."""
|
||||
if not self.irc.isInternalClient(numeric):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
|
||||
# Mangle message targets for IRCds that require it.
|
||||
target = self._expandPUID(target)
|
||||
|
||||
self._send(numeric, 'PRIVMSG %s :%s' % (target, text))
|
||||
|
||||
def notice(self, numeric, target, text):
|
||||
"""Sends a NOTICE from a PyLink client or server."""
|
||||
if (not self.irc.isInternalClient(numeric)) and \
|
||||
(not self.irc.isInternalServer(numeric)):
|
||||
raise LookupError('No such PyLink client/server exists.')
|
||||
|
||||
# Mangle message targets for IRCds that require it.
|
||||
target = self._expandPUID(target)
|
||||
|
||||
self._send(numeric, 'NOTICE %s :%s' % (target, text))
|
||||
|
||||
def topic(self, numeric, target, text):
|
||||
"""Sends a TOPIC change from a PyLink client."""
|
||||
if not self.irc.isInternalClient(numeric):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
self._send(numeric, 'TOPIC %s :%s' % (target, text))
|
||||
self.irc.channels[target].topic = text
|
||||
self.irc.channels[target].topicset = True
|
||||
|
||||
def spawnServer(self, name, sid=None, uplink=None, desc=None, endburst_delay=0):
|
||||
def spawn_server(self, name, sid=None, uplink=None, desc=None):
|
||||
"""
|
||||
Spawns a server off a PyLink server. desc (server description)
|
||||
defaults to the one in the config. uplink defaults to the main PyLink
|
||||
server, and sid (the server ID) is automatically generated if not
|
||||
given.
|
||||
|
||||
Note: TS6 doesn't use a specific ENDBURST command, so the endburst_delay
|
||||
option will be ignored if given.
|
||||
"""
|
||||
# -> :0AL SID test.server 1 0XY :some silly pseudoserver
|
||||
uplink = uplink or self.irc.sid
|
||||
uplink = uplink or self.sid
|
||||
name = name.lower()
|
||||
desc = desc or self.irc.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc']
|
||||
desc = desc or self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']
|
||||
|
||||
if sid is None: # No sid given; generate one!
|
||||
sid = self.sidgen.next_sid()
|
||||
assert len(sid) == 3, "Incorrect SID length"
|
||||
if sid in self.irc.servers:
|
||||
if sid in self.servers:
|
||||
raise ValueError('A server with SID %r already exists!' % sid)
|
||||
for server in self.irc.servers.values():
|
||||
for server in self.servers.values():
|
||||
if name == server.name:
|
||||
raise ValueError('A server named %r already exists!' % name)
|
||||
if not self.irc.isInternalServer(uplink):
|
||||
if not self.is_internal_server(uplink):
|
||||
raise ValueError('Server %r is not a PyLink server!' % uplink)
|
||||
if not utils.isServerName(name):
|
||||
if not self.is_server_name(name):
|
||||
raise ValueError('Invalid server name %r' % name)
|
||||
self._send(uplink, 'SID %s 1 %s :%s' % (name, sid, desc))
|
||||
self.irc.servers[sid] = IrcServer(uplink, name, internal=True, desc=desc)
|
||||
return sid
|
||||
|
||||
def squit(self, source, target, text='No reason given'):
|
||||
"""SQUITs a PyLink server."""
|
||||
# -> SQUIT 9PZ :blah, blah
|
||||
log.debug('source=%s, target=%s', source, target)
|
||||
self._send(source, 'SQUIT %s :%s' % (target, text))
|
||||
self.handle_squit(source, 'SQUIT', [target, text])
|
||||
self.servers[sid] = Server(self, uplink, name, internal=True, desc=desc)
|
||||
self._send_with_prefix(uplink, 'SID %s %s %s :%s' % (name, self.servers[sid].hopcount, sid, desc))
|
||||
return sid
|
||||
|
||||
def away(self, source, text):
|
||||
"""Sends an AWAY message from a PyLink client. <text> can be an empty string
|
||||
to unset AWAY status."""
|
||||
if text:
|
||||
self._send(source, 'AWAY :%s' % text)
|
||||
self._send_with_prefix(source, 'AWAY :%s' % text)
|
||||
else:
|
||||
self._send(source, 'AWAY')
|
||||
self.irc.users[source].away = text
|
||||
self._send_with_prefix(source, 'AWAY')
|
||||
self.users[source].away = text
|
||||
|
||||
### HANDLERS
|
||||
def handle_kick(self, source, command, args):
|
||||
"""Handles incoming KICKs."""
|
||||
# :70MAAAAAA KICK #test 70MAAAAAA :some reason
|
||||
channel = self.irc.toLower(args[0])
|
||||
kicked = self._getUid(args[1])
|
||||
|
||||
def handle_knock(self, numeric, command, args):
|
||||
"""Handles channel KNOCKs."""
|
||||
# InspIRCd:
|
||||
# <- :70MAAAAAA ENCAP * KNOCK #blah :abcdefg
|
||||
# Charybdis:
|
||||
# <- :42XAAAAAC KNOCK #endlessvoid
|
||||
# UnrealIRCd propagates knocks as a channel notice to all ops, so this handler is not used there.
|
||||
channel = args[0]
|
||||
try:
|
||||
reason = args[2]
|
||||
text = args[1]
|
||||
except IndexError:
|
||||
reason = ''
|
||||
|
||||
log.debug('(%s) Removing kick target %s from %s', self.irc.name, kicked, channel)
|
||||
self.handle_part(kicked, 'KICK', [channel, reason])
|
||||
return {'channel': channel, 'target': kicked, 'text': reason}
|
||||
text = ''
|
||||
return {'channel': channel, 'text': text}
|
||||
|
||||
def handle_nick(self, numeric, command, args):
|
||||
"""Handles incoming NICK changes."""
|
||||
# <- :70MAAAAAA NICK GL-devel 1434744242
|
||||
oldnick = self.irc.users[numeric].nick
|
||||
newnick = self.irc.users[numeric].nick = args[0]
|
||||
oldnick = self.users[numeric].nick
|
||||
newnick = self.users[numeric].nick = args[0]
|
||||
|
||||
# Update the nick TS.
|
||||
self.irc.users[numeric].ts = ts = int(args[1])
|
||||
self.users[numeric].ts = ts = int(args[1])
|
||||
|
||||
return {'newnick': newnick, 'oldnick': oldnick, 'ts': ts}
|
||||
|
||||
@ -331,47 +235,32 @@ class TS6BaseProtocol(IRCS2SProtocol):
|
||||
# -> :0AL000001 NICK Derp_ 1433728673
|
||||
# <- :70M SAVE 0AL000001 1433728673
|
||||
user = args[0]
|
||||
oldnick = self.irc.users[user].nick
|
||||
self.irc.users[user].nick = user
|
||||
oldnick = self.users[user].nick
|
||||
self.users[user].nick = user
|
||||
|
||||
# TS6 SAVE sets nick TS to 100. This is hardcoded in InspIRCd and
|
||||
# charybdis.
|
||||
self.irc.users[user].ts = 100
|
||||
self.users[user].ts = 100
|
||||
|
||||
return {'target': user, 'ts': 100, 'oldnick': oldnick}
|
||||
|
||||
def handle_topic(self, numeric, command, args):
|
||||
"""Handles incoming TOPIC changes from clients. For topic bursts,
|
||||
TB (TS6/charybdis) and FTOPIC (InspIRCd) are used instead."""
|
||||
# <- :70MAAAAAA TOPIC #test :test
|
||||
channel = self.irc.toLower(args[0])
|
||||
topic = args[1]
|
||||
def handle_server(self, numeric, command, args):
|
||||
"""Handles the SERVER command, used for introducing older (TS5) servers."""
|
||||
# <- :services.int SERVER a.bc 2 :(H) [GL] test jupe
|
||||
servername = args[0].lower()
|
||||
sdesc = args[-1]
|
||||
self.servers[servername] = Server(self, numeric, servername, desc=sdesc)
|
||||
return {'name': servername, 'sid': None, 'text': sdesc}
|
||||
|
||||
oldtopic = self.irc.channels[channel].topic
|
||||
self.irc.channels[channel].topic = topic
|
||||
self.irc.channels[channel].topicset = True
|
||||
|
||||
return {'channel': channel, 'setter': numeric, 'text': topic,
|
||||
'oldtopic': oldtopic}
|
||||
|
||||
def handle_part(self, source, command, args):
|
||||
"""Handles incoming PART commands."""
|
||||
channels = self.irc.toLower(args[0]).split(',')
|
||||
for channel in channels:
|
||||
# We should only get PART commands for channels that exist, right??
|
||||
self.irc.channels[channel].removeuser(source)
|
||||
try:
|
||||
self.irc.users[source].channels.discard(channel)
|
||||
except KeyError:
|
||||
log.debug("(%s) handle_part: KeyError trying to remove %r from %r's channel list?", self.irc.name, channel, source)
|
||||
try:
|
||||
reason = args[1]
|
||||
except IndexError:
|
||||
reason = ''
|
||||
# Clear empty non-permanent channels.
|
||||
if not (self.irc.channels[channel].users or ((self.irc.cmodes.get('permanent'), None) in self.irc.channels[channel].modes)):
|
||||
del self.irc.channels[channel]
|
||||
return {'channels': channels, 'text': reason}
|
||||
def handle_sid(self, numeric, command, args):
|
||||
"""Handles the SID command, used for introducing remote servers by our uplink."""
|
||||
# <- SID services.int 2 00A :Shaltúre IRC Services
|
||||
# parameters: server name, hopcount, sid, server description
|
||||
sname = args[0].lower()
|
||||
sid = args[2]
|
||||
sdesc = args[-1]
|
||||
self.servers[sid] = Server(self, numeric, sname, desc=sdesc)
|
||||
return {'name': sname, 'sid': sid, 'text': sdesc}
|
||||
|
||||
def handle_svsnick(self, source, command, args):
|
||||
"""Handles SVSNICK (forced nickname change attempts)."""
|
||||
@ -381,4 +270,4 @@ class TS6BaseProtocol(IRCS2SProtocol):
|
||||
|
||||
# UnrealIRCd:
|
||||
# <- :services.midnight.vpn SVSNICK GL Guest87795 1468303726
|
||||
return {'target': self._getUid(args[0]), 'newnick': args[1]}
|
||||
return {'target': self._get_UID(args[0]), 'newnick': args[1]}
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
unreal.py: UnrealIRCd 4.0 protocol module for PyLink.
|
||||
unreal.py: UnrealIRCd 4.x protocol module for PyLink.
|
||||
"""
|
||||
|
||||
import time
|
||||
@ -14,15 +14,15 @@ from pylinkirc.protocols.ts6_common import *
|
||||
|
||||
SJOIN_PREFIXES = {'q': '*', 'a': '~', 'o': '@', 'h': '%', 'v': '+', 'b': '&', 'e': '"', 'I': "'"}
|
||||
|
||||
# I'm not sure what the real limit is, but the text posted at
|
||||
# https://github.com/GLolol/PyLink/issues/378 suggests 427 characters.
|
||||
# https://github.com/unrealircd/unrealircd/blob/4cad9cb/src/modules/m_server.c#L1260 may
|
||||
# also help. (but why BUFSIZE-*80*?) -GL
|
||||
S2S_BUFSIZE = 427
|
||||
|
||||
class UnrealProtocol(TS6BaseProtocol):
|
||||
def __init__(self, irc):
|
||||
super().__init__(irc)
|
||||
# I'm not sure what the real limit is, but the text posted at
|
||||
# https://github.com/jlu5/PyLink/issues/378 suggests 427 characters.
|
||||
# https://github.com/unrealircd/unrealircd/blob/4cad9cb/src/modules/m_server.c#L1260 may
|
||||
# also help. (but why BUFSIZE-*80*?) -GL
|
||||
S2S_BUFSIZE = 427
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.protocol_caps |= {'slash-in-nicks', 'underscore-in-hosts'}
|
||||
# Set our case mapping (rfc1459 maps "\" and "|" together, for example)
|
||||
self.casemapping = 'ascii'
|
||||
@ -34,29 +34,16 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
'EOS': 'ENDBURST'}
|
||||
|
||||
self.caps = []
|
||||
self.irc.prefixmodes = {'q': '~', 'a': '&', 'o': '@', 'h': '%', 'v': '+'}
|
||||
self.prefixmodes = {'q': '~', 'a': '&', 'o': '@', 'h': '%', 'v': '+'}
|
||||
|
||||
self.needed_caps = ["VL", "SID", "CHANMODES", "NOQUIT", "SJ3", "NICKIP", "UMODE2", "SJOIN"]
|
||||
|
||||
# Some command aliases
|
||||
# Command aliases to handlers defined in parent modules
|
||||
self.handle_svskill = self.handle_kill
|
||||
|
||||
def _expandPUID(self, uid):
|
||||
"""
|
||||
Returns the outgoing nick for the given UID. For PUIDs (used to store UID-less
|
||||
3.2 users), this will change the PUID given to the actual user's nick,
|
||||
so that that the older IRCds can understand it.
|
||||
"""
|
||||
if uid in self.irc.users and '@' in uid:
|
||||
# UID exists and has a @ in it, meaning it's a PUID (orignick@counter style).
|
||||
# Return this user's nick accordingly.
|
||||
nick = self.irc.users[uid].nick
|
||||
log.debug('(%s) Mangling target PUID %s to nick %s', self.irc.name, uid, nick)
|
||||
return nick
|
||||
return uid
|
||||
self.topic_burst = self.topic
|
||||
|
||||
### OUTGOING COMMAND FUNCTIONS
|
||||
def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(),
|
||||
def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(),
|
||||
server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator',
|
||||
manipulatable=False):
|
||||
"""
|
||||
@ -65,8 +52,8 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
Note: No nick collision / valid nickname checks are done here; it is
|
||||
up to plugins to make sure they don't introduce anything invalid.
|
||||
"""
|
||||
server = server or self.irc.sid
|
||||
if not self.irc.isInternalServer(server):
|
||||
server = server or self.sid
|
||||
if not self.is_internal_server(server):
|
||||
raise ValueError('Server %r is not a PyLink server!' % server)
|
||||
|
||||
# Unreal 4.0 uses TS6-style UIDs. They don't start from AAAAAA like other IRCd's
|
||||
@ -74,18 +61,18 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
uid = self.uidgen[server].next_uid()
|
||||
|
||||
ts = ts or int(time.time())
|
||||
realname = realname or conf.conf['bot']['realname']
|
||||
realname = realname or conf.conf['pylink']['realname']
|
||||
realhost = realhost or host
|
||||
|
||||
# Add +xt so that vHost cloaking always works.
|
||||
modes = set(modes) # Ensure type safety
|
||||
modes |= {('+x', None), ('+t', None)}
|
||||
|
||||
raw_modes = self.irc.joinModes(modes)
|
||||
u = self.irc.users[uid] = IrcUser(nick, ts, uid, server, ident=ident, host=host, realname=realname,
|
||||
raw_modes = self.join_modes(modes)
|
||||
u = self.users[uid] = User(self, nick, ts, uid, server, ident=ident, host=host, realname=realname,
|
||||
realhost=realhost, ip=ip, manipulatable=manipulatable, opertype=opertype)
|
||||
self.irc.applyModes(uid, modes)
|
||||
self.irc.servers[server].users.add(uid)
|
||||
self.apply_modes(uid, modes)
|
||||
self.servers[server].users.add(uid)
|
||||
|
||||
# UnrealIRCd requires encoding the IP by first packing it into a binary format,
|
||||
# and then encoding the binary with Base64.
|
||||
@ -106,22 +93,22 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
encoded_ip = encoded_ip.strip().decode()
|
||||
|
||||
# <- :001 UID GL 0 1441306929 gl localhost 0018S7901 0 +iowx * midnight-1C620195 fwAAAQ== :realname
|
||||
self._send(server, "UID {nick} 0 {ts} {ident} {realhost} {uid} 0 {modes} "
|
||||
"{host} * {ip} :{realname}".format(ts=ts, host=host,
|
||||
nick=nick, ident=ident, uid=uid,
|
||||
modes=raw_modes, realname=realname,
|
||||
realhost=realhost, ip=encoded_ip))
|
||||
self._send_with_prefix(server, "UID {nick} {hopcount} {ts} {ident} {realhost} {uid} 0 {modes} "
|
||||
"{host} * {ip} :{realname}".format(ts=ts, host=host,
|
||||
nick=nick, ident=ident, uid=uid,
|
||||
modes=raw_modes, realname=realname,
|
||||
realhost=realhost, ip=encoded_ip,
|
||||
hopcount=self.servers[server].hopcount))
|
||||
|
||||
return u
|
||||
|
||||
def join(self, client, channel):
|
||||
"""Joins a PyLink client to a channel."""
|
||||
channel = self.irc.toLower(channel)
|
||||
if not self.irc.isInternalClient(client):
|
||||
if not self.is_internal_client(client):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
self._send(client, "JOIN %s" % channel)
|
||||
self.irc.channels[channel].users.add(client)
|
||||
self.irc.users[client].channels.add(channel)
|
||||
self._send_with_prefix(client, "JOIN %s" % channel)
|
||||
self._channels[channel].users.add(client)
|
||||
self.users[client].channels.add(channel)
|
||||
|
||||
def sjoin(self, server, channel, users, ts=None, modes=set()):
|
||||
"""Sends an SJOIN for a group of users to a channel.
|
||||
@ -132,17 +119,16 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
|
||||
Example uses:
|
||||
sjoin('100', '#test', [('', '100AAABBC'), ('o', 100AAABBB'), ('v', '100AAADDD')])
|
||||
sjoin(self.irc.sid, '#test', [('o', self.irc.pseudoclient.uid)])
|
||||
sjoin(self.sid, '#test', [('o', self.pseudoclient.uid)])
|
||||
"""
|
||||
# <- :001 SJOIN 1444361345 #test :*@+1JJAAAAAB %2JJAAAA4C 1JJAAAADS
|
||||
channel = self.irc.toLower(channel)
|
||||
server = server or self.irc.sid
|
||||
server = server or self.sid
|
||||
assert users, "sjoin: No users sent?"
|
||||
if not server:
|
||||
raise LookupError('No such PyLink server exists.')
|
||||
|
||||
changedmodes = set(modes or self.irc.channels[channel].modes)
|
||||
orig_ts = self.irc.channels[channel].ts
|
||||
changedmodes = set(modes or self._channels[channel].modes)
|
||||
orig_ts = self._channels[channel].ts
|
||||
ts = ts or orig_ts
|
||||
uids = []
|
||||
itemlist = []
|
||||
@ -163,17 +149,17 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
uids.append(user)
|
||||
|
||||
try:
|
||||
self.irc.users[user].channels.add(channel)
|
||||
self.users[user].channels.add(channel)
|
||||
except KeyError: # Not initialized yet?
|
||||
log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user)
|
||||
log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.name, channel, user)
|
||||
|
||||
# Track simple modes separately.
|
||||
simplemodes = set()
|
||||
for modepair in modes:
|
||||
if modepair[0][-1] in self.irc.cmodes['*A']:
|
||||
if modepair[0][-1] in self.cmodes['*A']:
|
||||
# Bans, exempts, invex get expanded to forms like "&*!*@some.host" in SJOIN.
|
||||
|
||||
if (modepair[0][-1], modepair[1]) in self.irc.channels[channel].modes:
|
||||
if (modepair[0][-1], modepair[1]) in self._channels[channel].modes:
|
||||
# Mode is already set; skip it.
|
||||
continue
|
||||
|
||||
@ -189,61 +175,62 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
|
||||
# Modes are optional; add them if they exist
|
||||
if modes:
|
||||
sjoin_prefix += " %s" % self.irc.joinModes(simplemodes)
|
||||
sjoin_prefix += " %s" % self.join_modes(simplemodes)
|
||||
|
||||
sjoin_prefix += " :"
|
||||
# Wrap arguments to the max supported S2S line length to prevent cutoff
|
||||
# (https://github.com/GLolol/PyLink/issues/378)
|
||||
for line in utils.wrapArguments(sjoin_prefix, itemlist, S2S_BUFSIZE):
|
||||
self.irc.send(line)
|
||||
# (https://github.com/jlu5/PyLink/issues/378)
|
||||
for line in utils.wrap_arguments(sjoin_prefix, itemlist, self.S2S_BUFSIZE):
|
||||
self.send(line)
|
||||
|
||||
self.irc.channels[channel].users.update(uids)
|
||||
self._channels[channel].users.update(uids)
|
||||
|
||||
self.updateTS(server, channel, ts, changedmodes)
|
||||
|
||||
def ping(self, source=None, target=None):
|
||||
"""Sends a PING to a target server. Periodic PINGs are sent to our uplink
|
||||
automatically by the Irc() internals; plugins shouldn't have to use this."""
|
||||
source = source or self.irc.sid
|
||||
target = target or self.irc.uplink
|
||||
if not (target is None or source is None):
|
||||
self._send(source, 'PING %s %s' % (self.irc.servers[source].name, self.irc.servers[target].name))
|
||||
def _ping_uplink(self):
|
||||
"""Sends a PING to the uplink."""
|
||||
if self.sid and self.uplink:
|
||||
self._send_with_prefix(self.sid, 'PING %s %s' % (self.get_friendly_name(self.sid), self.get_friendly_name(self.uplink)))
|
||||
|
||||
def mode(self, numeric, target, modes, ts=None):
|
||||
"""
|
||||
Sends mode changes from a PyLink client/server. The mode list should be
|
||||
a list of (mode, arg) tuples, i.e. the format of utils.parseModes() output.
|
||||
a list of (mode, arg) tuples, i.e. the format of utils.parse_modes() output.
|
||||
"""
|
||||
# <- :unreal.midnight.vpn MODE #test +ntCo GL 1444361345
|
||||
|
||||
if (not self.irc.isInternalClient(numeric)) and \
|
||||
(not self.irc.isInternalServer(numeric)):
|
||||
if (not self.is_internal_client(numeric)) and \
|
||||
(not self.is_internal_server(numeric)):
|
||||
raise LookupError('No such PyLink client/server exists.')
|
||||
|
||||
self.irc.applyModes(target, modes)
|
||||
self.apply_modes(target, modes)
|
||||
|
||||
if utils.isChannel(target):
|
||||
if self.is_channel(target):
|
||||
|
||||
# Fix assignment TypeError in the expandPUID bit (sets can't be
|
||||
# assigned to by index).
|
||||
modes = list(modes)
|
||||
|
||||
modes = list(modes) # Needed for indexing
|
||||
# Make sure we expand any PUIDs when sending outgoing modes...
|
||||
for idx, mode in enumerate(modes):
|
||||
if mode[0][-1] in self.irc.prefixmodes:
|
||||
log.debug('(%s) mode: expanding PUID of mode %s', self.irc.name, str(mode))
|
||||
if mode[0][-1] in self.prefixmodes:
|
||||
log.debug('(%s) mode: expanding PUID of mode %s', self.name, str(mode))
|
||||
modes[idx] = (mode[0], self._expandPUID(mode[1]))
|
||||
|
||||
# The MODE command is used for channel mode changes only
|
||||
ts = ts or self.irc.channels[self.irc.toLower(target)].ts
|
||||
ts = ts or self._channels[target].ts
|
||||
|
||||
# 7 characters for "MODE", the space between MODE and the target, the space between the
|
||||
# target and mode list, and the space between the mode list and TS.
|
||||
bufsize = S2S_BUFSIZE - 7
|
||||
bufsize = self.S2S_BUFSIZE - 7
|
||||
|
||||
# Subtract the length of the TS and channel arguments
|
||||
bufsize -= len(str(ts))
|
||||
bufsize -= len(target)
|
||||
|
||||
# Subtract the prefix (":SID " for servers or ":SIDAAAAAA " for servers)
|
||||
bufsize -= (5 if self.irc.isInternalServer(numeric) else 11)
|
||||
bufsize -= (5 if self.is_internal_server(numeric) else 11)
|
||||
|
||||
# There is also an (undocumented) 15 args per line limit for MODE. The target, mode
|
||||
# characters, and TS take up three args, so we're left with 12 spaces for parameters.
|
||||
@ -252,30 +239,42 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# * *** Warning! Possible desynch: MODE for channel #test ('+bbbbbbbbbbbb *!*@0.1 *!*@1.1 *!*@2.1 *!*@3.1 *!*@4.1 *!*@5.1 *!*@6.1 *!*@7.1 *!*@8.1 *!*@9.1 *!*@10.1 *!*@11.1') has fishy timestamp (12) (from pylink.local/pylink.local)
|
||||
|
||||
# Thanks to kevin and Jobe for helping me debug this!
|
||||
for modestring in self.irc.wrapModes(modes, bufsize, max_modes_per_msg=12):
|
||||
self._send(numeric, 'MODE %s %s %s' % (target, modestring, ts))
|
||||
for modestring in self.wrap_modes(modes, bufsize, max_modes_per_msg=12):
|
||||
self._send_with_prefix(numeric, 'MODE %s %s %s' % (target, modestring, ts))
|
||||
else:
|
||||
# For user modes, the only way to set modes (for non-U:Lined servers)
|
||||
# is through UMODE2, which sets the modes on the caller.
|
||||
# U:Lines can use SVSMODE/SVS2MODE, but I won't expect people to
|
||||
# U:Line a PyLink daemon...
|
||||
if not self.irc.isInternalClient(target):
|
||||
if not self.is_internal_client(target):
|
||||
raise ProtocolError('Cannot force mode change on external clients!')
|
||||
|
||||
# XXX: I don't expect usermode changes to ever get cut off, but length
|
||||
# checks could be added just to be safe...
|
||||
joinedmodes = self.irc.joinModes(modes)
|
||||
self._send(target, 'UMODE2 %s' % joinedmodes)
|
||||
joinedmodes = self.join_modes(modes)
|
||||
self._send_with_prefix(target, 'UMODE2 %s' % joinedmodes)
|
||||
|
||||
def topicBurst(self, numeric, target, text):
|
||||
"""Sends a TOPIC change from a PyLink server."""
|
||||
if not self.irc.isInternalServer(numeric):
|
||||
raise LookupError('No such PyLink server exists.')
|
||||
self._send(numeric, 'TOPIC %s :%s' % (target, text))
|
||||
self.irc.channels[target].topic = text
|
||||
self.irc.channels[target].topicset = True
|
||||
def set_server_ban(self, source, duration, user='*', host='*', reason='User banned'):
|
||||
"""
|
||||
Sets a server ban.
|
||||
"""
|
||||
# Permanent:
|
||||
# <- :unreal.midnight.vpn TKL + G ident host.net james!james@localhost 0 1500303745 :no reason
|
||||
# Temporary:
|
||||
# <- :unreal.midnight.vpn TKL + G * everyone james!james@localhost 1500303702 1500303672 :who needs reasons, do people even read them?
|
||||
assert not (user == host == '*'), "Refusing to set ridiculous ban on *@*"
|
||||
|
||||
def updateClient(self, target, field, text):
|
||||
if source in self.users:
|
||||
# GLINEs are always forwarded from the server as far as I can tell.
|
||||
real_source = self.get_server(source)
|
||||
else:
|
||||
real_source = source
|
||||
|
||||
setter = self.get_hostmask(source) if source in self.users else self.get_friendly_name(source)
|
||||
currtime = int(time.time())
|
||||
self._send_with_prefix(real_source, 'TKL + G %s %s %s %s %s :%s' % (user, host, setter, currtime+duration if duration != 0 else 0, currtime, reason))
|
||||
|
||||
def update_client(self, target, field, text):
|
||||
"""Updates the ident, host, or realname of any connected client."""
|
||||
field = field.upper()
|
||||
|
||||
@ -283,68 +282,62 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
raise NotImplementedError("Changing field %r of a client is "
|
||||
"unsupported by this protocol." % field)
|
||||
|
||||
if self.irc.isInternalClient(target):
|
||||
if self.is_internal_client(target):
|
||||
# It is one of our clients, use SETIDENT/HOST/NAME.
|
||||
if field == 'IDENT':
|
||||
self.irc.users[target].ident = text
|
||||
self._send(target, 'SETIDENT %s' % text)
|
||||
self.users[target].ident = text
|
||||
self._send_with_prefix(target, 'SETIDENT %s' % text)
|
||||
elif field == 'HOST':
|
||||
self.irc.users[target].host = text
|
||||
self._send(target, 'SETHOST %s' % text)
|
||||
self.users[target].host = text
|
||||
self._send_with_prefix(target, 'SETHOST %s' % text)
|
||||
elif field in ('REALNAME', 'GECOS'):
|
||||
self.irc.users[target].realname = text
|
||||
self._send(target, 'SETNAME :%s' % text)
|
||||
self.users[target].realname = text
|
||||
self._send_with_prefix(target, 'SETNAME :%s' % text)
|
||||
else:
|
||||
# It is a client on another server, use CHGIDENT/HOST/NAME.
|
||||
if field == 'IDENT':
|
||||
self.irc.users[target].ident = text
|
||||
self._send(self.irc.sid, 'CHGIDENT %s %s' % (target, text))
|
||||
self.users[target].ident = text
|
||||
self._send_with_prefix(self.sid, 'CHGIDENT %s %s' % (target, text))
|
||||
|
||||
# Send hook payloads for other plugins to listen to.
|
||||
self.irc.callHooks([self.irc.sid, 'CHGIDENT',
|
||||
self.call_hooks([self.sid, 'CHGIDENT',
|
||||
{'target': target, 'newident': text}])
|
||||
|
||||
elif field == 'HOST':
|
||||
self.irc.users[target].host = text
|
||||
self._send(self.irc.sid, 'CHGHOST %s %s' % (target, text))
|
||||
self.users[target].host = text
|
||||
self._send_with_prefix(self.sid, 'CHGHOST %s %s' % (target, text))
|
||||
|
||||
self.irc.callHooks([self.irc.sid, 'CHGHOST',
|
||||
self.call_hooks([self.sid, 'CHGHOST',
|
||||
{'target': target, 'newhost': text}])
|
||||
|
||||
elif field in ('REALNAME', 'GECOS'):
|
||||
self.irc.users[target].realname = text
|
||||
self._send(self.irc.sid, 'CHGNAME %s :%s' % (target, text))
|
||||
self.users[target].realname = text
|
||||
self._send_with_prefix(self.sid, 'CHGNAME %s :%s' % (target, text))
|
||||
|
||||
self.irc.callHooks([self.irc.sid, 'CHGNAME',
|
||||
self.call_hooks([self.sid, 'CHGNAME',
|
||||
{'target': target, 'newgecos': text}])
|
||||
|
||||
def invite(self, numeric, target, channel):
|
||||
"""Sends an INVITE from a PyLink client.."""
|
||||
if not self.irc.isInternalClient(numeric):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
self._send(numeric, 'INVITE %s %s' % (target, channel))
|
||||
|
||||
def knock(self, numeric, target, text):
|
||||
"""Sends a KNOCK from a PyLink client."""
|
||||
# KNOCKs in UnrealIRCd are actually just specially formatted NOTICEs,
|
||||
# sent to all ops in a channel.
|
||||
# <- :unreal.midnight.vpn NOTICE @#test :[Knock] by GL|!gl@hidden-1C620195 (test)
|
||||
assert utils.isChannel(target), "Can only knock on channels!"
|
||||
sender = self.irc.getServer(numeric)
|
||||
s = '[Knock] by %s (%s)' % (self.irc.getHostmask(numeric), text)
|
||||
self._send(sender, 'NOTICE @%s :%s' % (target, s))
|
||||
assert self.is_channel(target), "Can only knock on channels!"
|
||||
sender = self.get_server(numeric)
|
||||
s = '[Knock] by %s (%s)' % (self.get_hostmask(numeric), text)
|
||||
self._send_with_prefix(sender, 'NOTICE @%s :%s' % (target, s))
|
||||
|
||||
### HANDLERS
|
||||
|
||||
def connect(self):
|
||||
def post_connect(self):
|
||||
"""Initializes a connection to a server."""
|
||||
ts = self.irc.start_ts
|
||||
self.irc.prefixmodes = {'q': '~', 'a': '&', 'o': '@', 'h': '%', 'v': '+'}
|
||||
ts = self.start_ts
|
||||
self.prefixmodes = {'q': '~', 'a': '&', 'o': '@', 'h': '%', 'v': '+'}
|
||||
|
||||
# Track usages of legacy (Unreal 3.2) nicks.
|
||||
self.legacy_uidgen = utils.PUIDGenerator('U32user')
|
||||
self.legacy_uidgen = PUIDGenerator('U32user')
|
||||
|
||||
self.irc.umodes.update({'deaf': 'd', 'invisible': 'i', 'hidechans': 'p',
|
||||
self.umodes.update({'deaf': 'd', 'invisible': 'i', 'hidechans': 'p',
|
||||
'protected': 'q', 'registered': 'r',
|
||||
'snomask': 's', 'vhost': 't', 'wallops': 'w',
|
||||
'bot': 'B', 'cloak': 'x', 'ssl': 'z',
|
||||
@ -353,10 +346,10 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
'noctcp': 'T', 'showwhois': 'W',
|
||||
'*A': '', '*B': '', '*C': '', '*D': 'dipqrstwBxzGHIRSTW'})
|
||||
|
||||
f = self.irc.send
|
||||
host = self.irc.serverdata["hostname"]
|
||||
f = self.send
|
||||
host = self.serverdata["hostname"]
|
||||
|
||||
f('PASS :%s' % self.irc.serverdata["sendpass"])
|
||||
f('PASS :%s' % self.serverdata["sendpass"])
|
||||
# https://github.com/unrealircd/unrealircd/blob/2f8cb55e/doc/technical/protoctl.txt
|
||||
# We support the following protocol features:
|
||||
# SJOIN - supports SJOIN for user introduction
|
||||
@ -375,14 +368,23 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# not work for any UnrealIRCd 3.2 users.
|
||||
# ESVID - Supports account names in services stamps instead of just the signon time.
|
||||
# AFAIK this doesn't actually affect services' behaviour?
|
||||
f('PROTOCTL SJOIN SJ3 NOQUIT NICKv2 VL UMODE2 PROTOCTL NICKIP EAUTH=%s SID=%s VHP ESVID' % (self.irc.serverdata["hostname"], self.irc.sid))
|
||||
sdesc = self.irc.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc']
|
||||
f('SERVER %s 1 U%s-h6e-%s :%s' % (host, self.proto_ver, self.irc.sid, sdesc))
|
||||
f('NETINFO 1 %s %s * 0 0 0 :%s' % (self.irc.start_ts, self.proto_ver, self.irc.serverdata.get("netname", self.irc.name)))
|
||||
self._send(self.irc.sid, 'EOS')
|
||||
f('PROTOCTL SJOIN SJ3 NOQUIT NICKv2 VL UMODE2 PROTOCTL NICKIP EAUTH=%s SID=%s VHP ESVID' % (self.serverdata["hostname"], self.sid))
|
||||
sdesc = self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']
|
||||
f('SERVER %s 1 U%s-h6e-%s :%s' % (host, self.proto_ver, self.sid, sdesc))
|
||||
f('NETINFO 1 %s %s * 0 0 0 :%s' % (self.start_ts, self.proto_ver, self.serverdata.get("netname", self.name)))
|
||||
self._send_with_prefix(self.sid, 'EOS')
|
||||
|
||||
# Extban definitions
|
||||
self.extbans_acting = {'quiet': '~q:', 'ban_nonick': '~n:', 'ban_nojoins': '~j:',
|
||||
'filter': '~T:block:', 'filter_censor': '~T:censor:'}
|
||||
self.extbans_matching = {'ban_account': '~a:', 'ban_inchannel': '~c:', 'ban_opertype': '~O:',
|
||||
'ban_realname': '~r:', 'ban_account_legacy': '~R:', 'ban_certfp': '~S:'}
|
||||
|
||||
def handle_eos(self, numeric, command, args):
|
||||
"""EOS is used to denote end of burst."""
|
||||
self.servers[numeric].has_eob = True
|
||||
if numeric == self.uplink:
|
||||
self.connected.set()
|
||||
return {}
|
||||
|
||||
def handle_uid(self, numeric, command, args):
|
||||
@ -391,8 +393,9 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# arguments: nick, hopcount?, ts, ident, real-host, UID, services account (0 if none), modes,
|
||||
# displayed host, cloaked (+x) host, base64-encoded IP, and realname
|
||||
nick = args[0]
|
||||
self.check_nick_collision(nick)
|
||||
self._check_nick_collision(nick)
|
||||
ts, ident, realhost, uid, accountname, modestring, host = args[2:9]
|
||||
ts = int(ts)
|
||||
|
||||
if host == '*':
|
||||
# A single * means that there is no displayed/virtual host, and
|
||||
@ -419,32 +422,30 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
|
||||
realname = args[-1]
|
||||
|
||||
self.irc.users[uid] = IrcUser(nick, ts, uid, numeric, ident, host, realname, realhost, ip)
|
||||
self.irc.servers[numeric].users.add(uid)
|
||||
self.users[uid] = User(self, nick, ts, uid, numeric, ident, host, realname, realhost, ip)
|
||||
self.servers[numeric].users.add(uid)
|
||||
|
||||
# Handle user modes
|
||||
parsedmodes = self.irc.parseModes(uid, [modestring])
|
||||
self.irc.applyModes(uid, parsedmodes)
|
||||
parsedmodes = self.parse_modes(uid, [modestring])
|
||||
self.apply_modes(uid, parsedmodes)
|
||||
|
||||
# The cloaked (+x) host is completely separate from the displayed host
|
||||
# and real host in that it is ONLY shown if the user is +x (cloak mode
|
||||
# enabled) but NOT +t (vHost set).
|
||||
self.irc.users[uid].cloaked_host = args[9]
|
||||
self.users[uid].cloaked_host = args[9]
|
||||
|
||||
if ('+o', None) in parsedmodes:
|
||||
# If +o being set, call the CLIENT_OPERED internal hook.
|
||||
self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': 'IRC Operator'}])
|
||||
self._check_oper_status_change(uid, parsedmodes)
|
||||
|
||||
if ('+x', None) not in parsedmodes:
|
||||
# If +x is not set, update to use the person's real host.
|
||||
self.irc.users[uid].host = realhost
|
||||
self.users[uid].host = realhost
|
||||
|
||||
# Set the account name if present: if this is a number, set it to the user nick.
|
||||
if ('+r', None) in parsedmodes and accountname.isdigit():
|
||||
accountname = nick
|
||||
|
||||
if not accountname.isdigit():
|
||||
self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}])
|
||||
self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}])
|
||||
|
||||
# parse_as is used here to prevent legacy user introduction from being confused
|
||||
# with a nick change.
|
||||
@ -453,19 +454,19 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
|
||||
def handle_pass(self, numeric, command, args):
|
||||
# <- PASS :abcdefg
|
||||
if args[0] != self.irc.serverdata['recvpass']:
|
||||
raise ProtocolError("Error: RECVPASS from uplink does not match configuration!")
|
||||
if args[0] != self.serverdata['recvpass']:
|
||||
raise ProtocolError("RECVPASS from uplink does not match configuration!")
|
||||
|
||||
def handle_ping(self, numeric, command, args):
|
||||
if numeric == self.irc.uplink:
|
||||
self.irc.send('PONG %s :%s' % (self.irc.serverdata['hostname'], args[-1]), queue=False)
|
||||
if numeric == self.uplink:
|
||||
self.send('PONG %s :%s' % (self.serverdata['hostname'], args[-1]), queue=False)
|
||||
|
||||
def handle_server(self, numeric, command, args):
|
||||
"""Handles the SERVER command, which is used for both authentication and
|
||||
introducing legacy (non-SID) servers."""
|
||||
# <- SERVER unreal.midnight.vpn 1 :U3999-Fhin6OoEM UnrealIRCd test server
|
||||
sname = args[0]
|
||||
if numeric == self.irc.uplink and not self.irc.connected.is_set(): # We're doing authentication
|
||||
if self.uplink not in self.servers: # We're doing authentication
|
||||
for cap in self.needed_caps:
|
||||
if cap not in self.caps:
|
||||
raise ProtocolError("Not all required capabilities were met "
|
||||
@ -489,35 +490,12 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
if protover < self.min_proto_ver:
|
||||
raise ProtocolError("Protocol version too old! (needs at least %s "
|
||||
"(Unreal 4.x), got %s)" % (self.min_proto_ver, protover))
|
||||
self.irc.servers[numeric] = IrcServer(None, sname, desc=sdesc)
|
||||
self.servers[numeric] = Server(self, None, sname, desc=sdesc)
|
||||
|
||||
# Set irc.connected to True, meaning that protocol negotiation passed.
|
||||
log.debug('(%s) self.irc.connected set!', self.irc.name)
|
||||
self.irc.connected.set()
|
||||
else:
|
||||
# Legacy (non-SID) servers can still be introduced using the SERVER command.
|
||||
# <- :services.int SERVER a.bc 2 :(H) [GL] a
|
||||
servername = args[0].lower()
|
||||
sdesc = args[-1]
|
||||
self.irc.servers[servername] = IrcServer(numeric, servername, desc=sdesc)
|
||||
return {'name': servername, 'sid': None, 'text': sdesc}
|
||||
|
||||
def handle_sid(self, numeric, command, args):
|
||||
"""Handles the SID command, used for introducing remote servers by our uplink."""
|
||||
# <- SID services.int 2 00A :Shaltúre IRC Services
|
||||
sname = args[0].lower()
|
||||
sid = args[2]
|
||||
sdesc = args[-1]
|
||||
self.irc.servers[sid] = IrcServer(numeric, sname, desc=sdesc)
|
||||
return {'name': sname, 'sid': sid, 'text': sdesc}
|
||||
|
||||
def handle_squit(self, numeric, command, args):
|
||||
"""Handles the SQUIT command."""
|
||||
# <- SQUIT services.int :Read error
|
||||
# Convert the server name to a SID...
|
||||
args[0] = self._getSid(args[0])
|
||||
# Then, use the SQUIT handler in TS6BaseProtocol as usual.
|
||||
return super().handle_squit(numeric, 'SQUIT', args)
|
||||
return super().handle_server(numeric, command, args)
|
||||
|
||||
def handle_protoctl(self, numeric, command, args):
|
||||
"""Handles protocol negotiation."""
|
||||
@ -538,18 +516,18 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# <- PROTOCTL CHANMODES=beI,k,l,psmntirzMQNRTOVKDdGPZSCc NICKCHARS= SID=001 MLOCK TS=1441314501 EXTSWHOIS
|
||||
for cap in args:
|
||||
if cap.startswith('SID'):
|
||||
self.irc.uplink = cap.split('=', 1)[1]
|
||||
self.uplink = cap.split('=', 1)[1]
|
||||
elif cap.startswith('CHANMODES'):
|
||||
# Parse all the supported channel modes.
|
||||
supported_cmodes = cap.split('=', 1)[1]
|
||||
self.irc.cmodes['*A'], self.irc.cmodes['*B'], self.irc.cmodes['*C'], self.irc.cmodes['*D'] = supported_cmodes.split(',')
|
||||
self.cmodes['*A'], self.cmodes['*B'], self.cmodes['*C'], self.cmodes['*D'] = supported_cmodes.split(',')
|
||||
for namedmode, modechar in cmodes.items():
|
||||
if modechar in supported_cmodes:
|
||||
self.irc.cmodes[namedmode] = modechar
|
||||
self.irc.cmodes['*B'] += 'f' # Add +f to the list too, dunno why it isn't there.
|
||||
self.cmodes[namedmode] = modechar
|
||||
self.cmodes['*B'] += 'f' # Add +f to the list too, dunno why it isn't there.
|
||||
|
||||
# Add in the supported prefix modes.
|
||||
self.irc.cmodes.update({'halfop': 'h', 'admin': 'a', 'owner': 'q',
|
||||
self.cmodes.update({'halfop': 'h', 'admin': 'a', 'owner': 'q',
|
||||
'op': 'o', 'voice': 'v'})
|
||||
|
||||
def handle_join(self, numeric, command, args):
|
||||
@ -557,38 +535,34 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# <- :GL JOIN #pylink,#test
|
||||
if args[0] == '0':
|
||||
# /join 0; part the user from all channels
|
||||
oldchans = self.irc.users[numeric].channels.copy()
|
||||
oldchans = self.users[numeric].channels.copy()
|
||||
log.debug('(%s) Got /join 0 from %r, channel list is %r',
|
||||
self.irc.name, numeric, oldchans)
|
||||
self.name, numeric, oldchans)
|
||||
for ch in oldchans:
|
||||
self.irc.channels[ch].users.discard(numeric)
|
||||
self.irc.users[numeric].channels.discard(ch)
|
||||
self._channels[ch].users.discard(numeric)
|
||||
self.users[numeric].channels.discard(ch)
|
||||
return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'}
|
||||
|
||||
else:
|
||||
for channel in args[0].split(','):
|
||||
# Normalize channel case.
|
||||
channel = self.irc.toLower(channel)
|
||||
|
||||
c = self.irc.channels[channel]
|
||||
|
||||
self.irc.users[numeric].channels.add(channel)
|
||||
self.irc.channels[channel].users.add(numeric)
|
||||
c = self._channels[channel]
|
||||
self.users[numeric].channels.add(channel)
|
||||
self._channels[channel].users.add(numeric)
|
||||
# Call hooks manually, because one JOIN command in UnrealIRCd can
|
||||
# have multiple channels...
|
||||
self.irc.callHooks([numeric, command, {'channel': channel, 'users': [numeric], 'modes':
|
||||
self.call_hooks([numeric, command, {'channel': channel, 'users': [numeric], 'modes':
|
||||
c.modes, 'ts': c.ts}])
|
||||
|
||||
def handle_sjoin(self, numeric, command, args):
|
||||
"""Handles the UnrealIRCd SJOIN command."""
|
||||
# <- :001 SJOIN 1444361345 #test :001AAAAAA @001AAAAAB +001AAAAAC
|
||||
# <- :001 SJOIN 1483250129 #services +nt :+001OR9V02 @*~001DH6901 &*!*@test "*!*@blah.blah '*!*@yes.no
|
||||
channel = self.irc.toLower(args[1])
|
||||
chandata = self.irc.channels[channel].deepcopy()
|
||||
channel = args[1]
|
||||
chandata = self._channels[channel].deepcopy()
|
||||
userlist = args[-1].split()
|
||||
|
||||
namelist = []
|
||||
log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel)
|
||||
log.debug('(%s) handle_sjoin: got userlist %r for %r', self.name, userlist, channel)
|
||||
|
||||
modestring = ''
|
||||
|
||||
@ -603,7 +577,7 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# Strip extra spaces between the mode argument and the user list, if
|
||||
# there are any. XXX: report this as a bug in unreal's s2s protocol?
|
||||
modestring = [m for m in modestring if m]
|
||||
parsedmodes = self.irc.parseModes(channel, modestring)
|
||||
parsedmodes = self.parse_modes(channel, modestring)
|
||||
changedmodes = set(parsedmodes)
|
||||
except IndexError:
|
||||
pass
|
||||
@ -628,28 +602,28 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# <- :002 SJOIN 1486361658 #idlerpg :@
|
||||
continue
|
||||
|
||||
user = self._getUid(user) # Normalize nicks to UIDs for Unreal 3.2 links
|
||||
user = self._get_UID(user) # Normalize nicks to UIDs for Unreal 3.2 links
|
||||
# Unreal uses slightly different prefixes in SJOIN. +q is * instead of ~,
|
||||
# and +a is ~ instead of &.
|
||||
modeprefix = (r.group(1) or '').replace("~", "&").replace("*", "~")
|
||||
finalprefix = ''
|
||||
|
||||
log.debug('(%s) handle_sjoin: got modeprefix %r for user %r', self.irc.name, modeprefix, user)
|
||||
log.debug('(%s) handle_sjoin: got modeprefix %r for user %r', self.name, modeprefix, user)
|
||||
for m in modeprefix:
|
||||
# Iterate over the mapping of prefix chars to prefixes, and
|
||||
# find the characters that match.
|
||||
for char, prefix in self.irc.prefixmodes.items():
|
||||
for char, prefix in self.prefixmodes.items():
|
||||
if m == prefix:
|
||||
finalprefix += char
|
||||
namelist.append(user)
|
||||
self.irc.users[user].channels.add(channel)
|
||||
self.users[user].channels.add(channel)
|
||||
|
||||
# Only merge the remote's prefix modes if their TS is smaller or equal to ours.
|
||||
changedmodes |= {('+%s' % mode, user) for mode in finalprefix}
|
||||
|
||||
self.irc.channels[channel].users.add(user)
|
||||
self._channels[channel].users.add(user)
|
||||
|
||||
our_ts = self.irc.channels[channel].ts
|
||||
our_ts = self._channels[channel].ts
|
||||
their_ts = int(args[0])
|
||||
self.updateTS(numeric, channel, their_ts, changedmodes)
|
||||
|
||||
@ -670,7 +644,7 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# <- NICK GL32 2 1470699865 gl localhost unreal32.midnight.vpn GL +iowx hidden-1C620195 AAAAAAAAAAAAAAAAAAAAAQ== :realname
|
||||
# to this:
|
||||
# <- :001 UID GL 0 1441306929 gl localhost 0018S7901 0 +iowx * hidden-1C620195 fwAAAQ== :realname
|
||||
log.debug('(%s) got legacy NICK args: %s', self.irc.name, ' '.join(args))
|
||||
log.debug('(%s) got legacy NICK args: %s', self.name, ' '.join(args))
|
||||
|
||||
new_args = args[:] # Clone the old args list
|
||||
servername = new_args[5].lower() # Get the name of the users' server.
|
||||
@ -686,7 +660,7 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# hosts from UnrealIRCd 3.2 users. Otherwise, +x host cloaking won't work!
|
||||
new_args.insert(-2, args[4])
|
||||
|
||||
log.debug('(%s) translating legacy NICK args to: %s', self.irc.name, ' '.join(new_args))
|
||||
log.debug('(%s) translating legacy NICK args to: %s', self.name, ' '.join(new_args))
|
||||
|
||||
return self.handle_uid(servername, 'UID_LEGACY', new_args)
|
||||
else:
|
||||
@ -708,12 +682,12 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# send 0 as a TS argument (which should be ignored unless breaking the internal channel TS is desired).
|
||||
|
||||
# Also, we need to get rid of that extra space following the +f argument. :|
|
||||
if utils.isChannel(args[0]):
|
||||
channel = self.irc.toLower(args[0])
|
||||
oldobj = self.irc.channels[channel].deepcopy()
|
||||
if self.is_channel(args[0]):
|
||||
channel = args[0]
|
||||
oldobj = self._channels[channel].deepcopy()
|
||||
|
||||
modes = [arg for arg in args[1:] if arg] # normalize whitespace
|
||||
parsedmodes = self.irc.parseModes(channel, modes)
|
||||
parsedmodes = self.parse_modes(channel, modes)
|
||||
|
||||
if parsedmodes:
|
||||
if parsedmodes[0][0] == '+&':
|
||||
@ -721,28 +695,29 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# attempt to set modes by us was rejected for some reason (usually due to
|
||||
# timestamps). Drop the mode change to prevent mode floods.
|
||||
log.debug("(%s) Received mode bounce %s in channel %s! Our TS: %s",
|
||||
self.irc.name, modes, channel, self.irc.channels[channel].ts)
|
||||
self.name, modes, channel, self._channels[channel].ts)
|
||||
return
|
||||
|
||||
self.irc.applyModes(channel, parsedmodes)
|
||||
self.apply_modes(channel, parsedmodes)
|
||||
|
||||
if numeric in self.irc.servers and args[-1].isdigit():
|
||||
if numeric in self.servers and args[-1].isdigit():
|
||||
# Sender is a server AND last arg is number. Perform TS updates.
|
||||
their_ts = int(args[-1])
|
||||
if their_ts > 0:
|
||||
self.updateTS(numeric, channel, their_ts)
|
||||
return {'target': channel, 'modes': parsedmodes, 'channeldata': oldobj}
|
||||
else:
|
||||
# User mode change: pass those on to handle_umode2()
|
||||
self.handle_umode2(numeric, 'MODE', args[1:])
|
||||
# User mode change
|
||||
target = self._get_UID(args[0])
|
||||
return self._handle_umode(target, self.parse_modes(target, args[1:]))
|
||||
|
||||
def checkCloakChange(self, uid, parsedmodes):
|
||||
def _check_cloak_change(self, uid, parsedmodes):
|
||||
"""
|
||||
Checks whether +x/-x was set in the mode query, and changes the
|
||||
hostname of the user given to or from their cloaked host if True.
|
||||
"""
|
||||
|
||||
userobj = self.irc.users[uid]
|
||||
userobj = self.users[uid]
|
||||
final_modes = userobj.modes
|
||||
oldhost = userobj.host
|
||||
|
||||
@ -767,32 +742,24 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
|
||||
if newhost != oldhost:
|
||||
# Only send a payload if the old and new hosts are different.
|
||||
self.irc.callHooks([uid, 'SETHOST',
|
||||
self.call_hooks([uid, 'SETHOST',
|
||||
{'target': uid, 'newhost': newhost}])
|
||||
|
||||
def handle_svsmode(self, numeric, command, args):
|
||||
"""Handles SVSMODE, used by services for setting user modes on others."""
|
||||
# <- :source SVSMODE target +usermodes
|
||||
target = self._getUid(args[0])
|
||||
modes = args[1:]
|
||||
target = self._get_UID(args[0])
|
||||
|
||||
parsedmodes = self.irc.parseModes(target, modes)
|
||||
self.irc.applyModes(target, parsedmodes)
|
||||
|
||||
# If +x/-x is being set, update cloaked host info.
|
||||
self.checkCloakChange(target, parsedmodes)
|
||||
|
||||
return {'target': target, 'modes': parsedmodes}
|
||||
return self._handle_umode(target, self.parse_modes(target, args[1:]))
|
||||
|
||||
def handle_svs2mode(self, sender, command, args):
|
||||
"""
|
||||
Handles SVS2MODE, which sets services login information on the given target.
|
||||
"""
|
||||
# Once again this syntax is inconsistent and poorly documented. +d sets a
|
||||
# "services stamp" that some services packages use as an account name field,
|
||||
# while others simply use for tracking the login time? In a nutshell: check
|
||||
# for the +d argument: if it's an integer, ignore it and set accountname to
|
||||
# the user's nick. Otherwise, treat the parameter as a nick.
|
||||
# In a nutshell: check for the +d argument: if it's an integer, ignore
|
||||
# it and set the user's account name to their nick. Otherwise, treat the
|
||||
# parameter as the new account name (this is known as logging in AS some account,
|
||||
# which is supported by atheme and Anope 2.x).
|
||||
|
||||
# Logging in (with account info, atheme):
|
||||
# <- :NickServ SVS2MODE GL +rd GL
|
||||
@ -821,8 +788,8 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# <- :NickServ SVS2MODE 001SALZ01 +d GL
|
||||
# <- :NickServ SVS2MODE 001SALZ01 +r
|
||||
|
||||
target = self._getUid(args[0])
|
||||
parsedmodes = self.irc.parseModes(target, args[1:])
|
||||
target = self._get_UID(args[0])
|
||||
parsedmodes = self.parse_modes(target, args[1:])
|
||||
|
||||
if ('+r', None) in parsedmodes:
|
||||
# Umode +r is being set (log in)
|
||||
@ -832,19 +799,19 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
except IndexError:
|
||||
# If one doesn't exist, make it the same as the nick, but only if the account name
|
||||
# wasn't set already.
|
||||
if not self.irc.users[target].services_account:
|
||||
account = self.irc.getFriendlyName(target)
|
||||
if not self.users[target].services_account:
|
||||
account = self.get_friendly_name(target)
|
||||
else:
|
||||
return
|
||||
else:
|
||||
if account.isdigit():
|
||||
# If the +d argument is a number, ignore it and set the account name to the nick.
|
||||
account = self.irc.getFriendlyName(target)
|
||||
account = self.get_friendly_name(target)
|
||||
|
||||
elif ('-r', None) in parsedmodes:
|
||||
# Umode -r being set.
|
||||
|
||||
if not self.irc.users[target].services_account:
|
||||
if not self.users[target].services_account:
|
||||
# User already has no account; ignore.
|
||||
return
|
||||
|
||||
@ -857,34 +824,41 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
else:
|
||||
return
|
||||
|
||||
self.irc.callHooks([target, 'CLIENT_SERVICES_LOGIN', {'text': account}])
|
||||
self.call_hooks([target, 'CLIENT_SERVICES_LOGIN', {'text': account}])
|
||||
# The internal mode +d used for services stamps clashes with the DEAF mode, so don't parse it as
|
||||
# an actual mode mode parsing.
|
||||
return self._handle_umode(target, [mode for mode in parsedmodes if mode[0][-1] != 'd'])
|
||||
|
||||
def handle_umode2(self, numeric, command, args):
|
||||
def _handle_umode(self, target, parsedmodes):
|
||||
"""Internal helper function to parse umode changes."""
|
||||
if not parsedmodes:
|
||||
return
|
||||
|
||||
self.apply_modes(target, parsedmodes)
|
||||
|
||||
self._check_oper_status_change(target, parsedmodes)
|
||||
self._check_cloak_change(target, parsedmodes)
|
||||
|
||||
return {'target': target, 'modes': parsedmodes}
|
||||
|
||||
def handle_umode2(self, source, command, args):
|
||||
"""Handles UMODE2, used to set user modes on oneself."""
|
||||
# <- :GL UMODE2 +W
|
||||
parsedmodes = self.irc.parseModes(numeric, args)
|
||||
self.irc.applyModes(numeric, parsedmodes)
|
||||
|
||||
if ('+o', None) in parsedmodes:
|
||||
# If +o being set, call the CLIENT_OPERED internal hook.
|
||||
self.irc.callHooks([numeric, 'CLIENT_OPERED', {'text': 'IRC Operator'}])
|
||||
|
||||
self.checkCloakChange(numeric, parsedmodes)
|
||||
|
||||
return {'target': numeric, 'modes': parsedmodes}
|
||||
target = self._get_UID(source)
|
||||
return self._handle_umode(target, self.parse_modes(target, args))
|
||||
|
||||
def handle_topic(self, numeric, command, args):
|
||||
"""Handles the TOPIC command."""
|
||||
# <- GL TOPIC #services GL 1444699395 :weeee
|
||||
# <- TOPIC #services devel.relay 1452399682 :test
|
||||
channel = self.irc.toLower(args[0])
|
||||
channel = args[0]
|
||||
topic = args[-1]
|
||||
setter = args[1]
|
||||
ts = args[2]
|
||||
|
||||
oldtopic = self.irc.channels[channel].topic
|
||||
self.irc.channels[channel].topic = topic
|
||||
self.irc.channels[channel].topicset = True
|
||||
oldtopic = self._channels[channel].topic
|
||||
self._channels[channel].topic = topic
|
||||
self._channels[channel].topicset = True
|
||||
|
||||
return {'channel': channel, 'setter': setter, 'ts': ts, 'text': topic,
|
||||
'oldtopic': oldtopic}
|
||||
@ -892,76 +866,57 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
def handle_setident(self, numeric, command, args):
|
||||
"""Handles SETIDENT, used for self ident changes."""
|
||||
# <- :70MAAAAAB SETIDENT test
|
||||
self.irc.users[numeric].ident = newident = args[0]
|
||||
self.users[numeric].ident = newident = args[0]
|
||||
return {'target': numeric, 'newident': newident}
|
||||
|
||||
def handle_sethost(self, numeric, command, args):
|
||||
"""Handles CHGHOST, used for self hostname changes."""
|
||||
# <- :70MAAAAAB SETIDENT some.host
|
||||
self.irc.users[numeric].host = newhost = args[0]
|
||||
self.users[numeric].host = newhost = args[0]
|
||||
|
||||
# When SETHOST or CHGHOST is used, modes +xt are implicitly set on the
|
||||
# target.
|
||||
self.irc.applyModes(numeric, [('+x', None), ('+t', None)])
|
||||
self.apply_modes(numeric, [('+x', None), ('+t', None)])
|
||||
|
||||
return {'target': numeric, 'newhost': newhost}
|
||||
|
||||
def handle_setname(self, numeric, command, args):
|
||||
"""Handles SETNAME, used for self real name/gecos changes."""
|
||||
# <- :70MAAAAAB SETNAME :afdsafasf
|
||||
self.irc.users[numeric].realname = newgecos = args[0]
|
||||
self.users[numeric].realname = newgecos = args[0]
|
||||
return {'target': numeric, 'newgecos': newgecos}
|
||||
|
||||
def handle_chgident(self, numeric, command, args):
|
||||
"""Handles CHGIDENT, used for denoting ident changes."""
|
||||
# <- :GL CHGIDENT GL test
|
||||
target = self._getUid(args[0])
|
||||
self.irc.users[target].ident = newident = args[1]
|
||||
target = self._get_UID(args[0])
|
||||
self.users[target].ident = newident = args[1]
|
||||
return {'target': target, 'newident': newident}
|
||||
|
||||
def handle_chghost(self, numeric, command, args):
|
||||
"""Handles CHGHOST, used for denoting hostname changes."""
|
||||
# <- :GL CHGHOST GL some.host
|
||||
target = self._getUid(args[0])
|
||||
self.irc.users[target].host = newhost = args[1]
|
||||
target = self._get_UID(args[0])
|
||||
self.users[target].host = newhost = args[1]
|
||||
|
||||
# When SETHOST or CHGHOST is used, modes +xt are implicitly set on the
|
||||
# target.
|
||||
self.irc.applyModes(target, [('+x', None), ('+t', None)])
|
||||
self.apply_modes(target, [('+x', None), ('+t', None)])
|
||||
|
||||
return {'target': target, 'newhost': newhost}
|
||||
|
||||
def handle_chgname(self, numeric, command, args):
|
||||
"""Handles CHGNAME, used for denoting real name/gecos changes."""
|
||||
# <- :GL CHGNAME GL :afdsafasf
|
||||
target = self._getUid(args[0])
|
||||
self.irc.users[target].realname = newgecos = args[1]
|
||||
target = self._get_UID(args[0])
|
||||
self.users[target].realname = newgecos = args[1]
|
||||
return {'target': target, 'newgecos': newgecos}
|
||||
|
||||
def handle_invite(self, numeric, command, args):
|
||||
"""Handles incoming INVITEs."""
|
||||
# <- :GL INVITE PyLink-devel :#a
|
||||
target = self._getUid(args[0])
|
||||
channel = self.irc.toLower(args[1])
|
||||
# We don't actually need to process this; it's just something plugins/hooks can use
|
||||
return {'target': target, 'channel': channel}
|
||||
|
||||
def handle_kill(self, numeric, command, args):
|
||||
"""Handles incoming KILLs."""
|
||||
# <- :GL| KILL GLolol :hidden-1C620195!GL| (test)
|
||||
# Use ts6_common's handle_kill, but coerse UIDs to nicks first.
|
||||
|
||||
new_args = [self._getUid(args[0])]
|
||||
new_args.extend(args[1:])
|
||||
|
||||
return super().handle_kill(numeric, command, new_args)
|
||||
|
||||
def handle_tsctl(self, source, command, args):
|
||||
"""Handles /TSCTL alltime requests."""
|
||||
# <- :GL TSCTL alltime
|
||||
|
||||
if args[0] == 'alltime':
|
||||
# XXX: We override notice() here because that abstraction doesn't allow messages from servers.
|
||||
self._send(self.irc.sid, 'NOTICE %s :*** Server=%s time()=%d' % (source, self.irc.hostname(), time.time()))
|
||||
self._send_with_prefix(self.sid, 'NOTICE %s :*** Server=%s time()=%d' % (source, self.hostname(), time.time()))
|
||||
|
||||
Class = UnrealProtocol
|
||||
|
51
selectdriver.py
Normal file
51
selectdriver.py
Normal file
@ -0,0 +1,51 @@
|
||||
"""
|
||||
Socket handling driver using the selectors module. epoll, kqueue, and devpoll
|
||||
are used internally when available.
|
||||
"""
|
||||
|
||||
import selectors
|
||||
import threading
|
||||
|
||||
from pylinkirc import world
|
||||
from pylinkirc.log import log
|
||||
|
||||
SELECT_TIMEOUT = 0.5
|
||||
|
||||
selector = selectors.DefaultSelector()
|
||||
|
||||
def _process_conns():
|
||||
"""Main loop which processes connected sockets."""
|
||||
|
||||
while not world.shutting_down.is_set():
|
||||
for socketkey, mask in selector.select(timeout=SELECT_TIMEOUT):
|
||||
irc = socketkey.data
|
||||
try:
|
||||
if mask & selectors.EVENT_READ and not irc._aborted.is_set():
|
||||
irc._run_irc()
|
||||
except:
|
||||
log.exception('Error in select driver loop:')
|
||||
continue
|
||||
|
||||
def register(irc):
|
||||
"""
|
||||
Registers a network to the global selectors instance.
|
||||
"""
|
||||
log.debug('selectdriver: registering %s for network %s', irc._socket, irc.name)
|
||||
selector.register(irc._socket, selectors.EVENT_READ, data=irc)
|
||||
|
||||
def unregister(irc):
|
||||
"""
|
||||
Removes a network from the global selectors instance.
|
||||
"""
|
||||
if irc._socket.fileno() != -1:
|
||||
log.debug('selectdriver: de-registering %s for network %s', irc._socket, irc.name)
|
||||
selector.unregister(irc._socket)
|
||||
else:
|
||||
log.debug('selectdriver: skipping de-registering %s for network %s', irc._socket, irc.name)
|
||||
|
||||
def start():
|
||||
"""
|
||||
Starts a thread to process connections.
|
||||
"""
|
||||
t = threading.Thread(target=_process_conns, name="Selector driver loop")
|
||||
t.start()
|
2
setup.py
2
setup.py
@ -44,7 +44,7 @@ setup(
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
|
||||
url='https://github.com/GLolol/PyLink',
|
||||
url='https://github.com/jlu5/PyLink',
|
||||
|
||||
# Author details
|
||||
author='James Lu',
|
||||
|
162
structures.py
162
structures.py
@ -5,14 +5,19 @@ This module contains custom data structures that may be useful in various situat
|
||||
"""
|
||||
|
||||
import collections
|
||||
import collections.abc
|
||||
import json
|
||||
import pickle
|
||||
import os
|
||||
import threading
|
||||
from copy import copy, deepcopy
|
||||
import string
|
||||
|
||||
from .log import log
|
||||
from . import conf
|
||||
|
||||
_BLACKLISTED_COPY_TYPES = []
|
||||
|
||||
class KeyedDefaultdict(collections.defaultdict):
|
||||
"""
|
||||
Subclass of defaultdict allowing the key to be passed to the default factory.
|
||||
@ -25,6 +30,161 @@ class KeyedDefaultdict(collections.defaultdict):
|
||||
value = self[key] = self.default_factory(key)
|
||||
return value
|
||||
|
||||
class CopyWrapper():
|
||||
"""
|
||||
Base container class implementing copy methods.
|
||||
"""
|
||||
|
||||
def copy(self):
|
||||
"""Returns a shallow copy of this object instance."""
|
||||
return copy(self)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
"""Returns a deep copy of the channel object."""
|
||||
newobj = copy(self)
|
||||
log.debug('CopyWrapper: _BLACKLISTED_COPY_TYPES = %s', _BLACKLISTED_COPY_TYPES)
|
||||
for attr, val in self.__dict__.items():
|
||||
# We can't pickle IRCNetwork, so just return a reference of it.
|
||||
if not isinstance(val, tuple(_BLACKLISTED_COPY_TYPES)):
|
||||
log.debug('CopyWrapper: copying attr %r', attr)
|
||||
setattr(newobj, attr, deepcopy(val))
|
||||
|
||||
memo[id(self)] = newobj
|
||||
|
||||
return newobj
|
||||
|
||||
def deepcopy(self):
|
||||
"""Returns a deep copy of this object instance."""
|
||||
return deepcopy(self)
|
||||
|
||||
class CaseInsensitiveFixedSet(collections.abc.Set, CopyWrapper):
|
||||
"""
|
||||
Implements a fixed set storing items case-insensitively.
|
||||
"""
|
||||
|
||||
def __init__(self, *, data=None):
|
||||
if data is not None:
|
||||
assert isinstance(data, set)
|
||||
self._data = data
|
||||
else:
|
||||
self._data = set()
|
||||
|
||||
@staticmethod
|
||||
def _keymangle(key):
|
||||
"""Converts the given key to lowercase."""
|
||||
if isinstance(key, str):
|
||||
return key.lower()
|
||||
return key
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__.__name__, self._data)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._data)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._data)
|
||||
|
||||
def __contains__(self, key):
|
||||
return self._data.__contains__(self._keymangle(key))
|
||||
|
||||
def __copy__(self):
|
||||
return self.__class__(data=self._data.copy())
|
||||
|
||||
class CaseInsensitiveDict(collections.abc.MutableMapping, CaseInsensitiveFixedSet):
|
||||
"""
|
||||
A dictionary storing items case insensitively.
|
||||
"""
|
||||
def __init__(self, *, data=None):
|
||||
if data is not None:
|
||||
assert isinstance(data, dict)
|
||||
self._data = data
|
||||
else:
|
||||
self._data = {}
|
||||
|
||||
def __getitem__(self, key):
|
||||
key = self._keymangle(key)
|
||||
|
||||
return self._data[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._data[self._keymangle(key)] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self._data[self._keymangle(key)]
|
||||
|
||||
class IRCCaseInsensitiveDict(CaseInsensitiveDict):
|
||||
"""
|
||||
A dictionary storing items case insensitively, using IRC case mappings.
|
||||
"""
|
||||
def __init__(self, irc, *, data=None):
|
||||
super().__init__(data=data)
|
||||
self._irc = irc
|
||||
|
||||
def _keymangle(self, key):
|
||||
"""Converts the given key to lowercase."""
|
||||
if isinstance(key, str):
|
||||
return self._irc.to_lower(key)
|
||||
return key
|
||||
|
||||
def __copy__(self):
|
||||
return self.__class__(self._irc, data=self._data.copy())
|
||||
|
||||
class CaseInsensitiveSet(collections.abc.MutableSet, CaseInsensitiveFixedSet):
|
||||
"""
|
||||
A mutable set storing items case insensitively.
|
||||
"""
|
||||
|
||||
def add(self, key):
|
||||
self._data.add(self._keymangle(key))
|
||||
|
||||
def discard(self, key):
|
||||
self._data.discard(self._keymangle(key))
|
||||
|
||||
class IRCCaseInsensitiveSet(CaseInsensitiveSet):
|
||||
"""
|
||||
A set storing items case insensitively, using IRC case mappings.
|
||||
"""
|
||||
def __init__(self, irc, *, data=None):
|
||||
super().__init__(data=data)
|
||||
self._irc = irc
|
||||
|
||||
def _keymangle(self, key):
|
||||
"""Converts the given key to lowercase."""
|
||||
if isinstance(key, str):
|
||||
return self._irc.to_lower(key)
|
||||
return key
|
||||
|
||||
def __copy__(self):
|
||||
return self.__class__(self._irc, data=self._data.copy())
|
||||
|
||||
class CamelCaseToSnakeCase():
|
||||
"""
|
||||
Class which automatically converts missing attributes from camel case to snake case.
|
||||
"""
|
||||
|
||||
def __getattr__(self, attr):
|
||||
"""
|
||||
Attribute fetching fallback function which normalizes camel case attributes to snake case.
|
||||
"""
|
||||
assert isinstance(attr, str), "Requested attribute %r is not a string!" % attr
|
||||
|
||||
normalized_attr = '' # Start off with the first letter, which is ignored when processing
|
||||
for char in attr:
|
||||
if char in string.ascii_uppercase:
|
||||
char = '_' + char.lower()
|
||||
normalized_attr += char
|
||||
|
||||
classname = self.__class__.__name__
|
||||
if normalized_attr == attr:
|
||||
# __getattr__ only fires if normal attribute fetching fails, so we can assume that
|
||||
# the attribute was tried already and failed.
|
||||
raise AttributeError('%s object has no attribute with normalized name %r' % (classname, attr))
|
||||
|
||||
target = getattr(self, normalized_attr)
|
||||
log.warning('%s.%s is deprecated, considering migrating to %s.%s!', classname, attr, classname, normalized_attr)
|
||||
return target
|
||||
|
||||
class DataStore:
|
||||
"""
|
||||
Generic database class. Plugins should use a subclass of this such as JSONDataStore or
|
||||
@ -38,7 +198,7 @@ class DataStore:
|
||||
log.debug('(DataStore:%s) using implementation %s', self.name, self.__class__.__name__)
|
||||
log.debug('(DataStore:%s) database path set to %s', self.name, self.filename)
|
||||
|
||||
self.save_frequency = save_frequency or conf.conf['bot'].get('save_delay', 300)
|
||||
self.save_frequency = save_frequency or conf.conf['pylink'].get('save_delay', 300)
|
||||
log.debug('(DataStore:%s) saving every %s seconds', self.name, self.save_frequency)
|
||||
|
||||
if default_db is not None:
|
||||
|
192
test/test_utils.py
Normal file
192
test/test_utils.py
Normal file
@ -0,0 +1,192 @@
|
||||
"""
|
||||
Test cases for utils.py
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from pylinkirc import utils
|
||||
|
||||
class UtilsTestCase(unittest.TestCase):
|
||||
|
||||
def test_strip_irc_formatting(self):
|
||||
# Some messages from http://modern.ircdocs.horse/formatting.html#examples
|
||||
self.assertEqual(utils.strip_irc_formatting(
|
||||
"I love \x033IRC! \x03It is the \x037best protocol ever!"),
|
||||
"I love IRC! It is the best protocol ever!")
|
||||
|
||||
self.assertEqual(utils.strip_irc_formatting(
|
||||
"This is a \x1d\x0313,9cool \x03message"),
|
||||
"This is a cool message")
|
||||
|
||||
self.assertEqual(utils.strip_irc_formatting(
|
||||
"Don't spam 5\x0313,8,6\x03,7,8, and especially not \x029\x02\x1d!"),
|
||||
"Don't spam 5,6,7,8, and especially not 9!")
|
||||
|
||||
# Should not remove the ,
|
||||
self.assertEqual(utils.strip_irc_formatting(
|
||||
"\x0305,"),
|
||||
",")
|
||||
self.assertEqual(utils.strip_irc_formatting(
|
||||
"\x038,,,,."),
|
||||
",,,,.")
|
||||
|
||||
# Numbers are preserved
|
||||
self.assertEqual(utils.strip_irc_formatting(
|
||||
"\x031234 "),
|
||||
"34 ")
|
||||
self.assertEqual(utils.strip_irc_formatting(
|
||||
"\x03\x1f12"),
|
||||
"12")
|
||||
|
||||
self.assertEqual(utils.strip_irc_formatting(
|
||||
"\x0305t\x030,1h\x0307,02e\x0308,06 \x0309,13q\x0303,15u\x0311,14i\x0310,05c\x0312,04k\x0302,07 \x0306,08b\x0313,09r\x0305,10o\x0304,12w\x0307,02n\x0308,06 \x0309,13f\x0303,15o\x0311,14x\x0310,05 \x0312,04j\x0302,07u\x0306,08m\x0313,09p\x0305,10s\x0304,12 \x0307,02o\x0308,06v\x0309,13e\x0303,15r\x0311,14 \x0310,05t\x0312,04h\x0302,07e\x0306,08 \x0313,09l\x0305,10a\x0304,12z\x0307,02y\x0308,06 \x0309,13d\x0303,15o\x0311,14g\x0f"),
|
||||
"the quick brown fox jumps over the lazy dog")
|
||||
|
||||
def test_remove_range(self):
|
||||
self.assertEqual(utils.remove_range(
|
||||
"1", [1,2,3,4,5,6,7,8,9]),
|
||||
[2,3,4,5,6,7,8,9])
|
||||
|
||||
self.assertEqual(utils.remove_range(
|
||||
"2,4", [1,2,3,4,5,6,7,8,9]),
|
||||
[1,3,5,6,7,8,9])
|
||||
|
||||
self.assertEqual(utils.remove_range(
|
||||
"1-4", [1,2,3,4,5,6,7,8,9]),
|
||||
[5,6,7,8,9])
|
||||
|
||||
self.assertEqual(utils.remove_range(
|
||||
"1-3,7", [1,2,3,4,5,6,7,8,9]),
|
||||
[4,5,6,8,9])
|
||||
|
||||
self.assertEqual(utils.remove_range(
|
||||
"1-3,5-9", [1,2,3,4,5,6,7,8,9]),
|
||||
[4])
|
||||
|
||||
self.assertEqual(utils.remove_range(
|
||||
"1-2,3-5,6-9", [1,2,3,4,5,6,7,8,9]),
|
||||
[])
|
||||
|
||||
# Anti-patterns, but should be legal
|
||||
self.assertEqual(utils.remove_range(
|
||||
"4,2", [1,2,3,4,5,6,7,8,9]),
|
||||
[1,3,5,6,7,8,9])
|
||||
self.assertEqual(utils.remove_range(
|
||||
"4,4,4", [1,2,3,4,5,6,7,8,9]),
|
||||
[1,2,3,5,6,7,8,9])
|
||||
|
||||
# Empty subranges should be filtered away
|
||||
self.assertEqual(utils.remove_range(
|
||||
",2,,4,", [1,2,3,4,5,6,7,8,9]),
|
||||
[1,3,5,6,7,8,9])
|
||||
|
||||
# Not enough items
|
||||
with self.assertRaises(IndexError):
|
||||
utils.remove_range(
|
||||
"5", ["abcd", "efgh"])
|
||||
with self.assertRaises(IndexError):
|
||||
utils.remove_range(
|
||||
"1-5", ["xyz", "cake"])
|
||||
|
||||
# Ranges going in reverse or invalid
|
||||
with self.assertRaises(ValueError):
|
||||
utils.remove_range(
|
||||
"5-2", [":)", ":D", "^_^"])
|
||||
utils.remove_range(
|
||||
"2-2", [":)", ":D", "^_^"])
|
||||
|
||||
# 0th element
|
||||
with self.assertRaises(ValueError):
|
||||
utils.remove_range(
|
||||
"5,0", list(range(50)))
|
||||
|
||||
# List can't contain None
|
||||
with self.assertRaises(ValueError):
|
||||
utils.remove_range(
|
||||
"1-2", [None, "", 0, False])
|
||||
|
||||
# Malformed indices
|
||||
with self.assertRaises(ValueError):
|
||||
utils.remove_range(
|
||||
" ", ["some", "clever", "string"])
|
||||
utils.remove_range(
|
||||
" ,,, ", ["some", "clever", "string"])
|
||||
utils.remove_range(
|
||||
"a,b,c,1,2,3", ["some", "clever", "string"])
|
||||
|
||||
# Malformed ranges
|
||||
with self.assertRaises(ValueError):
|
||||
utils.remove_range(
|
||||
"1,2-", [":)", ":D", "^_^"])
|
||||
utils.remove_range(
|
||||
"-", [":)", ":D", "^_^"])
|
||||
utils.remove_range(
|
||||
"1-2-3", [":)", ":D", "^_^"])
|
||||
utils.remove_range(
|
||||
"-1-2", [":)", ":D", "^_^"])
|
||||
utils.remove_range(
|
||||
"3--", [":)", ":D", "^_^"])
|
||||
utils.remove_range(
|
||||
"--5", [":)", ":D", "^_^"])
|
||||
utils.remove_range(
|
||||
"-3--5", ["we", "love", "emotes"])
|
||||
|
||||
def test_get_hostname_type(self):
|
||||
self.assertEqual(utils.get_hostname_type("1.2.3.4"), 1)
|
||||
self.assertEqual(utils.get_hostname_type("192.168.0.1"), 1)
|
||||
self.assertEqual(utils.get_hostname_type("127.0.0.5"), 1)
|
||||
|
||||
self.assertEqual(utils.get_hostname_type("0::1"), 2)
|
||||
self.assertEqual(utils.get_hostname_type("::1"), 2)
|
||||
self.assertEqual(utils.get_hostname_type("fc00::1234"), 2)
|
||||
self.assertEqual(utils.get_hostname_type("1111:2222:3333:4444:5555:6666:7777:8888"), 2)
|
||||
|
||||
self.assertEqual(utils.get_hostname_type("example.com"), 0)
|
||||
self.assertEqual(utils.get_hostname_type("abc.mynet.local"), 0)
|
||||
self.assertEqual(utils.get_hostname_type("123.example"), 0)
|
||||
|
||||
self.assertEqual(utils.get_hostname_type("123.456.789.000"), 0)
|
||||
self.assertEqual(utils.get_hostname_type("1::2::3"), 0)
|
||||
self.assertEqual(utils.get_hostname_type("1:"), 0)
|
||||
self.assertEqual(utils.get_hostname_type(":5"), 0)
|
||||
|
||||
def test_parse_duration(self):
|
||||
# Base case: simple number
|
||||
self.assertEqual(utils.parse_duration("0"), 0)
|
||||
self.assertEqual(utils.parse_duration("256"), 256)
|
||||
|
||||
# Not valid: not a positive integer
|
||||
with self.assertRaises(ValueError):
|
||||
utils.parse_duration("-5")
|
||||
utils.parse_duration("3.1416")
|
||||
|
||||
# Not valid: wrong units or nonsense
|
||||
with self.assertRaises(ValueError):
|
||||
utils.parse_duration("")
|
||||
utils.parse_duration("3j")
|
||||
utils.parse_duration("5h6") # stray number at end
|
||||
utils.parse_duration("5h3k")
|
||||
utils.parse_duration(" 6d ")
|
||||
utils.parse_duration("6.6d") # we don't support monster math
|
||||
utils.parse_duration("zzzzzdstwataw")
|
||||
utils.parse_duration("3asdfjkl;")
|
||||
|
||||
# Test all supported units
|
||||
self.assertEqual(utils.parse_duration("3s"), 3)
|
||||
self.assertEqual(utils.parse_duration("1m"), 60)
|
||||
self.assertEqual(utils.parse_duration("9h"), 9 * 60 * 60)
|
||||
self.assertEqual(utils.parse_duration("15d"), 15 * 24 * 60 * 60)
|
||||
self.assertEqual(utils.parse_duration("3w"), 3 * 7 * 24 * 60 * 60)
|
||||
|
||||
# Composites
|
||||
self.assertEqual(utils.parse_duration("6m10s"), 6 * 60 + 10)
|
||||
self.assertEqual(utils.parse_duration("1d5h"), ((24+5) * 60 * 60))
|
||||
self.assertEqual(utils.parse_duration("2d3m4s"), (48 * 60 * 60 + 3 * 60 + 4))
|
||||
|
||||
# Not valid: wrong order of units
|
||||
with self.assertRaises(ValueError):
|
||||
utils.parse_duration("4s3d")
|
||||
utils.parse_duration("1m5w")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
627
utils.py
627
utils.py
@ -11,9 +11,10 @@ import importlib
|
||||
import os
|
||||
import collections
|
||||
import argparse
|
||||
import ipaddress
|
||||
|
||||
from .log import log
|
||||
from . import world, conf
|
||||
from . import world, conf, structures
|
||||
|
||||
# Load the protocol and plugin packages.
|
||||
from pylinkirc import protocols, plugins
|
||||
@ -33,186 +34,76 @@ class InvalidArgumentsError(TypeError):
|
||||
Exception raised (by IRCParser and potentially others) when a bot command is given invalid arguments.
|
||||
"""
|
||||
|
||||
class IncrementalUIDGenerator():
|
||||
class ProtocolError(RuntimeError):
|
||||
"""
|
||||
Incremental UID Generator module, adapted from InspIRCd source:
|
||||
https://github.com/inspircd/inspircd/blob/f449c6b296ab/src/server.cpp#L85-L156
|
||||
Exception raised when a network protocol violation is encountered in some way.
|
||||
"""
|
||||
|
||||
def __init__(self, sid):
|
||||
if not (hasattr(self, 'allowedchars') and hasattr(self, 'length')):
|
||||
raise RuntimeError("Allowed characters list not defined. Subclass "
|
||||
"%s by defining self.allowedchars and self.length "
|
||||
"and then calling super().__init__()." % self.__class__.__name__)
|
||||
self.uidchars = [self.allowedchars[0]]*self.length
|
||||
self.sid = str(sid)
|
||||
|
||||
def increment(self, pos=None):
|
||||
"""
|
||||
Increments the UID generator to the next available UID.
|
||||
"""
|
||||
# Position starts at 1 less than the UID length.
|
||||
if pos is None:
|
||||
pos = self.length - 1
|
||||
|
||||
# If we're at the last character in the list of allowed ones, reset
|
||||
# and increment the next level above.
|
||||
if self.uidchars[pos] == self.allowedchars[-1]:
|
||||
self.uidchars[pos] = self.allowedchars[0]
|
||||
self.increment(pos-1)
|
||||
else:
|
||||
# Find what position in the allowed characters list we're currently
|
||||
# on, and add one.
|
||||
idx = self.allowedchars.find(self.uidchars[pos])
|
||||
self.uidchars[pos] = self.allowedchars[idx+1]
|
||||
|
||||
def next_uid(self):
|
||||
"""
|
||||
Returns the next unused UID for the server.
|
||||
"""
|
||||
uid = self.sid + ''.join(self.uidchars)
|
||||
self.increment()
|
||||
return uid
|
||||
|
||||
class PUIDGenerator():
|
||||
"""
|
||||
Pseudo UID Generator module, using a prefix and a simple counter.
|
||||
"""
|
||||
|
||||
def __init__(self, prefix):
|
||||
self.prefix = prefix
|
||||
self.counter = 0
|
||||
|
||||
def next_uid(self, prefix=''):
|
||||
"""
|
||||
Generates the next PUID.
|
||||
"""
|
||||
uid = '%s@%s' % (prefix or self.prefix, self.counter)
|
||||
self.counter += 1
|
||||
return uid
|
||||
next_sid = next_uid
|
||||
|
||||
def add_cmd(func, name=None, **kwargs):
|
||||
"""Binds an IRC command function to the given command name."""
|
||||
world.services['pylink'].add_cmd(func, name=name, **kwargs)
|
||||
return func
|
||||
|
||||
def add_hook(func, command):
|
||||
"""Binds a hook function to the given command name."""
|
||||
def add_hook(func, command, priority=100):
|
||||
"""
|
||||
Binds a hook function to the given command name.
|
||||
|
||||
A custom priority can also be given (defaults to 100), and hooks with
|
||||
higher priority values will be called first."""
|
||||
command = command.upper()
|
||||
world.hooks[command].append(func)
|
||||
world.hooks[command].append((priority, func))
|
||||
world.hooks[command].sort(key=lambda pair: pair[0], reverse=True)
|
||||
return func
|
||||
|
||||
_nickregex = r'^[A-Za-z\|\\_\[\]\{\}\^\`][A-Z0-9a-z\-\|\\_\[\]\{\}\^\`]*$'
|
||||
def isNick(s, nicklen=None):
|
||||
"""Returns whether the string given is a valid nick."""
|
||||
if nicklen and len(s) > nicklen:
|
||||
return False
|
||||
return bool(re.match(_nickregex, s))
|
||||
|
||||
def isChannel(s):
|
||||
"""Returns whether the string given is a valid channel name."""
|
||||
return str(s).startswith('#')
|
||||
|
||||
def _isASCII(s):
|
||||
"""Returns whether the string given is valid ASCII."""
|
||||
chars = string.ascii_letters + string.digits + string.punctuation
|
||||
return all(char in chars for char in s)
|
||||
|
||||
def isServerName(s):
|
||||
"""Returns whether the string given is a valid IRC server name."""
|
||||
return _isASCII(s) and '.' in s and not s.startswith('.')
|
||||
|
||||
hostmaskRe = re.compile(r'^\S+!\S+@\S+$')
|
||||
def isHostmask(text):
|
||||
"""Returns whether the given text is a valid hostmask."""
|
||||
# Band-aid patch here to prevent bad bans set by Janus forwarding people into invalid channels.
|
||||
return hostmaskRe.match(text) and '#' not in text
|
||||
|
||||
def parseModes(irc, target, args):
|
||||
"""Parses a modestring list into a list of (mode, argument) tuples.
|
||||
['+mitl-o', '3', 'person'] => [('+m', None), ('+i', None), ('+t', None), ('+l', '3'), ('-o', 'person')]
|
||||
|
||||
This method is deprecated. Use irc.parseModes() instead.
|
||||
"""
|
||||
log.warning("(%s) utils.parseModes is deprecated. Use irc.parseModes() instead!", irc.name)
|
||||
return irc.parseModes(target, args)
|
||||
|
||||
def applyModes(irc, target, changedmodes):
|
||||
"""Takes a list of parsed IRC modes, and applies them on the given target.
|
||||
|
||||
The target can be either a channel or a user; this is handled automatically.
|
||||
|
||||
This method is deprecated. Use irc.applyModes() instead.
|
||||
"""
|
||||
log.warning("(%s) utils.applyModes is deprecated. Use irc.applyModes() instead!", irc.name)
|
||||
return irc.applyModes(target, changedmodes)
|
||||
|
||||
def expandpath(path):
|
||||
def expand_path(path):
|
||||
"""
|
||||
Returns a path expanded with environment variables and home folders (~) expanded, in that order."""
|
||||
return os.path.expanduser(os.path.expandvars(path))
|
||||
expandpath = expand_path # Consistency with os.path
|
||||
|
||||
def resetModuleDirs():
|
||||
def _reset_module_dirs():
|
||||
"""
|
||||
(Re)sets custom protocol module and plugin directories to the ones specified in the config.
|
||||
"""
|
||||
# Note: This assumes that the first element of the package path is the default one.
|
||||
plugins.__path__ = [plugins.__path__[0]] + [expandpath(path) for path in conf.conf['bot'].get('plugin_dirs', [])]
|
||||
log.debug('resetModuleDirs: new pylinkirc.plugins.__path__: %s', plugins.__path__)
|
||||
protocols.__path__ = [protocols.__path__[0]] + [expandpath(path) for path in conf.conf['bot'].get('protocol_dirs', [])]
|
||||
log.debug('resetModuleDirs: new pylinkirc.protocols.__path__: %s', protocols.__path__)
|
||||
plugins.__path__ = [plugins.__path__[0]] + [expandpath(path) for path in conf.conf['pylink'].get('plugin_dirs', [])]
|
||||
log.debug('_reset_module_dirs: new pylinkirc.plugins.__path__: %s', plugins.__path__)
|
||||
protocols.__path__ = [protocols.__path__[0]] + [expandpath(path) for path in conf.conf['pylink'].get('protocol_dirs', [])]
|
||||
log.debug('_reset_module_dirs: new pylinkirc.protocols.__path__: %s', protocols.__path__)
|
||||
resetModuleDirs = _reset_module_dirs
|
||||
|
||||
def loadPlugin(name):
|
||||
def _load_plugin(name):
|
||||
"""
|
||||
Imports and returns the requested plugin.
|
||||
"""
|
||||
return importlib.import_module(PLUGIN_PREFIX + name)
|
||||
loadPlugin = _load_plugin
|
||||
|
||||
def getProtocolModule(name):
|
||||
def _get_protocol_module(name):
|
||||
"""
|
||||
Imports and returns the protocol module requested.
|
||||
"""
|
||||
return importlib.import_module(PROTOCOL_PREFIX + name)
|
||||
getProtocolModule = _get_protocol_module
|
||||
|
||||
def getDatabaseName(dbname):
|
||||
"""
|
||||
Returns a database filename with the given base DB name appropriate for the
|
||||
current PyLink instance.
|
||||
|
||||
This returns '<dbname>.db' if the running config name is PyLink's default
|
||||
(pylink.yml), and '<dbname>-<config name>.db' for anything else. For example,
|
||||
if this is called from an instance running as './pylink testing.yml', it
|
||||
would return '<dbname>-testing.db'."""
|
||||
if conf.confname != 'pylink':
|
||||
dbname += '-%s' % conf.confname
|
||||
dbname += '.db'
|
||||
return dbname
|
||||
|
||||
def splitHostmask(mask):
|
||||
def split_hostmask(mask):
|
||||
"""
|
||||
Returns a nick!user@host hostmask split into three fields: nick, user, and host.
|
||||
"""
|
||||
nick, identhost = mask.split('!', 1)
|
||||
ident, host = identhost.split('@', 1)
|
||||
return [nick, ident, host]
|
||||
splitHostmask = split_hostmask
|
||||
|
||||
class ServiceBot():
|
||||
"""
|
||||
PyLink IRC Service class.
|
||||
"""
|
||||
|
||||
def __init__(self, name, default_help=True, default_list=True,
|
||||
nick=None, ident=None, manipulatable=False, desc=None):
|
||||
# Service name
|
||||
def __init__(self, name, default_help=True, default_list=True, manipulatable=False, default_nick=None, desc=None):
|
||||
# Service name and default nick
|
||||
self.name = name
|
||||
|
||||
# TODO: validate nick, ident, etc. on runtime as well
|
||||
assert isNick(name), "Invalid service name %r" % name
|
||||
|
||||
# Nick/ident to take. Defaults to the same as the service name if not given.
|
||||
self.nick = nick
|
||||
self.ident = ident
|
||||
self.default_nick = default_nick
|
||||
|
||||
# Tracks whether the bot should be manipulatable by the 'bots' plugin and other commands.
|
||||
self.manipulatable = manipulatable
|
||||
@ -226,9 +117,11 @@ class ServiceBot():
|
||||
# spawned.
|
||||
self.uids = {}
|
||||
|
||||
# Track what channels other than those defined in the config
|
||||
# that the bot should join by default.
|
||||
self.extra_channels = collections.defaultdict(set)
|
||||
# Track plugin-defined persistent channels. The bot will leave them if they're empty,
|
||||
# and rejoin whenever someone else does.
|
||||
# This is stored as a nested dictionary:
|
||||
# {"plugin1": {"net1": IRCCaseInsensitiveSet({"#a", "#b"}), "net2": ...}, ...}
|
||||
self.dynamic_channels = {}
|
||||
|
||||
# Service description, used in the default help command if one is given.
|
||||
self.desc = desc
|
||||
@ -236,6 +129,9 @@ class ServiceBot():
|
||||
# List of command names to "feature"
|
||||
self.featured_cmds = set()
|
||||
|
||||
# Maps command aliases to the respective primary commands
|
||||
self.alias_cmds = {}
|
||||
|
||||
if default_help:
|
||||
self.add_cmd(self.help)
|
||||
|
||||
@ -250,60 +146,90 @@ class ServiceBot():
|
||||
# which is handled by coreplugin.
|
||||
if irc is None:
|
||||
for irc in world.networkobjects.values():
|
||||
irc.callHooks([None, 'PYLINK_NEW_SERVICE', {'name': self.name}])
|
||||
irc.call_hooks([None, 'PYLINK_NEW_SERVICE', {'name': self.name}])
|
||||
else:
|
||||
raise NotImplementedError("Network specific plugins not supported yet.")
|
||||
|
||||
def join(self, irc, channels, autojoin=True):
|
||||
def join(self, irc, channels, ignore_empty=True):
|
||||
"""
|
||||
Joins the given service bot to the given channel(s).
|
||||
Joins the given service bot to the given channel(s). "channels" can be
|
||||
an iterable of channel names or the name of a single channel (type 'str').
|
||||
|
||||
The ignore_empty option sets whether we should skip joining empty
|
||||
channels and join them later when we see someone else join (for channels
|
||||
marked persistent). This option is automatically *disabled* on networks
|
||||
where we cannot monitor channels that we're not in (e.g. Clientbot).
|
||||
|
||||
Before PyLink 2.0-alpha3, this function implicitly marks channels i
|
||||
receives to be persistent - this is no longer the case!
|
||||
"""
|
||||
uid = self.uids.get(irc.name)
|
||||
if uid is None:
|
||||
return
|
||||
|
||||
if type(irc) == str:
|
||||
netname = irc
|
||||
else:
|
||||
netname = irc.name
|
||||
|
||||
# Ensure type safety: pluralize strings if only one channel was given, then convert to set.
|
||||
if type(channels) == str:
|
||||
if isinstance(channels, str):
|
||||
channels = [channels]
|
||||
channels = set(channels)
|
||||
|
||||
if autojoin:
|
||||
log.debug('(%s/%s) Adding channels %s to autojoin', netname, self.name, channels)
|
||||
self.extra_channels[netname] |= channels
|
||||
|
||||
# If the network was given as a string, look up the Irc object here.
|
||||
try:
|
||||
irc = world.networkobjects[netname]
|
||||
except KeyError:
|
||||
log.debug('(%s/%s) Skipping join(), IRC object not initialized yet', netname, self.name)
|
||||
return
|
||||
|
||||
try:
|
||||
u = self.uids[irc.name]
|
||||
except KeyError:
|
||||
log.debug('(%s/%s) Skipping join(), UID not initialized yet', irc.name, self.name)
|
||||
return
|
||||
if irc.has_cap('visible-state-only'):
|
||||
# Disable dynamic channel joining on networks where we can't monitor channels for joins.
|
||||
ignore_empty = False
|
||||
|
||||
# Specify modes to join the services bot with.
|
||||
joinmodes = irc.serverdata.get("%s_joinmodes" % self.name) or conf.conf.get(self.name, {}).get('joinmodes') or ''
|
||||
joinmodes = irc.get_service_option(self.name, 'joinmodes', default='')
|
||||
joinmodes = ''.join([m for m in joinmodes if m in irc.prefixmodes])
|
||||
|
||||
for chan in channels:
|
||||
if isChannel(chan):
|
||||
if u in irc.channels[chan].users:
|
||||
log.debug('(%s) Skipping join of services %s to channel %s - it is already present', irc.name, self.name, chan)
|
||||
for channel in channels:
|
||||
if irc.is_channel(channel):
|
||||
if channel in irc.channels:
|
||||
if uid in irc.channels[channel].users:
|
||||
log.debug('(%s/%s) Skipping join to %r - we are already present', irc.name, self.name, channel)
|
||||
continue
|
||||
elif ignore_empty:
|
||||
log.debug('(%s/%s) Skipping joining empty channel %r', irc.name, self.name, channel)
|
||||
continue
|
||||
log.debug('(%s) Joining services %s to channel %s with modes %r', irc.name, self.name, chan, joinmodes)
|
||||
if joinmodes: # Modes on join were specified; use SJOIN to burst our service
|
||||
irc.proto.sjoin(irc.sid, chan, [(joinmodes, u)])
|
||||
else:
|
||||
irc.proto.join(u, chan)
|
||||
|
||||
irc.callHooks([irc.sid, 'PYLINK_SERVICE_JOIN', {'channel': chan, 'users': [u]}])
|
||||
log.debug('(%s/%s) Joining channel %s with modes %r', irc.name, self.name, channel, joinmodes)
|
||||
|
||||
if joinmodes: # Modes on join were specified; use SJOIN to burst our service
|
||||
irc.sjoin(irc.sid, channel, [(joinmodes, uid)])
|
||||
else:
|
||||
irc.join(uid, channel)
|
||||
|
||||
irc.call_hooks([irc.sid, 'PYLINK_SERVICE_JOIN', {'channel': channel, 'users': [uid]}])
|
||||
else:
|
||||
log.warning('(%s) Ignoring invalid autojoin channel %r.', irc.name, chan)
|
||||
log.warning('(%s/%s) Ignoring invalid channel %r', irc.name, self.name, channel)
|
||||
|
||||
def part(self, irc, channels, reason=''):
|
||||
"""
|
||||
Parts the given service bot from the given channel(s) if no plugins
|
||||
still register it as a persistent dynamic channel.
|
||||
|
||||
"channels" can be an iterable of channel names or the name of a single
|
||||
channel (type 'str').
|
||||
"""
|
||||
uid = self.uids.get(irc.name)
|
||||
if uid is None:
|
||||
return
|
||||
|
||||
if isinstance(channels, str):
|
||||
channels = [channels]
|
||||
|
||||
to_part = []
|
||||
persistent_channels = self.get_persistent_channels(irc)
|
||||
for channel in channels:
|
||||
if channel in irc.channels and uid in irc.channels[channel].users:
|
||||
if channel in persistent_channels:
|
||||
log.debug('(%s/%s) Not parting %r because it is registered '
|
||||
'as a dynamic channel: %r', irc.name, self.name, channel,
|
||||
persistent_channels)
|
||||
continue
|
||||
to_part.append(channel)
|
||||
irc.part(uid, channel, reason)
|
||||
else:
|
||||
log.debug('(%s/%s) Ignoring part to %r, we are not there', irc.name, self.name, channel)
|
||||
continue
|
||||
|
||||
irc.call_hooks([uid, 'PYLINK_SERVICE_PART', {'channels': to_part, 'text': reason}])
|
||||
|
||||
def reply(self, irc, text, notice=None, private=None):
|
||||
"""Replies to a message as the service in question."""
|
||||
@ -343,24 +269,24 @@ class ServiceBot():
|
||||
if cmd and show_unknown_cmds and not cmd.startswith('\x01'):
|
||||
# Ignore empty commands and invalid command errors from CTCPs.
|
||||
self.reply(irc, 'Error: Unknown command %r.' % cmd)
|
||||
log.info('(%s/%s) Received unknown command %r from %s', irc.name, self.name, cmd, irc.getHostmask(source))
|
||||
log.info('(%s/%s) Received unknown command %r from %s', irc.name, self.name, cmd, irc.get_hostmask(source))
|
||||
return
|
||||
|
||||
log.info('(%s/%s) Calling command %r for %s', irc.name, self.name, cmd, irc.getHostmask(source))
|
||||
log.info('(%s/%s) Calling command %r for %s', irc.name, self.name, cmd, irc.get_hostmask(source))
|
||||
for func in self.commands[cmd]:
|
||||
try:
|
||||
func(irc, source, cmd_args)
|
||||
except NotAuthorizedError as e:
|
||||
self.reply(irc, 'Error: %s' % e)
|
||||
log.warning('(%s) Denying access to command %r for %s; msg: %s', irc.name, cmd,
|
||||
irc.getHostmask(source), e)
|
||||
irc.get_hostmask(source), e)
|
||||
except InvalidArgumentsError as e:
|
||||
self.reply(irc, 'Error: %s' % e)
|
||||
except Exception as e:
|
||||
log.exception('Unhandled exception caught in command %r', cmd)
|
||||
self.reply(irc, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e)))
|
||||
|
||||
def add_cmd(self, func, name=None, featured=False):
|
||||
def add_cmd(self, func, name=None, featured=False, aliases=None):
|
||||
"""Binds an IRC command function to the given command name."""
|
||||
if name is None:
|
||||
name = func.__name__
|
||||
@ -370,9 +296,168 @@ class ServiceBot():
|
||||
if featured:
|
||||
self.featured_cmds.add(name)
|
||||
|
||||
# If this is an alias, store the primary command in the alias_cmds dict
|
||||
if aliases is not None:
|
||||
for alias in aliases:
|
||||
if name == alias:
|
||||
log.error('Refusing to alias command %r (in plugin %r) to itself!', name, func.__module__)
|
||||
continue
|
||||
|
||||
self.add_cmd(func, name=alias) # Bind the alias as well.
|
||||
self.alias_cmds[alias] = name
|
||||
|
||||
self.commands[name].append(func)
|
||||
return func
|
||||
|
||||
def get_nick(self, irc, fails=0):
|
||||
"""
|
||||
If the 'fails' argument is set to zero, this method returns the preferred nick for this
|
||||
service bot on the given network. The following fields are checked in order:
|
||||
# 1) Network specific nick settings for this service (servers:<netname>:servicename_nick)
|
||||
# 2) Global settings for this service (servicename:nick)
|
||||
# 3) The service's hardcoded default nick.
|
||||
# 4) The literal service name.
|
||||
|
||||
If the 'fails' argument is set to a non-zero value, a list of *alternate* (fallback) nicks
|
||||
will be fetched from these fields in this order:
|
||||
# 1) Network specific altnick settings for this service (servers:<netname>:servicename_altnicks)
|
||||
# 2) Global altnick settings for this service (servicename:altnicks)
|
||||
|
||||
If such an alternate nicks list exists, an alternate nick will be chosen based on the value
|
||||
of the 'fails' argument:
|
||||
- If nick fetching fails once, return the 1st alternate nick from the list,
|
||||
- If nick fetching fails twice, return the 2nd alternate nick from the list, ...
|
||||
|
||||
Otherwise, if the alternate nicks list doesn't exist, or if there is no corresponding value
|
||||
for the current 'fails' value, the preferred nick plus the 'fails' number of underscores (_)
|
||||
will be used instead.
|
||||
- fails=1 => preferred_nick_
|
||||
- fails=2 => preferred_nick__
|
||||
|
||||
If the resulting nick is too long for the given network, ProtocolError will be raised.
|
||||
"""
|
||||
sbconf = conf.conf.get(self.name, {})
|
||||
nick = irc.serverdata.get("%s_nick" % self.name) or sbconf.get('nick') or self.default_nick or self.name
|
||||
|
||||
if fails >= 1:
|
||||
altnicks = irc.serverdata.get("%s_altnicks" % self.name) or sbconf.get('altnicks') or []
|
||||
try:
|
||||
nick = altnicks[fails-1]
|
||||
except IndexError:
|
||||
nick += ('_' * fails)
|
||||
|
||||
if irc.maxnicklen > 0 and len(nick) > irc.maxnicklen:
|
||||
raise ProtocolError("Nick %r too long for network (maxnicklen=%s)" % (nick, irc.maxnicklen))
|
||||
|
||||
assert nick
|
||||
return nick
|
||||
|
||||
def get_ident(self, irc):
|
||||
"""
|
||||
Returns the preferred ident for this service bot on the given network. The following fields are checked in order:
|
||||
# 1) Network specific ident settings for this service (servers:<netname>:servicename_ident)
|
||||
# 2) Global settings for this service (servicename:ident)
|
||||
# 3) The service's hardcoded default nick.
|
||||
# 4) The literal service name.
|
||||
"""
|
||||
sbconf = conf.conf.get(self.name, {})
|
||||
return irc.serverdata.get("%s_ident" % self.name) or sbconf.get('ident') or self.default_nick or self.name
|
||||
|
||||
def get_host(self, irc):
|
||||
"""
|
||||
Returns the preferred hostname for this service bot on the given network. The following fields are checked in order:
|
||||
# 1) Network specific hostname settings for this service (servers:<netname>:servicename_host)
|
||||
# 2) Global settings for this service (servicename:host)
|
||||
# 3) The PyLink server hostname.
|
||||
"""
|
||||
sbconf = conf.conf.get(self.name, {})
|
||||
return irc.serverdata.get("%s_host" % self.name) or sbconf.get('host') or irc.hostname()
|
||||
|
||||
def get_realname(self, irc):
|
||||
"""
|
||||
Returns the preferred real name for this service bot on the given network. The following fields are checked in order:
|
||||
# 1) Network specific realname settings for this service (servers:<netname>:servicename_realname)
|
||||
# 2) Global settings for this service (servicename:realname)
|
||||
# 3) The globally configured real name (pylink:realname).
|
||||
# 4) The literal service name.
|
||||
"""
|
||||
sbconf = conf.conf.get(self.name, {})
|
||||
return irc.serverdata.get("%s_realname" % self.name) or sbconf.get('realname') or conf.conf['pylink'].get('realname') or self.name
|
||||
|
||||
def add_persistent_channel(self, irc, namespace, channel, try_join=True):
|
||||
"""
|
||||
Adds a persistent channel to the service bot on the given network and namespace.
|
||||
"""
|
||||
namespace = self.dynamic_channels.setdefault(namespace, {})
|
||||
chanlist = namespace.setdefault(irc.name, structures.IRCCaseInsensitiveSet(irc))
|
||||
chanlist.add(channel)
|
||||
|
||||
if try_join:
|
||||
self.join(irc, [channel])
|
||||
|
||||
def remove_persistent_channel(self, irc, namespace, channel, try_part=True, part_reason=''):
|
||||
"""
|
||||
Removes a persistent channel from the service bot on the given network and namespace.
|
||||
"""
|
||||
chanlist = self.dynamic_channels[namespace][irc.name].remove(channel)
|
||||
|
||||
if try_part and irc.connected.is_set():
|
||||
self.part(irc, [channel], reason=part_reason)
|
||||
|
||||
def get_persistent_channels(self, irc, namespace=None):
|
||||
"""
|
||||
Returns a set of persistent channels for the IRC network, optionally filtering
|
||||
by namespace is one is given.
|
||||
"""
|
||||
channels = structures.IRCCaseInsensitiveSet(irc)
|
||||
if namespace:
|
||||
chanlist = self.dynamic_channels.get(namespace, {}).get(irc.name, set())
|
||||
log.debug('(%s/%s) get_persistent_channels: adding channels '
|
||||
'%r from namespace %r (single)', irc.name, self.name,
|
||||
chanlist, namespace)
|
||||
channels |= chanlist
|
||||
else:
|
||||
for dch_namespace, dch_data in self.dynamic_channels.items():
|
||||
chanlist = dch_data.get(irc.name, set())
|
||||
log.debug('(%s/%s) get_persistent_channels: adding channels '
|
||||
'%r from namespace %r', irc.name, self.name,
|
||||
chanlist, dch_namespace)
|
||||
channels |= chanlist
|
||||
channels |= set(irc.serverdata.get(self.name+'_channels', []))
|
||||
channels |= set(irc.serverdata.get('channels', []))
|
||||
return channels
|
||||
|
||||
def clear_persistent_channels(self, irc, namespace, try_part=True, part_reason=''):
|
||||
"""
|
||||
Clears the persistent channels defined by a namespace.
|
||||
|
||||
irc can be None to clear persistent channels for all networks in this namespace.
|
||||
"""
|
||||
dch_data = self.dynamic_channels.get(namespace, {})
|
||||
|
||||
if irc is not None:
|
||||
if irc.name in dch_data:
|
||||
chanlist = dch_data[irc.name]
|
||||
log.debug('(%s/%s) Clearing persistent channels %r from namespace %r',
|
||||
irc.name, self.name, chanlist, namespace)
|
||||
|
||||
del dch_data[irc.name]
|
||||
|
||||
if try_part:
|
||||
self.part(irc, chanlist, reason=part_reason)
|
||||
|
||||
|
||||
else: # when irc is None
|
||||
del self.dynamic_channels[namespace]
|
||||
|
||||
for netname, chanlist in dch_data.items():
|
||||
log.debug('(%s/%s) Globally clearing persistent channels %r from namespace %r',
|
||||
netname, self.name, chanlist, namespace)
|
||||
if try_part and netname in world.networkobjects:
|
||||
self.part(world.networkobjects[netname], chanlist,
|
||||
reason=part_reason)
|
||||
|
||||
|
||||
def _show_command_help(self, irc, command, private=False, shortform=False):
|
||||
"""
|
||||
Shows help for the given command.
|
||||
@ -394,6 +479,7 @@ class ServiceBot():
|
||||
if command not in self.commands:
|
||||
_reply('Error: Unknown command %r.' % command)
|
||||
return
|
||||
|
||||
else:
|
||||
funcs = self.commands[command]
|
||||
if len(funcs) > 1:
|
||||
@ -411,12 +497,12 @@ class ServiceBot():
|
||||
_reply(args_desc.strip())
|
||||
if not shortform:
|
||||
# Note: we handle newlines in docstrings a bit differently. Per
|
||||
# https://github.com/GLolol/PyLink/issues/307, only double newlines (and
|
||||
# https://github.com/jlu5/PyLink/issues/307, only double newlines (and
|
||||
# combinations of more) have the effect of showing a new line on IRC.
|
||||
# Single newlines are stripped so that word wrap can be applied in source
|
||||
# code without affecting the output on IRC.
|
||||
# TODO: we should probably verify that the output line doesn't exceed IRC
|
||||
# line length limits...
|
||||
# (On the same topic, real line wrapping on IRC is done in irc.msg() as of
|
||||
# 2.0-beta1)
|
||||
next_line = ''
|
||||
for linenum, line in enumerate(lines[1:], 1):
|
||||
stripped_line = line.strip()
|
||||
@ -437,10 +523,20 @@ class ServiceBot():
|
||||
_reply_format(next_line)
|
||||
next_line = '' # Reset the next line buffer
|
||||
else:
|
||||
# Show the last line.
|
||||
_reply_format(next_line)
|
||||
else:
|
||||
_reply("Error: Command %r doesn't offer any help." % command)
|
||||
return
|
||||
|
||||
# Regardless of whether help text is available, mention aliases.
|
||||
if not shortform:
|
||||
if command in self.alias_cmds:
|
||||
_reply(' ')
|
||||
_reply('This command is an alias for \x02%s\x02.' % self.alias_cmds[command])
|
||||
aliases = set(alias for alias, primary in self.alias_cmds.items() if primary == command)
|
||||
if aliases:
|
||||
_reply(' ')
|
||||
_reply('Available aliases: \x02%s\x02' % ', '.join(aliases))
|
||||
|
||||
def help(self, irc, source, args):
|
||||
"""<command>
|
||||
@ -472,8 +568,8 @@ class ServiceBot():
|
||||
except IndexError:
|
||||
plugin_filter = None
|
||||
|
||||
# Don't show CTCP handlers in the public command list.
|
||||
cmds = sorted(cmd for cmd in self.commands.keys() if '\x01' not in cmd)
|
||||
# Don't show CTCP handlers or aliases in the public command list.
|
||||
cmds = sorted(cmd for cmd in self.commands.keys() if '\x01' not in cmd and cmd not in self.alias_cmds)
|
||||
|
||||
if plugin_filter is not None:
|
||||
# Filter by plugin, if the option was given.
|
||||
@ -511,7 +607,7 @@ class ServiceBot():
|
||||
self._show_command_help(irc, cmd, private=True, shortform=True)
|
||||
self.reply(irc, 'End of command listing.', private=True)
|
||||
|
||||
def registerService(name, *args, **kwargs):
|
||||
def register_service(name, *args, **kwargs):
|
||||
"""Registers a service bot."""
|
||||
name = name.lower()
|
||||
if name in world.services:
|
||||
@ -519,14 +615,15 @@ def registerService(name, *args, **kwargs):
|
||||
|
||||
# Allow disabling service spawning either globally or by service.
|
||||
elif name != 'pylink' and not (conf.conf.get(name, {}).get('spawn_service',
|
||||
conf.conf['bot'].get('spawn_services', True))):
|
||||
conf.conf['pylink'].get('spawn_services', True))):
|
||||
return world.services['pylink']
|
||||
|
||||
world.services[name] = sbot = ServiceBot(name, *args, **kwargs)
|
||||
sbot.spawn()
|
||||
return sbot
|
||||
registerService = register_service
|
||||
|
||||
def unregisterService(name):
|
||||
def unregister_service(name):
|
||||
"""Unregisters an existing service bot."""
|
||||
name = name.lower()
|
||||
|
||||
@ -542,11 +639,12 @@ def unregisterService(name):
|
||||
if name == 'pylink':
|
||||
ircobj.pseudoclient = None
|
||||
|
||||
ircobj.proto.quit(uid, "Service unloaded.")
|
||||
ircobj.quit(uid, "Service unloaded.")
|
||||
|
||||
del world.services[name]
|
||||
unregisterService = unregister_service
|
||||
|
||||
def wrapArguments(prefix, args, length, separator=' ', max_args_per_line=0):
|
||||
def wrap_arguments(prefix, args, length, separator=' ', max_args_per_line=0):
|
||||
"""
|
||||
Takes a static prefix and a list of arguments, and returns a list of strings
|
||||
with the arguments wrapped across multiple lines. This is useful for breaking up
|
||||
@ -554,7 +652,7 @@ def wrapArguments(prefix, args, length, separator=' ', max_args_per_line=0):
|
||||
"""
|
||||
strings = []
|
||||
|
||||
assert args, "wrapArguments: no arguments given"
|
||||
assert args, "wrap_arguments: no arguments given"
|
||||
|
||||
buf = prefix
|
||||
|
||||
@ -562,7 +660,7 @@ def wrapArguments(prefix, args, length, separator=' ', max_args_per_line=0):
|
||||
|
||||
while args:
|
||||
assert len(prefix+args[0]) <= length, \
|
||||
"wrapArguments: Argument %r is too long for the given length %s" % (args[0], length)
|
||||
"wrap_arguments: Argument %r is too long for the given length %s" % (args[0], length)
|
||||
|
||||
# Add arguments until our buffer is up to the length limit.
|
||||
if (len(buf + args[0]) + 1) <= length and ((not max_args_per_line) or len(buf.split(' ')) < max_args_per_line):
|
||||
@ -577,6 +675,7 @@ def wrapArguments(prefix, args, length, separator=' ', max_args_per_line=0):
|
||||
strings.append(buf)
|
||||
|
||||
return strings
|
||||
wrapArguments = wrap_arguments
|
||||
|
||||
class IRCParser(argparse.ArgumentParser):
|
||||
"""
|
||||
@ -595,18 +694,126 @@ class IRCParser(argparse.ArgumentParser):
|
||||
def exit(self, *args):
|
||||
return
|
||||
|
||||
class DeprecatedAttributesObject():
|
||||
"""
|
||||
Object implementing deprecated attributes and warnings on access.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.deprecated_attributes = {}
|
||||
# From http://modern.ircdocs.horse/formatting.html
|
||||
_strip_color_regex = re.compile(r'\x03(\d{1,2}(,\d{1,2})?)?')
|
||||
_irc_formatting_chars = "\x02\x1D\x1F\x1E\x11\x16\x0F\x03"
|
||||
|
||||
def __getattribute__(self, attr):
|
||||
# Note: "self.deprecated_attributes" calls this too, so the != check is
|
||||
# needed to prevent a recursive loop!
|
||||
if attr != 'deprecated_attributes' and attr in self.deprecated_attributes:
|
||||
log.warning('Attribute %s.%s is deprecated: %s' % (self.__class__.__name__, attr,
|
||||
self.deprecated_attributes.get(attr)))
|
||||
def strip_irc_formatting(text):
|
||||
"""Returns text with IRC formatting (colors, underlines, bold, italics, reverse) removed."""
|
||||
text = _strip_color_regex.sub('', text)
|
||||
for char in _irc_formatting_chars:
|
||||
text = text.replace(char, '')
|
||||
return text
|
||||
|
||||
return object.__getattribute__(self, attr)
|
||||
_subrange_re = re.compile(r'(?P<start>(\d+))-(?P<end>(\d+))')
|
||||
def remove_range(rangestr, mylist):
|
||||
"""
|
||||
Removes a range string of (one-indexed) items from the list.
|
||||
Range strings are indices or ranges of them joined together with a ",":
|
||||
e.g. "5", "2", "2-10", "1,3,5-8"
|
||||
|
||||
See test/test_utils.py for more complete examples.
|
||||
"""
|
||||
if None in mylist:
|
||||
raise ValueError("mylist must not contain None!")
|
||||
|
||||
# Split and filter out empty subranges
|
||||
ranges = filter(None, rangestr.split(','))
|
||||
if not ranges:
|
||||
raise ValueError("Invalid range string %r" % rangestr)
|
||||
|
||||
for subrange in ranges:
|
||||
match = _subrange_re.match(subrange)
|
||||
if match:
|
||||
start = int(match.group('start'))
|
||||
end = int(match.group('end'))
|
||||
|
||||
if end <= start:
|
||||
raise ValueError("Range start (%d) is <= end (%d) in range string %r" %
|
||||
(start, end, rangestr))
|
||||
elif 0 in (end, start):
|
||||
raise ValueError("Got range index 0 in range string %r, this function is one-indexed" %
|
||||
rangestr)
|
||||
|
||||
# For our purposes, make sure the start and end are within the list
|
||||
mylist[start-1], mylist[end-1]
|
||||
|
||||
# Replace the entire range with None's
|
||||
log.debug('utils.remove_range: removing items from %s to %s: %s', start, end, mylist[start-1:end])
|
||||
mylist[start-1:end] = [None] * (end-(start-1))
|
||||
|
||||
elif subrange in string.digits:
|
||||
index = int(subrange)
|
||||
if index == 0:
|
||||
raise ValueError("Got index 0 in range string %r, this function is one-indexed" %
|
||||
rangestr)
|
||||
log.debug('utils.remove_range: removing item %s: %s', index, mylist[index-1])
|
||||
mylist[index-1] = None
|
||||
|
||||
else:
|
||||
raise ValueError("Got invalid subrange %r in range string %r" %
|
||||
(subrange, rangestr))
|
||||
|
||||
return list(filter(lambda x: x is not None, mylist))
|
||||
|
||||
def get_hostname_type(address):
|
||||
"""
|
||||
Returns whether the given address is an IPv4 address (1), IPv6 address (2), or neither
|
||||
(0; assumed to be a hostname instead).
|
||||
"""
|
||||
try:
|
||||
ip = ipaddress.ip_address(address)
|
||||
except ValueError:
|
||||
return 0
|
||||
else:
|
||||
if isinstance(ip, ipaddress.IPv4Address):
|
||||
return 1
|
||||
elif isinstance(ip, ipaddress.IPv6Address):
|
||||
return 2
|
||||
else:
|
||||
raise ValueError("Got unknown value %r from ipaddress.ip_address()" % address)
|
||||
|
||||
_duration_re = re.compile(r"^((?P<week>\d+)w)?((?P<day>\d+)d)?((?P<hour>\d+)h)?((?P<minute>\d+)m)?((?P<second>\d+)s)?$")
|
||||
def parse_duration(text):
|
||||
"""
|
||||
Takes in a duration string and returns the equivalent amount of seconds.
|
||||
|
||||
Time strings are in the following format:
|
||||
- '123' => 123 seconds
|
||||
(positive integers are treated as # of seconds)
|
||||
- '1w2d3h4m5s' => 1 week, 2 days, 3 hours, 4 minutes, and 5 seconds
|
||||
(must be in decreasing order by unit)
|
||||
- '72h' => 72 hours
|
||||
- '1h5s' => 1 hour and 5 seconds
|
||||
and so on...
|
||||
"""
|
||||
# If we get an already valid number, just return it
|
||||
if text.isdigit():
|
||||
return int(text)
|
||||
|
||||
match = _duration_re.match(text)
|
||||
if not match:
|
||||
raise ValueError("Failed to parse duration string %r" % text)
|
||||
result = 0
|
||||
matched = 0
|
||||
|
||||
if match.group('week'):
|
||||
result += int(match.group('week')) * 7 * 24 * 60 * 60
|
||||
matched += 1
|
||||
if match.group('day'):
|
||||
result += int(match.group('day')) * 24 * 60 * 60
|
||||
matched += 1
|
||||
if match.group('hour'):
|
||||
result += int(match.group('hour')) * 60 * 60
|
||||
matched += 1
|
||||
if match.group('minute'):
|
||||
result += int(match.group('minute')) * 60
|
||||
matched += 1
|
||||
if match.group('second'):
|
||||
result += int(match.group('second'))
|
||||
matched += 1
|
||||
|
||||
if not matched:
|
||||
raise ValueError("Failed to parse duration string %r" % text)
|
||||
|
||||
return result
|
||||
|
9
world.py
9
world.py
@ -22,17 +22,22 @@ exttarget_handlers = {}
|
||||
|
||||
# Trigger to be set when all IRC objects are initially created.
|
||||
started = threading.Event()
|
||||
|
||||
# Global daemon starting time.
|
||||
start_ts = time.time()
|
||||
|
||||
# Trigger to set on shutdown.
|
||||
shutting_down = threading.Event()
|
||||
|
||||
# Source address.
|
||||
source = "https://github.com/GLolol/PyLink" # CHANGE THIS IF YOU'RE FORKING!!
|
||||
source = "https://github.com/jlu5/PyLink" # CHANGE THIS IF YOU'RE FORKING!!
|
||||
|
||||
# Fallback hostname used in various places internally when hostname isn't configured.
|
||||
fallback_hostname = 'pylink.int'
|
||||
|
||||
# Defines messages to be logged as soon as the log system is set up, for modules like conf that are
|
||||
# initialized before log. This is processed (and then not used again) when the log module loads.
|
||||
log_queue = deque()
|
||||
_log_queue = deque()
|
||||
|
||||
# Determines whether we have a PID file that needs to be removed.
|
||||
_should_remove_pid = False
|
||||
|
Loading…
Reference in New Issue
Block a user