3
0
mirror of https://github.com/jlu5/PyLink.git synced 2025-04-21 07:17:54 +02:00

Compare commits

...

371 Commits

Author SHA1 Message Date
James Lu
43e41cdfa5 PyLink 3.1.0
Update README links and mark the project as discontinued.
2023-01-03 18:48:24 -08:00
Paige Thompson
c453926718
Allow loading a custom CA certificate via a ssl_cafile option (#677)
Co-authored-by: Paige Thompson <paigeadele@gmail.com>
2022-09-06 19:04:54 -07:00
James Lu
523c1d2b13 relay: strip / from idents
Closes #673.
2022-02-21 02:21:49 -08:00
James Lu
ab982662b1 Remove travis-ci config
[skip ci]
2022-02-05 14:34:59 -08:00
James Lu
fb2327d4e7 Update README
[skip ci]
2022-02-04 12:27:28 -08:00
James Lu
8c4efc030a raw: fix permission check logic 2022-01-02 11:19:38 -08:00
James Lu
499a0dd403 drone: add PyPI push, build Docker on a branch to allow for weekly cron 2021-12-30 11:19:35 -08:00
James Lu
ca2d603fa1 RELNOTES: fix date 2021-12-30 11:04:52 -08:00
James Lu
cd2ecc3853 Merge branch 'ci-update' 2021-12-30 11:04:29 -08:00
James Lu
a0a2cda49f PyLink 3.1-beta1 2021-12-30 11:02:41 -08:00
James Lu
6391d6c282 Run CI on push, against multiple Python versions 2021-12-28 10:56:50 -08:00
James Lu
563b48e9c5 Revert "drone CI: remove date based tags, it isn't strictly necessary"
This reverts commit 0cba40e4c512932a80aae1413707faac67d74f9d.
2021-12-28 10:46:46 -08:00
James Lu
2915bf2236 opercmds: more consistent formatting for KILL messages
Any "Killed (sender (...)" prefixes should be added by the protocol module.
2021-12-25 01:02:19 -08:00
James Lu
d6de0d97f3 unreal: send kill messages without killpath
Closes #671.
2021-12-25 01:00:42 -08:00
James Lu
171eccf9c7 Further harden exec and raw in the default setup
Closes #568.
2021-12-25 00:47:50 -08:00
James Lu
d3ccdca3d1 Merge branch 'py37-base' 2021-12-25 00:33:45 -08:00
James Lu
3155172fc8 Revert "setup.py: work around installation error on Python 3.4"
This reverts commit c91b5f74ea5449f65205d0581416856f018cd2a0.
2021-12-25 00:33:36 -08:00
James Lu
243efbd0f8 Revert "test_irc_parsers: fix Python 3.5 support"
This reverts commit 8cf1beb1839ce874730256aad7dfa4496d37c217.
2021-12-25 00:33:36 -08:00
James Lu
8d01eaa5c8 Bump base version to Python 3.7 2021-12-25 00:32:32 -08:00
James Lu
ada130b1cd Drop references to devel branch
This project is not currently active enough to justify a multi-branch setup..
2021-12-25 00:31:15 -08:00
James Lu
e6401a19df relay: remove race condition-prone print
Speculative fix #670
2021-12-25 00:27:34 -08:00
James Lu
ac7339e460 Default to system IPv4/IPv6 preference when resolving hostnames
Fixes #667. This implementation is similar but also preserves compatibility with the "ipv6" option,
which allows setting the preferred address type without hardcoding a bind IP.
2021-12-25 00:18:03 -08:00
James Lu
f55057092a inspircd: set default target_version to insp3 2021-12-24 23:43:12 -08:00
James Lu
46cc621df1 More concise UID generators 2021-07-14 21:57:56 -07:00
James Lu
bc3a7abe02 ircs2s_common: don't strip away other whitespace chars when tokenizing 2021-07-14 21:57:56 -07:00
James Lu
3f59dcd884 unreal: bounce attempts to CHGIDENT/HOST/NAME protected services clients 2021-06-23 09:35:10 -07:00
James Lu
fc971aa679 pmodule-spec: Update notes about non-IRC protocols and PyLinkNetworkCoreWithUtils 2021-06-15 00:22:14 -07:00
James Lu
e25e3834a8 Add support for oper notices (GLOBOPS/OPERWALL) (#511) 2021-06-15 00:21:03 -07:00
James Lu
2ae72d6723 Expose SSL/TLS state in UID hooks when available (#169) 2021-06-13 01:00:41 -07:00
James Lu
8322817395 p10: fix message handling; sender numerics are not prefixed with ":" here
Regression from 8db238e869208d29d35cac10f0aa6899d3eb3cb1
2021-06-13 00:44:41 -07:00
James Lu
6ba99b302f Merge remote-tracking branch 'origin/wip/track-user-ssl' into devel 2021-06-13 00:11:18 -07:00
James Lu
92f60ecb4b Bump to 3.1-dev 2021-06-13 00:11:08 -07:00
James Lu
075c1d141d relay: sanitize idents on hybrid further 2021-06-09 20:19:26 -07:00
James Lu
d94593e4f6 unreal: declare EXTSWHOIS support 2021-06-09 20:18:34 -07:00
James Lu
8db238e869 ircs2s_common: only read sender prefixes on lines starting with ":"
This fixes incorrect behaviour if the nick of a sender matches that of a S2S command (e.g. "ping")
2021-06-09 20:17:27 -07:00
James Lu
da7f9611bc Remove my old nickname from examples 2021-06-09 20:15:49 -07:00
James Lu
ed1644a636 relay: workaround strict ident checks on hybrid 2021-06-05 00:39:23 -07:00
James Lu
1b433b741b classes: add ProtocolError to __all__ 2021-06-05 00:39:18 -07:00
James Lu
53e30520b8 Guard exec plugin behind a config option 2021-06-04 17:48:29 -07:00
James Lu
ba5f89d03c Move to libera.chat
[skip ci]
2021-05-19 18:03:13 -07:00
James Lu
88294ff0b0 relay-quickstart: a long needed refresh
- Document in more detail how Relay actually works (mirroring / puppeting via a services server)
- Document more clearly the steps to create a channel: especially mention how CREATE and LINK have to be run from different networks.
- Various typo fixes
2021-05-03 21:18:24 -07:00
James Lu
0b6845fa92 relay: allow deleting channels created before a casemapping change 2021-02-27 12:02:42 -08:00
James Lu
4500e27931 relay: fix channel not found errors on LINK when remote casemapping differs
This can happen, e.g. if the remote network uses casemapping=ascii and has a channel containing "|", and the local network uses casemapping=rfc1459.

Thanks to iTzNavic for reporting.
2021-02-27 12:01:01 -08:00
James Lu
537c643ed0 ircs2s_common: gracefully handle QUIT messages without a reason
Closes #663.
2021-02-14 22:39:57 -08:00
James Lu
d9aa5e9869 Merge remote-tracking branch 'origin/master' into devel 2021-02-14 22:33:59 -08:00
James Lu
17fccfeca9 Add a basic linting config 2021-02-09 12:31:25 -08:00
James Lu
c5541b58e5 docs: mention that Clientbot-only relays are not supported 2021-02-09 12:31:21 -08:00
James Lu
5eca51d979 example-conf: inspircd 3 support is not beta anymore 2021-01-20 11:58:50 -08:00
James Lu
15d51b3455 Don't loop infinitely if _send fails... 2021-01-10 10:28:34 -08:00
James Lu
b254a7f971 readme refresh
yeah, I'm officially looking for new maintainers
2020-12-07 12:50:15 -08:00
James Lu
d53ed4f25d readme refresh
yeah, I'm officially looking for new maintainers
2020-12-07 12:08:19 -08:00
James Lu
cc2298d0be inspircd: track SSL/TLS status of remote users 2020-10-19 14:03:35 -07:00
James Lu
8ee0f3fdab hybrid, p10, ts6, unreal: track SSL/TLS status of remote users (#169) 2020-10-19 13:58:55 -07:00
James Lu
e0cc238001 ts6: fix ssl umode definitions 2020-10-19 13:58:03 -07:00
James Lu
b02aadf378 _send: break if the socket is None 2020-09-29 18:49:43 +00:00
James Lu
d50de12834 Retry when socket.send() fails with BlockingIOError / EAGAIN 2020-09-29 17:45:13 +00:00
James Lu
2aa00d6efc relay: skip messages from clientbot networks when relay_clientbot isn't loaded 2020-08-15 23:23:29 -07:00
Celelibi
16a7cef1aa plugin relay: Rename homeirc to origirc
Signed-off-by: Celelibi <celelibi@gmail.com>
2020-06-18 21:15:53 -07:00
Celelibi
43532fd1cc ClientbotWrapperProtocol: Missing log message argument: channel
Signed-off-by: Celelibi <celelibi@gmail.com>
2020-06-18 21:15:53 -07:00
Celelibi
cb0af148d8 IRCNetwork: Unused attribute _selector_key
Was always None because the called function doesn't return a value.

Signed-off-by: Celelibi <celelibi@gmail.com>
2020-06-18 21:15:53 -07:00
Celelibi
dcd0a28c89 PyLinkNetworkCore: rename filename, config -> channel, chandata
Signed-off-by: Celelibi <celelibi@gmail.com>
2020-06-18 21:15:53 -07:00
Celelibi
7204ef1cf1 plugin stats: Refactor multiline function call
Signed-off-by: Celelibi <celelibi@gmail.com>
2020-06-18 21:15:53 -07:00
Celelibi
88116dbe8d plugin stats: Missing parentheses change number of arguments
Signed-off-by: Celelibi <celelibi@gmail.com>
2020-06-18 21:15:53 -07:00
Celelibi
b49d5775e2 TS6Protocol: Missing logging argument in handle_realhost
Signed-off-by: Celelibi <celelibi@gmail.com>
2020-06-18 21:15:53 -07:00
Celelibi
03c9c71dc3 TS6Protocol: change servername to numeric in handle_pass
Signed-off-by: Celelibi <celelibi@gmail.com>
2020-06-18 21:15:53 -07:00
Celelibi
26bdc90781 TS6Protocol: target_ircd renamed when refactored
Signed-off-by: Celelibi <celelibi@gmail.com>
2020-06-18 21:15:53 -07:00
Celelibi
6bf66f9e4d KeyedDefaultdict: super() already bind to self
Signed-off-by: Celelibi <celelibi@gmail.com>
2020-06-18 21:15:53 -07:00
Celelibi
84b73bb89f
Tidy up imports and define __all__ in modules (#660) 2020-06-18 15:47:20 -07:00
Celelibi
9470e9329a
Implement path configuration for files created by pylink (#659)
* Configure log directory

Signed-off-by: Celelibi <celelibi@gmail.com>

* Configure data store directory

Signed-off-by: Celelibi <celelibi@gmail.com>

* Configure PID file directory

Signed-off-by: Celelibi <celelibi@gmail.com>
2020-05-30 11:49:01 -07:00
James Lu
947732580a PyLink 3.0.0 2020-04-11 00:28:24 -07:00
James Lu
3d5f9685c5 README: update badge colours & order
[skip ci]
2020-04-10 16:27:42 -07:00
James Lu
c53ee0a80c README: refresh, add Docker install instructions
[skip ci]
2020-04-10 14:56:52 -07:00
James Lu
0cba40e4c5 drone CI: remove date based tags, it isn't strictly necessary 2020-04-10 13:38:18 -07:00
James Lu
c14fc5cf07 Add Drone CI pipeline for building Docker image 2020-04-10 13:27:27 -07:00
James Lu
ba22b18cc4 Replace references to PyLink 2.1 with 3.0 2020-04-10 11:15:54 -07:00
James Lu
a143d98ac1 global: always coerse channel argument to str
These can be type int as of v3.0, e.g. in pylink-discord
2020-03-30 13:09:38 -07:00
James Lu
a19f257bd8 clientbot: remove references to self.irc
Reported by @genius3000.
2020-03-29 01:15:11 -07:00
James Lu
380854f88a Remove references to ./pylink
Running the launcher from the source folder isn't necessary anymore.
2020-03-24 18:07:18 -07:00
James Lu
8ffd787075 global: call match_text() on the target network object
Technically this will only make a difference if a server object overrides match_text(), but this is the more correct solution.
2020-03-16 19:07:18 -07:00
James Lu
b4d7883a71 global: ignore networks that don't have .pseudoclient set
This is the case for e.g. the control network object in pylink-discord.
2020-03-16 19:06:28 -07:00
James Lu
f17540a7e2 README refresh; update supported IRCds list
- Move charybdis to extended support - I have not seen any networks use this in production
- Move legacy p10 / ts6 variants into a new Legacy support section

[skip ci]
2020-03-14 10:28:21 -07:00
James Lu
b433fed718 unreal: update module header 2020-03-08 16:47:31 -07:00
James Lu
475349dc39 p10: warn when receiving an invalid subcommand with use_extended_accounts=true 2020-03-08 16:29:16 -07:00
James Lu
a8f0bd4fb7 classes: remove self.proto, self.irc compat links 2020-03-08 15:37:06 -07:00
James Lu
f7e84e7816 PyLink 3.0-rc1 2020-02-22 23:57:43 -08:00
James Lu
bdfeec09c6 Merge remote-tracking branch 'origin/master' into devel
Conflicts:
	classes.py
2020-02-22 23:57:02 -08:00
James Lu
7ce8f41f95 Add changelog for 3.0-rc1 2020-02-22 23:56:27 -08:00
James Lu
99c379e32f example-conf: unmark encoding option as experimental 2020-02-22 23:56:10 -08:00
James Lu
908dcb4873 p10: ignore ACCOUNT subcommands other than R, M, and U 2020-02-16 11:31:02 -08:00
James Lu
b083b6dede commands.version: replace license notice with (a much more useful) Python version 2020-01-26 13:48:43 -08:00
James Lu
5c3306bcff ircs2s_common: fix handling when failing to extract kill reason 2020-01-26 00:31:17 -08:00
James Lu
7c0deaa684 relay-quickstart: describe delinking another network from our channels
[skip ci]
2020-01-09 08:53:44 -08:00
James Lu
574a3056a3 modelists: update githack URLs
[skip ci]
2019-12-29 10:08:47 -08:00
James Lu
0e13243d02 chatircd: fold usermode +t into sslonlymsg 2019-12-29 10:04:38 -08:00
James Lu
6e1352dfd3 inspircd: alias antiredirect (umode +L) to noforward 2019-12-29 10:02:02 -08:00
James Lu
34cd0ea7d5 modelists: mark module-provided modes on InspIRCd 2019-12-29 10:00:55 -08:00
James Lu
ea2925ef37 modelists: drop m_ prefix from insp3 column 2019-12-29 09:53:15 -08:00
James Lu
0e10b62705 ngircd: fixup mode definitions 2019-12-29 09:50:47 -08:00
James Lu
f01fada92f unreal: declare support for umodes +Z and +G 2019-12-29 09:50:00 -08:00
James Lu
5a487114b6 docs/modelists: add some missing RFC1459 mode definitions
[skip ci]
2019-12-29 09:32:27 -08:00
James Lu
52730268b0 exttarget: use match_text() to match exttarget text fields
(cherry picked from commit bfe8a23dbc363aee0813dce9784f03ec87127411)
2019-12-26 11:59:33 -08:00
James Lu
4d24aa236b exttargets.account: fix extraneous lowercasing of the network name
(cherry picked from commit b45fe4c12110fa3d8925a53e5386e8216768da25)
2019-12-26 11:59:33 -08:00
James Lu
a178a92d6a classes: fix incorrect wrap_messages() stub
(cherry picked from commit 5731a301ce98400e77108b3f6dbc98d675c0a902)
2019-12-26 11:59:33 -08:00
James Lu
bfe8a23dbc exttarget: use match_text() to match exttarget text fields 2019-12-25 16:04:58 -08:00
James Lu
b45fe4c121 exttargets.account: fix extraneous lowercasing of the network name 2019-12-25 15:58:18 -08:00
James Lu
ae01c5e418 Add Dockerfile
Closes #652.

Co-authored-by: James Newton <hello@jamesnewton.com>
2019-12-23 00:02:57 -08:00
James Lu
a79354cd52 apply_modes: fix removing multiple ban modes in one command 2019-12-22 23:57:36 -08:00
James Lu
847c26cd7b Declare Python 3.8 support
Also switch CI base version from xenial to bionic.
2019-12-22 23:13:49 -08:00
James Lu
44770fb6d1 classes: fix SyntaxWarning: "is" with a literal on Python 3.8 2019-12-22 23:11:58 -08:00
James Lu
594b7124ff inspircd: warn when using InspIRCd 2 compat mode on an InspIRCd 3 uplink
Some commands like KICK are not mangled correctly in this mode.
2019-12-22 22:37:49 -08:00
James Lu
dadf2c3211 structures: suppress CopyWrapper logging 2019-12-22 22:32:10 -08:00
James Lu
b24dc206e0 inspircd: negotiate casemapping setting on link for InspIRCd 3
Closes #654.
2019-12-22 22:27:12 -08:00
James Lu
b495cb4c5e classes: clean up _expandPUID
Also make the logging less noisy.
2019-12-22 22:14:26 -08:00
James Lu
b1b14dfb55 classes: disable logging about modes by default 2019-12-22 22:14:26 -08:00
James Lu
b15c885dbc RELNOTES: move all github references to my new username
[skip ci]
2019-12-08 12:21:33 -08:00
James Lu
6a71ca64ac PyLink 2.1-beta1 2019-12-08 12:18:13 -08:00
James Lu
0a264b430e docs/t: fix wrong version for the last two protocol caps 2019-12-08 12:17:33 -08:00
James Lu
daf496cc96 PyLink 2.0.3
-----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCgAdFiEEwmmRkfzwXj5hKJctxWWyWArxYlwFAl2gudoACgkQxWWyWArx
 YlwcUg/9ERp6HnviDa46cXt2OcQg0eNAuTFl0H+YCpjzj2Qvg8uBdCjh0jzAahh4
 rGw4XbKp5l3kXWMyjoy1roKqYbYy6nUdE4HCiER7wxpHqbUnfprCv6REIOxazz7Z
 jRwKbD89FKPtn957C7qfBBY93QX6h5v92QsotuAwyxDpN/3UTtpP11c/zb48m6Fx
 Mmabgj7HWAalB5w7wny0Xe97z7nTozzqlZeI3lrCShuST57n9w5Jh+rLD0CDqoro
 hzeezG4z9VvbaO7Pg/i+tdWinsZCKM5sLX9UuGen/3I2ncn7C3/e0mbtsQnFnCRL
 +BhJJanZWFcjxzIa/FhGall01yID/Uz5KIWsa/cOGe0KXDUPkqcBLmWs6/7zBFi8
 /QZL74SZXZrURFwpK/UCEmfJ0xX6reMMI+5Pj4HwAZsj8ZWGsfqp4criWiFsGjds
 XMa5/kM7bfC+fB3yHQIRQiu6z4QwB+t4B2HiECpRsx/Rr7zNu3lFMZsRj78KnWN8
 Hi2AoG8K0VMUSZKJIv/97turXmWKZE3D/+RnsteNIAxIgunjEg5Loq882O4p9jsj
 x4aolzXxtgYIqKDIn+WA8ilmOIF/eXKWp5QI7ciEKrEwSC/deVP734BLaPOJwMlA
 84XOwRy0a0WXdLexdRg6ANgwXzXFU1B/UayWDuNSbVAirM42naWJAjMEAAEKAB0W
 IQRPiKnwz3Cu98LInv4I4XxWErSJPwUCXaC53QAKCRAI4XxWErSJP6BEEADNSCxm
 8/xG5I7CxbBsODfD9wyOqgNNHD5xMsOhf6VILFauScE6M5afdfHCQQ9YSDERGIPu
 AvpeP0+R2Zy+ef08JZJcjn1DAH4FzgoAUOrlb2mtuAK/+fzDYAzgA+93BB3s+/ny
 CzEA60B2Yx2vhRYyZrU+TztCnUhDvbx/rNuCP/SKBG/dCSyRjm6el6C+1tQkyk6t
 LcFlh4Qvn6WcFxICKfWe4vscIoF3WmIj9q8fuoBQh700DEIoOg0bI08WgZctFLtC
 TMtKX6Or3K8G6HT3UJKHOYD2l04iXGxHbs/bbtvvUWnlBxAE2LDxGrJr251HaGgh
 rHHCp+eQZt21zmESnZ1FX8eDeTCEKbFTgEwIpQFOVttsNhZ4brdJDXL7p1aw5PsX
 pSmo/bfhOrhreFijFB7UlnYbPUINCW3ISgjypuKN1GQbA9gmulcK19LB4f+dmvTU
 MQi6Mo7j2ew0J3zc5kP6DNX2AkdAc2Nw8dXPtrWJr/ubjUBZDgM2CrWnTE+QJlk5
 5g/Lx0V4BG6GeYxmsuOEOV3EEH1QvCehgsmpyeJgYVYqq1wtIk1g4CGKtn+fY+7K
 y8TMueCPCXJtLOoH8iWTd//oy58c/IxQQGYmfk+sxUOIrIgvhU/oFIE+77LpkgEx
 E1Zd6Lct0+S6XiadvopE5IeE2CDr5DV9DqEMvA==
 =Oxxo
 -----END PGP SIGNATURE-----

Merge tag '2.0.3' into devel

PyLink 2.0.3

Conflicts:
	RELNOTES.md
	VERSION
	coremods/control.py
	example-conf.yml
	plugins/changehost.py
2019-12-08 11:34:18 -08:00
James Lu
b92f39d79c Update IRCd notes & supported IRCd versions
[skip ci]
2019-11-30 00:10:55 -08:00
James Lu
b9561500fd relay: simplify clientbot forcepart/kick formatting 2019-11-29 23:52:19 -08:00
James Lu
72c57d433a clientbot: add option to always autorejoin channels
Closes #647.
2019-11-29 23:51:57 -08:00
James Lu
7eff7861ed structures: implement _from_iterable() so that set operations work 2019-11-29 23:35:52 -08:00
James Lu
1a89813cd4 unreal: stop sending NETINFO on link
NETINFO isn't strictly necessary for services servers, and not sending it suppresses protocol version/network name mismatch warnings.
2019-11-29 23:03:35 -08:00
James Lu
d73e2fc209 unreal: read user modes from PROTOCTL USERMODES when available
This is sent by UnrealIRCd 4.2.3 and later.
2019-11-17 12:24:54 -08:00
James Lu
8a48d4d8cc inspircd: fix sending ping replies from subservers 2019-11-17 10:48:58 -08:00
James Lu
7bf1a9e08d Add can-manage-bot-channels protocol capability
This allows skipping part/join for service bots on platforms where this is not possible.
2019-11-02 13:55:49 -07:00
James Lu
85922cd376 README: drop webchat link
[skip ci]

(cherry picked from commit 4ceeb1630f5bc45947a9fd215da27e06eed37375)
2019-11-01 08:13:07 -07:00
James Lu
4ceeb1630f README: drop webchat link
[skip ci]
2019-10-28 19:28:07 -07:00
James Lu
41497a8a13 automode: mangle channels to "#chanid" on networks where they're stored as int
This allows the bulk of automode's commands to actually function.
2019-10-14 09:57:47 -07:00
James Lu
2c53ce0682 PyLink 2.0.3 2019-10-11 10:18:43 -07:00
James Lu
d28a9681ac automode: disable on networks where IRC modes aren't supported
Closes #638.
2019-10-10 22:24:44 -07:00
James Lu
297d31dab2 Add has-irc-modes capability (#620) 2019-10-10 22:17:11 -07:00
James Lu
f99be51515 changehost: add enable and enforce as network specific options.
Closes #611.
2019-10-10 21:45:18 -07:00
James Lu
da67d6c42f changehost: port most options to get_service_option(s) (#611, #642) 2019-10-10 21:04:15 -07:00
James Lu
1623462b73 relay: use get_service_options() to combine clientbot styles options (#642) 2019-10-10 19:16:58 -07:00
James Lu
e0d82cdf3d Add get_service_options API to merge together global & local network options
First part of #642.
2019-10-10 18:49:07 -07:00
James Lu
9ec83f3995 Base test for get_service_option() 2019-10-09 20:55:52 -07:00
James Lu
601b811912 test/ptf: reorganize tests into sections 2019-10-09 20:53:58 -07:00
James Lu
72e96156b5 changehost: listen for services account changes
This allows for consistent account based hostmasks for SASL gateways, etc.
2019-10-09 20:32:47 -07:00
James Lu
4095eea3a7 changehost: simplify _changehost() syntax 2019-10-09 20:30:53 -07:00
James Lu
8cf1beb183 test_irc_parsers: fix Python 3.5 support
open() only supports pathlib paths on 3.6 and later.
2019-09-15 16:27:17 -07:00
James Lu
52001ac82d .travis.yml: add 3.7, remove 3.4 tests 2019-09-15 16:24:55 -07:00
James Lu
c8ba6291a6 parse_irc_command: ignore empty IRC lines
I seem to be getting this on my InspIRCd 2 test server?
2019-09-10 19:46:46 -07:00
James Lu
083dc6a58f Rewrite is_server_name() to fail on hostnames with - and _ 2019-09-10 19:31:57 -07:00
James Lu
462fa91622 Add validate-hostname tests from ircdocs/parser-tests 2019-09-10 19:22:53 -07:00
James Lu
b803c23b57 Add in mask-match tests from ircdocs/parser-tests 2019-09-10 19:19:16 -07:00
James Lu
899443d2fe split_hostmask: raise an error on empty nick/ident/host 2019-09-10 19:12:26 -07:00
James Lu
fe4bea2948 Add in userhost-split tests from ircdocs/parser-tests 2019-09-10 19:12:18 -07:00
James Lu
01705f8393 Skip message tag parse tests for now 2019-09-10 19:10:58 -07:00
James Lu
943168df53 parse_message_args: remove extraneous \'s that aren't escaping characters 2019-09-10 18:31:07 -07:00
James Lu
aba198dbd6 parse_args: ignore extra spaces not part of the final multi-word arg 2019-09-10 18:11:07 -07:00
James Lu
188d0f647e ircs2s_common: make parse_message_tags() a classmethod 2019-09-10 18:11:07 -07:00
James Lu
19f7ba38b3 Begin integrating ircdocs/parser-tests 2019-09-10 18:11:07 -07:00
James Lu
c1859b64fa inspircd: fix handling of SVSTOPIC on insp3 2019-08-29 11:16:10 -07:00
James Lu
f9368dd5cc Protocol tests for get_hostmask(), get_friendly_name() 2019-08-26 16:54:27 -07:00
James Lu
2baec4c65a Protocol tests for wrap_modes() 2019-08-26 16:47:49 -07:00
James Lu
ee4997dd72 Tests for join_modes, base case tests for apply_modes / reverse_modes 2019-08-26 16:24:58 -07:00
James Lu
ebce431ba4 reverse_modes: test cycling prefix modes 2019-08-26 16:10:08 -07:00
James Lu
a1f3af9099 reverse_modes: deduplicate reversing modes with arguments 2019-08-26 16:10:08 -07:00
James Lu
d93c071446 reverse_modes: test mode cycling with simple modes and bans 2019-08-26 16:10:08 -07:00
James Lu
9168880204 parse_modes: fix handling of +b-b ban cycles 2019-08-26 16:10:08 -07:00
James Lu
c2b5966739 reverse_modes: ignore unsetting simple modes that didn't exist 2019-08-26 16:10:08 -07:00
James Lu
b685f416f6 reverse_modes: treat mode arguments case insensitively 2019-08-26 16:10:08 -07:00
James Lu
0533827ddf reverse_modes: add basic tests 2019-08-26 16:10:02 -07:00
James Lu
32219ccb78 reverse_modes: return a list and not a set of modes
This ensures that order is kept when the input is a list.
2019-08-26 13:16:52 -07:00
James Lu
808e1d1f5a protocol tests: cleanup 2019-08-26 12:59:57 -07:00
James Lu
304631ebd0 Fixes to clientbot._get_UID() behaviour 2019-08-26 12:17:07 -07:00
James Lu
27eed3334b parse_modes: test combinations of nicks and UIDs in prefix modes 2019-08-23 21:24:00 -07:00
James Lu
c1dbfdab48 classes, clientbot: don't allow _get_UID in parse_modes to create new users 2019-08-23 21:22:28 -07:00
James Lu
da58669de5 parse_modes: case fold parameters to modes 2019-08-23 21:01:55 -07:00
James Lu
6ad34672d3 apply_modes: fix statekeeping with current modes mapping 2019-08-23 00:22:25 -07:00
James Lu
46f081e19b apply_modes: treat modes with arguments case-insensitively 2019-08-23 00:11:21 -07:00
James Lu
cb4d2cc384 Add more mode cycling (+b/-b, etc.) tests 2019-08-23 00:11:21 -07:00
James Lu
3eb90fa65c More rigorous testing of +k/-k parsing 2019-08-23 00:11:21 -07:00
James Lu
fe51f71a6e apply_modes: refactor checks for existing modes 2019-08-23 00:10:30 -07:00
James Lu
087ca0947b inspircd: write InspIRCd 3.x in file header 2019-08-22 22:58:34 -07:00
James Lu
a885b79306 More tests for parse_modes(), apply_modes() 2019-08-22 22:58:24 -07:00
James Lu
575cff297d Channel: remove call to deprecated function name 2019-08-22 22:58:15 -07:00
James Lu
e5493eac87 docs/modelists: regenerate & update channel modes list
- channel-modes: split inspircd column into insp20, insp3 sections - this will let us account for current and future differences between the two
- channel-modes: modularize unreal mode list
2019-08-22 21:04:58 -07:00
James Lu
26bfc06869 unreal: get rid of weird cmode +f workaround 2019-08-22 21:02:30 -07:00
James Lu
d3f2a370da Revert "inspircd: don't allow _ in hosts"
This reverts commit ac8b7babf15e1ef760c8fe2fdf83178aaed09ddf.
2019-08-22 19:02:59 -07:00
James Lu
a8832a5f93 modelists: update extban listing 2019-08-22 19:00:39 -07:00
James Lu
0b8ed2dae9 unreal: declare support for msgbypass and timedban extbans
Closes #557.
2019-08-22 18:48:46 -07:00
James Lu
452a47d4f1 relay: handle acting extbans for +e too
InspIRCd acting extbans and UnrealIRCd ~m are both used in theis context.
2019-08-22 18:40:23 -07:00
James Lu
d57b121600 unreal: work around a potential race when sending kills on join
(cherry picked from commit 1780271dd0077cb19fb0367d1df99827b10362b5)
2019-08-22 17:46:15 -07:00
James Lu
e0a618f317 [SECURITY] permissions: only whitelist the defined login:user for legacy accounts
It's possible for login:user and login:accounts to be used together, although this is discouraged.

(cherry picked from commit 4eb0420378e7b627dbf1f3c90e0e33012f54d4b6)
2019-08-22 17:46:15 -07:00
James Lu
e02ab9f2ff relay: consistency fixes for the hideoper setting
- Don't enforce +H on /oper when the hideoper option is disabled
- Skip relaying -H if the hideoper option is enabled - closes #629

(cherry picked from commit 9a74626d62b18a695f76449369645a42ceffb8cc)
2019-08-22 17:46:15 -07:00
James Lu
2cdcd8e193 clientbot: fix error when MODES is defined in ISUPPORT but given no value
(cherry picked from commit 61ca8dd781aa1bc0458d9c4cbb2e6223f0fa0499)

This fixes connections to e.g. Oragono
2019-08-22 17:46:15 -07:00
James Lu
fae63d77b2 README: mention that ngIRCd's CloakHost and CloakUserToNick are not supported
Cloak tools that enforce hosts on remote users are by nature unsupportable because they cause hostmask desyncs when forwarding Relay users. This in turn makes channel moderation impossible.

[skip ci]

(cherry picked from commit 1a692f55ad6e4a08f0b7eab08c11f6677b1d0ae9)
2019-08-22 17:44:16 -07:00
James Lu
f3569b4fd9 ts6: add support for hiding PyLink servers 2019-08-22 17:39:00 -07:00
James Lu
5d579481aa Base protocol tests for _get_UID, parse_modes 2019-08-18 20:55:10 -07:00
James Lu
6b78b45b20 ngircd: make linking to non-ngIRCd servers a fatal error 2019-08-18 19:51:37 -07:00
James Lu
1a692f55ad README: mention that ngIRCd's CloakHost and CloakUserToNick are not supported
Cloak tools that enforce hosts on remote users are by nature unsupportable because they cause hostmask desyncs when forwarding Relay users. This in turn makes channel moderation impossible.

[skip ci]
2019-08-18 19:50:48 -07:00
James Lu
4a8c96c883 And now, a test fixture for protocols/ 2019-08-18 16:36:02 -07:00
James Lu
07d8c8828a relay: fix incorrect variable when logging invalid channels in LINK
Where on earth is c even defined?
2019-08-04 11:41:28 -07:00
James Lu
80188c3673 Sort imports via isort 2019-07-14 15:12:29 -07:00
James Lu
19d794a6f5 relay_clientbot: refactor 'rpm' to handle duplicate nicks & nicks containing spaces
Closes #650.
2019-07-14 13:21:47 -07:00
James Lu
6ac2daebfa commands: improvements to the 'showuser' command
- Indent output lines for each specific user
- Skip showing Home server / Nick TS line if neither is available
- Handle nicks with spaces in them
- Show user modes after basic details
2019-07-14 13:21:47 -07:00
James Lu
8e85fa935d PyLink 2.1-alpha2 2019-07-14 12:29:35 -07:00
James Lu
350ba5f89c Changelog draft for 2.1-alpha2
[skip ci]
2019-07-13 02:16:29 -07:00
James Lu
edd27eea41 relay: format KILL sources when relaying local kills
Kill reason formatting was changed in #520.
2019-07-01 14:18:35 -07:00
James Lu
bcdd26926d IRCNetwork: use bytearray for buffers 2019-07-01 14:18:35 -07:00
James Lu
4bd334e2b8 antispam: read nicks from userdata when handling QUITs (#617)
get_hostmask() doesn't work on someone who has already quit.
2019-07-01 14:18:31 -07:00
James Lu
e3e0eac747 classes: revise docstrings
Mostly, mention which methods are IRC specific and which should be overridden to support other platforms.
2019-07-01 14:17:26 -07:00
James Lu
c7fd037879 Revise handling of KILL and QUIT hooks
- Both of these now always contain a non-empty userdata argument.
- If we receive both a KILL and a QUIT for any client, only the one received first will be sent as a hook.
- Also, adjust _remove_client() to return the data of the user that was removed.
2019-07-01 13:36:53 -07:00
James Lu
35b38dfb05 antispam: add part / quit message filtering for plugins like Relay
Closes #617.
2019-06-27 13:07:04 -07:00
James Lu
b6cf09ae52 example-conf: fixes to antispam examples
- It should be servers::<server name>::antispam_textfilter_globs, not servers::<server name>::antispam_textfilters_globs
- Matches (via utils.match_text) are Unicode case-insensitive as of PyLink 2.1
2019-06-27 13:07:04 -07:00
James Lu
93f608a504 writing-plugins: mention that editing hook payloads is allowed
Part of #452.
2019-06-27 13:07:02 -07:00
James Lu
9ad2b03833 permissions-reference: briefly mention (pi)eval, iexec commands 2019-06-26 13:54:32 -07:00
James Lu
19c7dce931 commands: add a 'shownet' command
Basic info available to everyone include network name, protocol module, and encoding.

For those with the commands.shownet.extended permission, this also allows looking up disconnected networks defined in the config, and shows configured IP:port, PyLink hostname, SID, and SID range.

Closes #578.
2019-06-26 13:54:32 -07:00
James Lu
37822fda42 inspircd: implement spawn_server() on InspIRCd 3 (#644) 2019-06-26 13:54:25 -07:00
James Lu
4eb0420378 permissions: only whitelist the defined login:user, not all accounts
It's possible for login:user and login:accounts to be used together, although this is discouraged.
2019-06-26 13:18:32 -07:00
James Lu
9a74626d62 relay: consistency fixes for the hideoper setting
- Don't enforce +H on /oper when the hideoper option is disabled
- Skip relaying -H if the hideoper option is enabled - closes #629
2019-06-26 13:18:32 -07:00
James Lu
c1158fd33a exttargets: convert $account target to str before matching
Closes #639.
2019-06-26 13:18:32 -07:00
James Lu
caa94f983f relay: mangle <( to [ and >) to ] for better displays 2019-06-24 15:08:13 -07:00
James Lu
729abbd6bf Update dependency definitions
- Make cachetools a hard dependency - closes #648
- Mark unidecode as an optional dependency for Relay - #561
2019-06-24 15:03:51 -07:00
James Lu
61ca8dd781 clientbot: fix error when MODES is defined in ISUPPORT but given no value 2019-06-23 20:13:04 -07:00
James Lu
df468064d6 clientbot: rework to support freeform nicks
By overriding _get_UID() to only return non-virtual clients, we can stop worrying about nick conflicts and remove relay nick tags from Clientbot.
2019-06-23 19:46:23 -07:00
James Lu
c56713887e classes: use _get_UID in parse_modes() to allow overriding nick lookup behaviour 2019-06-23 19:45:29 -07:00
James Lu
798fc7b0bf match_host: stop implicitly coersing target nicks to UIDs 2019-06-23 19:45:08 -07:00
James Lu
1852ff5774 relay: passthrough nicks in normalize_nick() on server supporting freeform-nicks 2019-06-23 17:48:15 -07:00
James Lu
30f7a77d18 Revert most of 1c0ea24acdd2d1fbba36073cf62907bf0e1a84c3
1c0ea24acdd2d1fbba36073cf62907bf0e1a84c3 "relay_clientbot: normalize sender names to the senders' home networks"

In the future we hope to remove nick restrictions in Clientbot entirely, and just use freeform nicks for virtual users.
2019-06-23 17:48:08 -07:00
James Lu
957697d275 networks: don't allow disconnecting servers marked virtual-server 2019-06-23 17:43:12 -07:00
James Lu
c5b94cdf21 control: ignore virtual servers in rehash 2019-06-23 17:39:15 -07:00
James Lu
f2b6de8889 Declare new protocol capabilities: virtual-server, freeform-nicks 2019-06-23 17:29:43 -07:00
James Lu
ed4404bf4b relay: fake revert mode changes we couldn't bounce (#23)
This allows services to revert mode changes CLAIM was not happy with, instead of causing another mode war during this process.
2019-06-21 15:38:49 -07:00
James Lu
dcab011673 relay: pretend mode reverts on SJOIN always succeed (#23)
This prevents remote services from bypassing CLAIM, since the end result of a mode war is that they remained opped.
2019-06-21 15:28:52 -07:00
James Lu
94cd1d8f22 relay: implement kick/mode/topic war prevention (#23)
This adds cachetools as a dependency for Relay.
2019-06-21 14:57:43 -07:00
James Lu
042d11d7ba relay: remove extraneous variable 2019-06-21 14:03:21 -07:00
James Lu
a6205e1ebc README, setup.py: drop ircmatch dependency (#636) 2019-06-21 12:51:12 -07:00
James Lu
74566c3aab antispam, changehost: remove references to ircmatch (#636) 2019-06-21 12:51:12 -07:00
James Lu
9f31a0a587 classes: drop use of ircmatch (#636) 2019-06-21 12:51:12 -07:00
James Lu
b7d93fe86a utils: add match_text(), general glob matching function
In preparation for ircmatch removal (#636)
2019-06-21 12:51:12 -07:00
James Lu
46d1738f66 example-conf: mention PyLink 2.0.3 instead of 2.1 for CryptContext changes 2019-06-16 11:39:07 -07:00
James Lu
6054476900 More secure password hashing defaults
(cherry picked from commit eba5d912999cd3d5346eb12949f592fa755ebdc5)

  Default hash method to pbkdf2-sha256 & allow customizing CryptContext options

  This introduces a new login::cryptcontext_settings config option.

  Closes #645.
2019-06-16 11:36:34 -07:00
James Lu
c7e4c05cbd changehost: only send a host change if new host != original
(cherry picked from commit 13be40e08bf4ca842b3908ec9b15ee9008ef95b9)
2019-06-16 11:36:34 -07:00
James Lu
e25a5df4db ClientbotBaseProtocol: disallow part() from the main pseudoclient by default 2019-06-16 11:24:45 -07:00
James Lu
0836845ff9 Merge relay showchan/showuser info into commands.py
This makes error handling easier and is needed to support duplicate nicks anyways.
2019-06-16 11:22:36 -07:00
James Lu
dfc4e4954a commands: remove explicit cutoff of 20 users/line in showchan
irc.reply() in PyLink 2.0+ handles line wrapping automatically.
2019-06-16 11:20:26 -07:00
James Lu
fe95a4a571 commands: rework showuser to better handle duplicate nicks
This is freely allowed on Discord, for example.
2019-06-16 11:19:19 -07:00
James Lu
fc4a16eda1 bots, opercmds: handle cases where target nick is disambiguous 2019-06-16 11:04:07 -07:00
James Lu
242267a4a2 classes: revise some function descriptions 2019-06-16 10:31:23 -07:00
James Lu
011d70e816 classes: make nick_to_uid more versatile against duplicate nicks
This adds a couple of new options:
- multi: return all matches for nick instead of just the last result. (Return an empty list if no matches)
- filterfunc: if specified, filter matched users by the given function first."""
2019-06-16 10:30:46 -07:00
James Lu
3b07f8ab2b fantasy: accept "@servicebot" as fantasy trigger prefix
For platforms like Discord where this form of address is the norm.

Closes https://github.com/PyLink/pylink-discord/issues/17
2019-06-15 23:56:55 -07:00
James Lu
3d039b78e2 relay: use [] as altchars for Base64 fallback
This ensures that mangled nicks can be reversed more easily (by removing leading _'s and replacing - with =)
2019-06-15 23:48:46 -07:00
James Lu
886a98a396 Drop official support for Python 3.4
Our lowest support target is now Python 3.5 (Debian 9, Ubuntu 16.04)
2019-06-07 14:34:04 -07:00
James Lu
c62ec4fde0 setup.py: move from expiringdict to cachetools (#445) 2019-06-07 14:31:04 -07:00
James Lu
3d5e7cd1c1 pylink-mkpasswd: use hash() instead of encrypt()
- This function was renamed in Passlib 1.7, deprecating the old name.

- Depend accordingly on Passlib >= 1.7.0
2019-06-07 14:25:22 -07:00
James Lu
eba5d91299 Default hash method to pbkdf2-sha256 & allow customizing CryptContext options
This introduces a new login::cryptcontext_settings config option.

Closes #645.
2019-06-07 14:13:39 -07:00
James Lu
8b298df362 example-conf: various wording tweaks (SSL -> TLS, etc.) 2019-06-06 23:57:01 -07:00
James Lu
42a2061783 Merge branch 'wip/insp3' into devel
protocols/inspircd: add native support for InspIRCd 3.x

Closes #644.
2019-06-06 23:54:57 -07:00
James Lu
dd58dcf377 inspircd: show a note when linking to insp3 servers using insp20 compat 2019-06-06 23:50:08 -07:00
James Lu
04d36e93a1 inspircd: document target_version variable 2019-06-06 23:49:27 -07:00
James Lu
2b04050bf5 inspircd: minor cleanup 2019-05-31 19:01:25 -07:00
James Lu
8d2ae6af50 example-conf: rewrap comments for the first server example 2019-05-31 18:44:07 -07:00
James Lu
762b47120d inspircd: support insp3 INVITE 2019-05-31 18:28:28 -07:00
James Lu
722881bc33 inspircd: fix incorrect lstrip() usage when mangling mode names 2019-05-31 18:13:21 -07:00
James Lu
917543dd12 inspircd: burst shorter version strings on insp3
These get shown in /map, for example.
2019-05-31 18:13:15 -07:00
James Lu
b260a28c8f inspircd: handle insp3 SERVER command 2019-05-31 18:12:06 -07:00
James Lu
12784a4b5b inspircd: handle insp3 IJOIN with TS & flags 2019-05-31 17:46:36 -07:00
James Lu
ea753774fd inspircd: check for local protocol version instead of the remote's
We should be speaking the insp20 protocol even to insp3 servers if configured to do so, not some broken hybrid of the two.
OPERTYPE handling remains an exception.
2019-05-31 17:35:49 -07:00
James Lu
1c0ea24acd relay_clientbot: normalize sender names to the senders' home networks
This should work for most messages, except NICK changes and MODE targets.
2019-05-18 19:44:45 -07:00
James Lu
50e9d2d959 example-conf: load servprotect by default 2019-05-13 17:08:11 +08:00
James Lu
26fa5d38a2 README: update list of optional dependencies (#445) 2019-05-13 17:07:50 +08:00
James Lu
ec379a6e81 servprotect: migrate to cachetools (but leave expiringdict as fallback)
Closes #445.
2019-05-13 16:59:57 +08:00
James Lu
c43d13ef61 inspircd: FTOPIC handling for InspIRCd 3 2019-05-02 18:05:54 -07:00
James Lu
66485ec6a2 inspircd: send SINFO instead of VERSION on 1205 2019-05-02 17:42:45 -07:00
James Lu
3d69b7f4e8 ircs2s_common: fix sending the wrong target in PING 2019-05-02 17:36:42 -07:00
James Lu
ad4cb9561c inspircd: add FJOIN, IJOIN, KICK handling for InspIRCd 3
IJOIN is new. Strip membership IDs from incoming FJOIN and KICK for now.
2019-05-02 17:36:42 -07:00
James Lu
08386a8ef7 inspircd: get rid of MIN_PROTO_VER
We should always check that our remote has a protocol version >= our own.

i.e. support links using PyLink 1202 <-> InspIRCd 1205, PyLink 1205 <-> InspIRCd 1205, but NOT PyLink 1205 <-> InspIRCd 1202
2019-05-02 17:36:42 -07:00
James Lu
db6d5d6d05 inspircd: actually read our DEFAULT_IRCD setting 2019-05-02 17:36:38 -07:00
James Lu
42e1eda51a inspircd: use NUM to send numerics on insp3 2019-05-02 17:06:04 -07:00
James Lu
4276607ee4 inspircd: rework modelist negotiation to support InspIRCd 3.0 2019-05-02 16:52:29 -07:00
James Lu
0fe8a8d51a inspircd: move protocol version check into CAPAB START handler
InspIRCd 3.0 stopped sending the protocol version in CAPAB CAPABILITIES, but it's always available in CAPAB START.
2019-05-02 16:11:53 -07:00
James Lu
6f617cb068 inspircd: allow choosing the target IRCd via "target_version" option 2019-05-02 16:11:21 -07:00
James Lu
44a364df98 Move message tags code from clientbot to ircs2s_common 2019-05-02 15:54:34 -07:00
James Lu
e3d72c43a4 inspircd: move proto_ver constants into the class definition 2019-05-02 15:47:03 -07:00
James Lu
d082495297 launcher: drop experimental tag from -d/--daemonize 2019-05-02 15:41:27 -07:00
James Lu
0273faf933 PyLink 2.1-alpha1 2019-05-02 15:38:08 -07:00
James Lu
81bf6480df clientbot: avoid adding empty nicks to the state
It looks like names replies may end with an extra space, which should not be considered as part of the nick list..

(cherry picked from commit f90b0c857745090ff330fe70421966e11d65a9d6)
2019-04-29 12:19:16 -07:00
James Lu
7e088dfacb clientbot: log the entire args list when splitting /names reply fails
(cherry picked from commit a8bb5f66e53c9d3d3f7872587b2c752ff8a1b16d)
2019-04-29 12:19:16 -07:00
James Lu
f90b0c8577 clientbot: avoid adding empty nicks to the state
It looks like names replies may end with an extra space, which should not be considered as part of the nick list..
2019-04-29 12:04:11 -07:00
James Lu
a8bb5f66e5 clientbot: log the entire args list when splitting /names reply fails 2019-04-29 11:58:18 -07:00
James Lu
ad9a51fc33 commands.showuser: properly handle numeric-type UIDs and channels 2019-04-09 19:10:20 -07:00
James Lu
13be40e08b changehost: only send a host change if new host != original 2019-04-09 19:01:53 -07:00
James Lu
739c87ef50 clientbot: only split /names replies by spaces
This fixes issues with colored hostnames because \x1f is treated as a whitespace char by str.split().

Closes #641.

(cherry picked from commit 905679963331764db17f0bda2dfd7b299d8d2a20)
2019-04-08 22:36:28 -07:00
James Lu
9056799633 clientbot: only split /names replies by spaces
This fixes issues with colored hostnames because \x1f is treated as a whitespace char by str.split().

Closes #641.
2019-04-08 22:35:37 -07:00
James Lu
5ca57cb3c1 Decrease default log file size from 50 MiB to 20 MiB 2019-04-06 02:20:28 -07:00
James Lu
41cbd455d6 relay: only check _invisible flag on actual users 2019-04-02 21:22:40 -07:00
James Lu
8f10af9942 PyLink 2.0.2 2019-03-31 01:33:46 -07:00
Ian Carpenter
28166ed4ee Only write the pid file when --no-pid isn't passed (#633)
Prevents --no-pid from breaking

(cherry picked from commit 30387c9ed5e02205a637f34fa9fc0a7fdcb3f98c)
2019-03-31 01:25:23 -07:00
James Lu
71353a29c2 relay: add support for hiding users marked invisible or offline
First part of https://github.com/PyLink/pylink-discord/issues/19
2019-03-28 20:14:58 -07:00
James Lu
0ffbaa8e5e control: suppress NotImplementedError when disconnecting networks on shutdown 2019-03-28 20:14:28 -07:00
James Lu
2c028e2762 classes: remove channels, modes from User.get_fields()
These don't really make sense to be formatted as a string.
2019-03-28 20:14:04 -07:00
James Lu
63d63c0137 relay: allow trailing .'s in subserver names 2019-03-20 21:14:12 -07:00
James Lu
88f45fb1d5 relay: whitelist ~ in idents 2019-03-20 21:08:55 -07:00
James Lu
c9176a06fc relay: check for nicks starting with numbers or - after removing Unicode 2019-03-20 21:08:21 -07:00
James Lu
1780271dd0 unreal: work around a potential race when sending kills on join 2019-03-01 23:34:41 -08:00
James Lu
190e51211f log: use pylinkirc as logger name 2019-02-20 13:22:26 -08:00
James Lu
4f17a7986b PyLinkNC: don't overwrite sid, serverdata if they're already set 2019-02-20 13:22:26 -08:00
Ian Carpenter
24f85acfb8 Explicitly specify UTF-8 for the log files (#634)
For some reason, Windows defaults to cp1252, which breaks logging when some utf-8 characters get logged
2019-02-18 17:14:30 -08:00
Ian Carpenter
30387c9ed5 Only write the pid file when --no-pid isn't passed (#633)
Prevents --no-pid from breaking
2019-02-18 17:14:04 -08:00
James Lu
ba17821af4 Declare visible-state-only by default in ClientbotBaseProtocol 2019-02-16 16:43:05 -08:00
James Lu
9d459fab92 relay: fixes to support IDs as channel name
- Don't enforce local channel validity rules on the remote channel name
- Normalize channels to str before looking them up in the DB
2019-02-16 16:35:47 -08:00
James Lu
55360dd0b2 classes: allow Server name to be a non-string type 2019-02-16 16:32:42 -08:00
James Lu
96815a0a32 relay: sanitize idents before mirroring them to IRC 2019-02-12 15:55:14 -08:00
James Lu
ac8b7babf1 inspircd: don't allow _ in hosts
CHGHOST on InspIRCd 2.0 does not see this as valid.
2019-02-12 15:55:10 -08:00
James Lu
a9f59307c9 relay: use base64 as fallback if unidecode returns an empty string for nick
This is the case for e.g. nicks that are only an emoji.
2019-02-12 14:17:53 -08:00
James Lu
11b65ee809 relay: rework nick normalization with optional unidecode support
This will attempt to translate UTF-8 nicks to ASCII ones instead of doing the ugly '||||' replace.
Also, the fallback character for disallowed nick characters is now "-" instead of "|".

TODO: document relay::use_unidecode

Closes #561.
2019-02-12 00:58:20 -08:00
James Lu
cfbadb4539 Move _squit, _get_SID, _get_UID wrappers into PyLinkNCWUtils
ClientbotBaseProtocol requires these for the squit wrapper to work.
2019-02-12 00:38:37 -08:00
James Lu
11e7589304 relay_clientbot: fix previous commit 2019-02-10 14:35:34 -08:00
James Lu
76f534ce84 relay_clienbot: fix rpm when UIDs are ints 2019-02-10 14:22:18 -08:00
James Lu
873283e61e clientbot: properly bounce kicks on networks not implementing them 2019-02-10 13:01:31 -08:00
James Lu
a9b8bfe94d classes: allow _expandPUID() to work when UIDs are ints 2019-02-10 13:01:11 -08:00
James Lu
5731a301ce classes: fix incorrect wrap_messages() stub 2019-02-10 13:00:53 -08:00
James Lu
d089b8d40e clientbot: split wrapper stuff into a ClientbotBaseProtocol class
Closes #632.
2019-02-10 12:40:02 -08:00
James Lu
5e1da09901 control: don't remove network objects with virtual_parent on rehash 2019-02-08 15:10:21 -08:00
James Lu
6e7c58ee36 PyLinkNCWithUtils: don't assume mode args are strings 2019-02-07 14:55:27 -08:00
James Lu
61c8677802 classes, relay_clientbot: more type safety for protocols/discord 2019-02-07 13:50:32 -08:00
James Lu
23cb7c173a stats: hide login blocks not relevant to this network 2019-02-07 13:50:32 -08:00
James Lu
52f588c920 Track affected servers in SQUIT hooks 2018-12-27 12:09:40 -08:00
James Lu
82ce9aac6c writing-plugins.md: mention that the default hook priority is 100 2018-12-27 12:08:36 -08:00
James Lu
cb7708e095 Bump VERSION to 2.1-dev 2018-12-27 12:08:17 -08:00
James Lu
852c257e88 relay: remove use of re-entrant locks
This shouldn't be necessary anymore.
2018-12-27 11:58:39 -08:00
James Lu
bf9eb8d4ea relay: fix incorrect in-place changes of modedelta modes
This caused the database to be filled with extraneous "-modename" entries when removing modes from the previous modedelta.
2018-11-30 10:21:51 -08:00
James Lu
61d559926f Bump VERSION to 2.0.2-dev 2018-11-10 23:22:07 -08:00
James Lu
d01a9fe9b4 antispam: more lookalike chars for o, \, # 2018-11-10 23:21:54 -08:00
James Lu
aa5412712a antispam: more lookalike unicode chars
Courtesy of @nathan0
2018-11-10 10:20:41 -08:00
James Lu
32e9cc689e antispam: filter away Unicode lookalike characters when processing
Based off 56b48e4e51
2018-11-07 16:16:46 -08:00
James Lu
6462ae3a3c example-conf: minor rewording for show_unknown_commands
[skip ci]
2018-10-28 21:13:38 -07:00
James Lu
77febfe69f Allow disabling dynamic channels via a new "join_empty_channels" option 2018-10-27 18:48:12 -07:00
James Lu
94a345423e classes: allow callers to override the make_channel_ban() ban style 2018-10-20 12:34:11 -07:00
James Lu
eb231a2aad classes: always raise an error if make_channel_ban creates something invalid 2018-10-20 12:33:42 -07:00
James Lu
31a65697a3 example-conf: document the ban_style option 2018-10-20 12:30:40 -07:00
James Lu
6ceaabe092 classes: use get_fields() in make_channel_ban() for more reliable substitutions 2018-10-20 12:30:09 -07:00
James Lu
5a482118b8 example-conf: describe more thoroughly how antispam handles unsupported punishments 2018-10-20 12:28:41 -07:00
James Lu
5c4fba653f IRCNetwork: disable throttling by default
On large networks, this seems to slows down relay bursts to the point they're no longer usable.
2018-10-10 22:49:10 -07:00
James Lu
dac8410b63 relay: shortcut if the remote network is not ready 2018-10-10 22:49:10 -07:00
James Lu
12d1412cba PyLinkNCWUtils: stop logging the entirety of prefixmodes
This creates a lot of spam on larger channels.
2018-10-10 22:49:04 -07:00
James Lu
60b7894cd6 IRCNetwork: try to abort immediately if the send queue is full 2018-10-08 16:26:29 -07:00
James Lu
0b3793380b relay: remove TCONDITION_TIMEOUT 2018-10-08 14:59:01 -07:00
James Lu
1ee93d2cc4 relay: remove world.started check, this shouldn't be needed anymore 2018-10-08 14:44:37 -07:00
James Lu
767ff15200 relay: add an explicit forcetag command
We used to be able to just /kill to forcetag, but with PyLink 2.0 kills actually get relayed.
2018-10-08 12:42:38 -07:00
James Lu
57faa1443a relay: rename nick_collide() to forcetag_nick() 2018-10-08 12:41:52 -07:00
James Lu
20b3a61cd6 relay: simplify is_relay_client() 2018-10-08 12:12:09 -07:00
James Lu
7fb4c8da04 automode: manage persistent channels on the right network 2018-10-06 23:46:49 -07:00
James Lu
724a71e1b2 PyLink 2.0.1 2018-10-06 00:39:12 -07:00
James Lu
97603fe169 Changelog for PyLink 2.0.1
[skip ci]
2018-10-06 00:35:30 -07:00
James Lu
4d39ad1c84 unreal: bump protocol version to 4200
Corresponding to UnrealIRCd 4.2.0.
2018-10-05 23:57:20 -07:00
James Lu
a3e18081a6 relay: don't relay as text modes being set on netburst (#627) 2018-09-21 21:53:34 -07:00
James Lu
2f4476eb0c unreal: remove invalid comparison
(Regression from commit fac6fe506f9c58c3d96b0f6bea67a04da4a64632)
2018-09-21 21:40:56 -07:00
James Lu
667fa610ec unreal: remove duplicate conversion 2018-09-16 11:29:25 -07:00
James Lu
e929fda293 unreal: Bump protocol version to 4019 (4.0.19-rc1) 2018-09-12 17:35:30 -07:00
James Lu
d5fd7b03f5 unreal: enable slash-in-hosts, it seems to work fine 2018-09-12 17:35:25 -07:00
James Lu
81170e8062 Try to fix Travis deployments
- Use unittest discover instead of 'cd' to run tests
- Disable cleanup before deploy (as our __init__.py is generated at build time)
2018-08-31 10:09:22 -07:00
James Lu
fac6fe506f unreal: use SJOIN in join() to work around weird TS corrupting bugs
This seems to be what Unreal itself does anyways.
2018-08-25 18:50:51 -04:00
James Lu
119873a9ed RELNOTES: fix some mistakes in the 2.0.0 changelog
[skip ci]
2018-08-24 23:46:52 -07:00
James Lu
298913200c clientbot: properly handle cases when the bot is told to kick itself
- Treat kicks to the PyLink client as a real user kick instead of a virtual one
- Forward on kick hooks if the PyLink client is kick target - this allows things like autorejoin to work

Closes #377.
2018-08-25 01:14:26 -04:00
James Lu
79b3387bcb Revert "docs/t: warn in main articles that specifications in master may be outdated"
This reverts commit 58b717a2a0178f283981b04eeb18bb1e34132e85.
2018-08-23 21:31:34 -07:00
James Lu
5838d88404 networks: reload shared modules used by protocol modules too 2018-08-23 02:57:03 -04:00
James Lu
8a096e537c PyLinkNCWUtils: add "ignore_ts_errors" server option to suppress bogus TS warnings 2018-08-23 02:52:54 -04:00
James Lu
dda8a2a081 launcher: rename has_pid variable to pid_exists
This name is a lot more descriptive.
2018-08-19 17:00:14 -07:00
James Lu
874dfaf3f1 launcher: fix PID files not being read if psutil isn't installed 2018-08-19 16:57:57 -07:00
James Lu
49badd1665 updateTS: silently ignore messages with ts = 0
Closes #625.
2018-08-19 19:41:29 -04:00
James Lu
6a2a6a769f docs/adv-relay-config: resync default message formats & add missing line for MODE
[skip ci]
2018-08-19 15:57:21 -07:00
James Lu
e95b11d329 PyLink 2.0.0 2018-07-31 15:04:25 -07:00
James Lu
51f441085e .travis.yml refresh
- Update to xenial
- Remove unused pandoc dependency
- Run our (currently very minimal) test suite
2018-07-28 11:01:01 -07:00
James Lu
c91b5f74ea setup.py: work around installation error on Python 3.4 2018-07-28 10:53:49 -07:00
James Lu
9cf507183d clientbot: filter PART hooks for only parts we didn't initialize
This fixes Relay being confused by its own "Relay plugin unloaded" /part's, and makes Clientbot's behaviour in this regard consistent with other protocols.
2018-07-18 19:14:47 -07:00
James Lu
12f6bb5e18 relay: don't relay kill->kick when remotechan is None 2018-07-18 18:45:49 -07:00
James Lu
e7b0458091 _state_cleanup_core: don't delete internal clients, period 2018-07-18 18:45:34 -07:00
85 changed files with 5224 additions and 2084 deletions

25
.dockerignore Normal file
View File

@ -0,0 +1,25 @@
*.yml
*.yaml
# Git, CI, etc. config files
.*
test/
# Automatically generated by setup.py
/__init__.py
env/
build/
__pycache__/
.idea/
*.py[cod]
*.bak
*~
*#
*.save*
*.db
*.pid
*.pem
.eggs
*.egg-info/
dist/
log/

30
.drone-write-tags.sh Executable file
View File

@ -0,0 +1,30 @@
#!/bin/bash
# Write Docker tags for Drone CI: version-YYMMDD, version, major version, latest
VERSION="$1"
if test -z "$VERSION"; then
echo "Reading version from VERSION file" >&2
VERSION=$(<VERSION)
fi
if [[ "$VERSION" == *"alpha"* || "$VERSION" == *"dev"* ]]; then
# This should never trigger if reference based tagging is enabled
echo "ERROR: Pushing alpha / dev tags is not supported"
exit 1
fi
major_version="$(printf '%s' "$VERSION" | cut -d . -f 1)"
# Date based tag
printf '%s' "$VERSION-$(date +%Y%m%d),"
# Program version
printf '%s' "$VERSION,"
if [[ "$VERSION" == *"beta"* ]]; then
printf '%s' "$major_version-beta,"
printf '%s' "latest-beta"
else # Stable or rc build
printf '%s' "$major_version,"
printf '%s' "latest"
fi

94
.drone.jsonnet Normal file
View File

@ -0,0 +1,94 @@
local test(py_version) = {
"kind": "pipeline",
"type": "docker",
"name": "test-" + py_version,
"steps": [
{
"name": "test",
"image": "python:" + py_version,
"commands": [
"git submodule update --recursive --remote --init",
"pip install -r requirements-docker.txt",
"python3 setup.py install",
"python3 -m unittest discover test/ --verbose"
]
}
]
};
local build_docker(py_version) = {
"kind": "pipeline",
"type": "docker",
"name": "build_docker",
"steps": [
{
"name": "set Docker image tags",
"image": "bash",
"commands": [
"bash .drone-write-tags.sh $DRONE_TAG > .tags",
"# Will build the following tags:",
"cat .tags"
]
},
{
"name": "build Docker image",
"image": "plugins/docker",
"settings": {
"repo": "jlu5/pylink",
"username": {
"from_secret": "docker_user"
},
"password": {
"from_secret": "docker_token"
}
}
}
],
"trigger": {
"event": [
"push"
],
"branch": ["release"],
},
"depends_on": ["test-" + py_version]
};
local deploy_pypi(py_version) = {
"kind": "pipeline",
"type": "docker",
"name": "deploy_pypi",
"steps": [
{
"name": "pypi_publish",
"image": "plugins/pypi",
"settings": {
"username": "__token__",
"password": {
"from_secret": "pypi_token"
}
}
}
],
"trigger": {
"event": [
"tag"
],
"ref": {
"exclude": [
"refs/tags/*alpha*",
"refs/tags/*beta*",
"refs/tags/*dev*"
]
}
},
"depends_on": ["test-" + py_version]
};
[
test("3.7"),
test("3.8"),
test("3.9"),
test("3.10"),
deploy_pypi("3.10"),
build_docker("3.10"),
]

3
.gitignore vendored
View File

@ -3,6 +3,9 @@
!example-*.yml !example-*.yml
!.*.yml !.*.yml
# Generated from .drone.jsonnet
.drone.yml
# Automatically generated by setup.py # Automatically generated by setup.py
/__init__.py /__init__.py

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "test/parser_tests"]
path = test/parser-tests
url = https://github.com/ircdocs/parser-tests

2
.isort.cfg Normal file
View File

@ -0,0 +1,2 @@
[settings]
line_length=100

4
.pylintrc Normal file
View File

@ -0,0 +1,4 @@
[FORMAT]
max-line-length=120
good-names=ip,f,i

View File

@ -1,33 +0,0 @@
dist: trusty
sudo: false
language: python
python:
- '3.4'
- '3.5'
- '3.6'
script: python3 -m compileall .
addons:
apt:
packages:
- pandoc
deploy:
provider: pypi
# Enable this to use test mode
# server: https://testpypi.python.org/pypi
user:
secure: Ql6ihu5MDgWuAvT9NYfriGUYGhHpsqwXfZHWDQT+DfRjOqHo9QT7PnfexeBoe6L6cYUkEnIrnAXKtBXGy6UmyvfrnvBl68877dLVuoC8PfQ4J0ej7TVnCJmT/LwRqFvzZXkeg4CIlJsVJ6pvrPHXQBDPH1rj/rWCucchrofmps8=
password:
secure: JOHSaZDPCImV/TlQ7hqKLzEvxY4/gpYGlZlOvxgFEd/k/sGk13sva1MfQkOh7Fgjblhk/CHt59wVKXa0VaylRugFQnXb+NYNrxYON0IRVsKON20XaLXg7qsyKCS4ml+7cd2KvM8a6LVO9078yLWAhTZkZ69nLIRZwFbmL5+mep4=
on:
tags: true
# Only deploy on tags that don't have -alpha, -beta, etc. suffixes attached
condition: $(python3 -c 'import re,os; print(bool(re.match(r"^(\d+\.){2,}\d+$", os.environ.get("TRAVIS_TAG", ""))))') == "True"
python: '3.6'
notifications:
email: false

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM python:3-alpine
RUN adduser -D -H -u 10000 pylink
VOLUME /pylink
COPY . /pylink-src
RUN cd /pylink-src && pip3 install --no-cache-dir -r requirements-docker.txt
RUN cd /pylink-src && python3 setup.py install
RUN rm -r /pylink-src
USER pylink
WORKDIR /pylink
# Run in no-PID file mode by default
CMD ["pylink", "-n"]

105
README.md
View File

@ -1,37 +1,34 @@
# PyLink IRC Services # PyLink IRC Services
[webchatlink]: https://webchat.overdrivenetworks.com/?channels=PyLink ## END OF LIFE NOTICE: This project is no longer maintained. So long and thanks for all the fish.
<!--
[![Latest stable release](https://img.shields.io/github/v/tag/jlu5/pylink?label=stable&color=1a1)](https://github.com/PyLink/PyLink/tree/master)
[![PyPI version](https://img.shields.io/pypi/v/pylinkirc.svg?maxAge=2592000)](https://pypi.python.org/pypi/pylinkirc/) [![PyPI version](https://img.shields.io/pypi/v/pylinkirc.svg?maxAge=2592000)](https://pypi.python.org/pypi/pylinkirc/)
[![PyPI supported Python versions](https://img.shields.io/pypi/pyversions/pylinkirc.svg?maxAge=2592000)](https://www.python.org/downloads/) [![Docker image version](https://img.shields.io/docker/v/jlu5/pylink/latest?label=docker)](https://hub.docker.com/r/jlu5/pylink)
[![PyPi license](https://img.shields.io/pypi/l/pylinkirc.svg?maxAge=2592000)](LICENSE.MPL2) [![Supported Python versions](https://img.shields.io/badge/python-3.7%20and%20later-50e)](https://www.python.org/downloads/)
[![Live chat](https://img.shields.io/badge/IRC-live%20chat%20%C2%BB-green.svg)][webchatlink] -->
PyLink is an extensible, plugin-based IRC services framework written in Python. It aims to be: PyLink is an extensible, plugin-based IRC services framework written in Python. It aims to be:
1) a replacement for the now-defunct Janus. 1) a transparent server-side relayer between IRC networks.
2) a versatile framework for developing IRC services. 2) a versatile framework for developing IRC services.
PyLink and any bundled software are licensed under the Mozilla Public License, version 2.0 ([LICENSE.MPL2](LICENSE.MPL2)). The corresponding documentation in the [docs/](docs/) folder is licensed under the Creative Attribution-ShareAlike 4.0 International License. ([LICENSE.CC-BY-SA-4.0](LICENSE.CC-BY-SA-4.0)) PyLink is licensed under the Mozilla Public License, version 2.0 ([LICENSE.MPL2](LICENSE.MPL2)). The [corresponding documentation](docs/) is licensed under the Creative Attribution-ShareAlike 4.0 International License. ([LICENSE.CC-BY-SA-4.0](LICENSE.CC-BY-SA-4.0))
## Support the project ## Getting help
[![Donate via PayPal](https://img.shields.io/badge/donate-paypal-50CAF2.svg)](https://www.paypal.me/jlucode)
## Obtaining support
**First, MAKE SURE you've read the [FAQ](docs/faq.md)!** **First, MAKE SURE you've read the [FAQ](docs/faq.md)!**
**When upgrading between major versions, remember to read the [release notes](RELNOTES.md) for any breaking changes!** **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/jlu5/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/PyLink/PyLink/issues). Pull requests are likewise welcome.
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 ## Installation
### Pre-requisites ### Pre-requisites
* CPython 3.4 or above (other intepreters are untested and unsupported) * Python 3.7 or above - prefer the newest Python 3.x when available
* A Unix-like operating system: PyLink is actively developed on Linux only, so we cannot guarantee that things will work properly on other systems. * 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 welcome. If you are a developer and want to help make PyLink more portable, patches are welcome.
@ -42,31 +39,45 @@ If you are a developer and want to help make PyLink more portable, patches are w
* Setuptools (`pip3 install setuptools`) * Setuptools (`pip3 install setuptools`)
* PyYAML (`pip3 install pyyaml`) * PyYAML (`pip3 install pyyaml`)
* ircmatch (`pip3 install ircmatch`) * cachetools (`pip3 install cachetools`)
* *For password encryption*: Passlib (`pip3 install passlib`) * *For hashed password support*: Passlib >= 1.7.0 (`pip3 install passlib`)
* *For better PID file tracking (i.e. removing stale PID files in the case of a crash)*: psutil (`pip3 install psutil`) * *For Unicode support in Relay*: unidecode (`pip3 install Unidecode`)
* *For the servprotect plugin*: expiringdict (`pip3 install expiringdict`) * *For extended PID file tracking (i.e. removing stale PID files after a crash)*: psutil (`pip3 install psutil`)
2) Clone the repository: `git clone https://github.com/jlu5/PyLink && cd PyLink` 2) Clone the repository: `git clone https://github.com/PyLink/PyLink && cd PyLink`
- Previously there was a *devel* branch for testing versions of PyLink - this practice has since been discontinued.
3) Pick your branch. 3) Install PyLink using `python3 setup.py install` (global install) or `python3 setup.py install --user` (local install)
* 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.
* The **devel** branch is where active development goes, and it can be accessed by running `git checkout devel` in your Git tree.
4) Install PyLink using `python3 setup.py install` (global install) or `python3 setup.py install --user` (local install)
* Note: `--user` is a *literal* string; *do not* replace it with your username. * Note: `--user` is a *literal* string; *do not* replace it with your username.
* **Whenever you switch branches or update PyLink's sources via `git pull`, you will need to re-run this command for changes to apply!** * **Whenever you switch branches or update PyLink's sources via `git pull`, you will need to re-run this command for changes to apply!**
### Installing via Docker
As of PyLink 3.0 there is a Docker image available on Docker Hub: [jlu5/pylink](https://hub.docker.com/r/jlu5/pylink)
It supports the following tags:
- Rolling tags: **`latest`** (latest stable/RC release), **`latest-beta`** (latest beta snapshot)
- Pinned to a major branch: e.g. **`3`** (latest 3.x stable release), **`3-beta`** (latest 3.x beta snapshot)
- Pinned to a specific version: e.g. **`3.0.0`**
To use this image you should mount your configuration/DB folder into `/pylink`. **Make sure this directory is writable by UID 10000.**
```bash
$ docker run -v $HOME/pylink:/pylink jlu5/pylink
```
### Installing via PyPI (stable branch only) ### Installing via PyPI (stable branch only)
1) Make sure you're running the right pip command: on most distros, pip for Python 3 uses the command `pip3`. 1) Make sure you're running the right pip command: on most distros, pip for Python 3 uses the command `pip3`.
2) Run `pip3 install pylinkirc` to download and install PyLink. pip will automatically resolve dependencies. 2) Run `pip3 install pylinkirc` to download and install PyLink. pip will automatically resolve dependencies.
3) Download or copy https://github.com/jlu5/PyLink/blob/master/example-conf.yml for an example configuration. 3) Download or copy https://github.com/PyLink/PyLink/blob/master/example-conf.yml for an example configuration.
## Configuration ## Configuration
1) Rename `example-conf.yml` to `pylink.yml` (or a similarly named `.yml` file) and configure your instance there. Note that the configuration format isn't finalized yet - this means that your configuration may break in an update! 1) Rename `example-conf.yml` to `pylink.yml` (or a similarly named `.yml` file) and configure your instance there.
2) Run `pylink` from the command line. PyLink will load its configuration from `pylink.yml` by default, but you can override this by running `pylink` with a config argument (e.g. `pylink mynet.yml`). 2) Run `pylink` from the command line. PyLink will load its configuration from `pylink.yml` by default, but you can override this by running `pylink` with a config argument (e.g. `pylink mynet.yml`).
@ -76,31 +87,39 @@ If you are a developer and want to help make PyLink more portable, patches are w
These IRCds (in alphabetical order) are frequently tested and well supported. If any issues occur, please file a bug on the issue tracker. 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` * [InspIRCd](http://www.inspircd.org/) (2.0 - 3.x) - module `inspircd`
- For KLINE support to work, a `shared{}` block should be added for PyLink on all servers. - Set the `target_version` option to `insp3` to target InspIRCd 3.x (default), or `insp20` to target InspIRCd 2.0 (legacy).
* [InspIRCd](http://www.inspircd.org/) 2.0.x - module `inspircd`
- 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. - 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. - 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` * [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](https://www.unrealircd.org/) (4.2.x - 5.0.x) - 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. - 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.
- 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.
### Extended support ### Extended support
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. 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.
* [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.
* [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.
* [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.
- For use with Relay, the `CloakHostModeX` setting will work fine but `CloakHost` and `CloakUserToNick` are *not* supported.
### Legacy extended support
Support for these IRCds was added at some point but is no longer actively maintained, either due to inactive upstream development or a perceived lack of interest. We recommend migrating to an IRCd in the above two sections.
* [beware-ircd](http://ircd.bircd.org/) (1.6.3) - module `p10` * [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). - 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. - 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. - 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` * [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. - 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` * [IRCd-Hybrid](http://www.ircd-hybrid.org/) (8.2.x / svn trunk) - module `hybrid`
- For host changing support and optimal functionality, a `service{}` block / U-line should be added for PyLink on every IRCd across your network. - 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. - For KLINE support to work, a `shared{}` block should also be added for PyLink on all servers.
@ -109,18 +128,14 @@ Support for these IRCds exist, but are not tested as frequently and thoroughly.
- On ircd-ratbox, all known IPs of users will be shown in `/whois`, even if the client is e.g. a cloaked relay client. If you're paranoid about this, turn off Relay IP forwarding on the ratbox network(s). - On ircd-ratbox, all known IPs of users will be shown in `/whois`, even if the client is e.g. a cloaked relay client. If you're paranoid about this, turn off Relay IP forwarding on the ratbox network(s).
- For KLINE support to work, a `shared{}` block should be added for PyLink on all servers. - 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` * [IRCu](http://coder-com.undernet.org/) (u2.10.12.16+) - module `p10`
- Host changing is not supported. - Host changing (changehost, relay) is not supported.
* [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` * [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. - Outbound host changing (i.e. for the `changehost` plugin) is not supported.
Other TS6 and P10 variations may work, but are not officially supported.
### Clientbot ### Clientbot
Since v1.0, PyLink supports connecting to IRCds as a relay bot and forwarding users back, similar to Janus' Clientbot. This can be useful if the IRCd a network used isn't supported, or if you want to relay certain channels without fully linking with a network. PyLink supports connecting to IRCds as a relay bot and forwarding users back as virtual clients, similar to Janus' Clientbot. This can be useful if the IRCd a network used isn't supported, or if you want to relay certain channels without fully linking with a network.
For Relay to work properly with Clientbot, be sure to load the `relay_clientbot` plugin in conjunction with `relay`. For Relay to work properly with Clientbot, be sure to load the `relay_clientbot` plugin in conjunction with `relay`.
Note: **Clientbot links can only be used as a leaf for Relay links - they CANNOT be used to host channels!** This means that Relay does not support having all your networks be Clientbot - in those cases you are better off using a classic relay bot, like [RelayNext for Limnoria](https://github.com/jlu5/SupyPlugins/tree/master/RelayNext).

View File

@ -1,3 +1,283 @@
# PyLink 3.1.0 (2023-01-03)
This will be my (**@jlu5**'s) last release.
Changes since 3.1-beta1:
### Feature changes
- Allow loading a custom CA certificate via a ssl_cafile option (#677). Thanks to **@paigeadelethompson** for contributing
### Bug fixes
- relay: strip slashes (/) from idents
- raw: fix permission check logic
# PyLink 3.1-beta1 (2021-12-30)
### Feature changes
- **PyLink now requires Python >= 3.7**
- **protocols/inspircd now defaults to InspIRCd 3.x mode** (`target_version=insp3`)
- **Default to system IPv4/IPv6 preference when resolving hostnames.** For existing users, this means that PyLink will most likely **default to IPv6** when resolving hostnames if your server supports it!
- You can override this by setting `ipv6: false` in your server config block or using an explicit `bindhost`. Connections made to IPs directly are not affected.
- **[SECURITY]** exec and raw plugins are now locked behind config options, to disable running arbitrary code by admins
- Implement path configuration for PyLink data files (#659)
### Bug fixes
- Various fixes as detected by pylint, thanks to **@Celelibi** for reporting
- ircs2s_common: fix parsing clash if the sender's nick matches an IRC command (e.g. on UnrealIRCd)
- ircs2s_common: gracefully handle QUIT messages without a reason (seen on Anope / InspIRCd)
- Properly handle EAGAIN in non-blocking sockets
- relay: fix "channel not found" errors on LINK when the remote casemapping differs. This mainly affects channels with "|" and other RFC1459 special cases in their name
- unreal: bounce attempts to CHGIDENT/HOST/NAME services clients
- unreal: fix formatting of outgoing `/kill` (#671)
- opercmds: remove double "Killed by" prefixes in the `kill` command
### Internal improvements
- Added best-effort tracking of user SSL/TLS status (#169)
- Add support for oper notices (GLOBOPS/OPERWALL) (#511)
- relay: better ident sanitizing for IRCd-Hybrid
- Refactored UID generators to be more concise
# PyLink 3.0.0 (2020-04-11)
Changes since 3.0-rc1:
- Added new install instructions via Docker
- global: fix type-safety errors when connected to pylink-discord
- Various code cleanup
For a broader summary of changes since 2.0.x, consult the below release notes for PyLink 3.0-rc1.
# PyLink 3.0-rc1 (2020-02-22)
PyLink 3.0 brings PyLink up to date with the latest IRCds (InspIRCd 3, UnrealIRCd 5), and introduces Discord integration via the [pylink-discord](https://github.com/PyLink/pylink-discord) contrib module. It also improves support for Unicode nicks in Relay and Clientbot.
## Major changes since PyLink 2.0.x
- **Added support for InspIRCd 3 and UnrealIRCd 5.x**:
- To enable InspIRCd 3 link mode (highly recommended) on InspIRCd 3 networks, set the `target_version: insp3` option in the relevant network's `server` block.
- For UnrealIRCd 5 support, no changes in PyLink are necessary.
- **Updated list of required dependencies: added cachetools, removed ircmatch and expiringdict**
- Relay: added an optional dependency on unidecode, to translate Unicode nicks to ASCII on networks not supporting it.
- Also, nick normalization is now skipped on protocols where it is not necessary (Clientbot, Discord)
- Relay now tracks kill/mode/topic clashes and will stop relaying when it detects such a conflict ([issue#23](https://github.com/jlu5/PyLink/issues/23))
- Changehost now supports network-specific configuration ([issue#611](https://github.com/jlu5/PyLink/issues/611)) and listening for services account changes - this allows for consistent account based hostmasks for SASL gateways, etc.
- Clientbot: added an option to continuously rejoin channels the bot is not in. [issue#647](https://github.com/jlu5/PyLink/issues/647)
- Antispam: added optional quit/part message filtering ([issue#617](https://github.com/jlu5/PyLink/issues/617))
### API changes
- **API Break:** Channel, Server, and User keys may now be type `int` - previously, they were always strings.
- **API Break:** PyLink now supports having multiple UIDs mapping to the same nick, as this is common on platforms like Discord.
- `nick_to_uid()` has been reworked to optionally return multiple nicks when `multi=True`
- Using `nick_to_uid()` without this option will raise a warning if duplicate nicks are present, and may be deprecated in the future.
- Added `utils.match_text()`, a general (regex-based) glob matcher to replace `ircmatch` calls ([issue#636](https://github.com/jlu5/PyLink/issues/636)).
- Editing hook payloads is now officially supported in plugin hook handlers ([issue#452](https://github.com/jlu5/PyLink/issues/452))
- Added new protocol module capabilities:
- `can-manage-bot-channels`
- `freeform-nicks`
- `has-irc-modes`
- `virtual-server`
- SQUIT hooks now track a list of affected servers (SIDs) in the `affected_servers` field
This branch was previously known as PyLink 2.1. For more detailed changes between 3.0-rc1 and individual 2.1 snapshots, see their separate changelogs below.
## Changes since PyLink 2.1-beta1
### Feature changes
- Added a Dockerfile for PyLink, thanks to @jameswritescode. Official images will be available on Docker Hub before the final 2.1 release.
- unreal: declare support for usermodes +G (censor) and +Z (secureonlymsg)
- The `version` command now prints the Python interpreter version to assist with debugging
### Bug fixes
- Fix desync when removing multiple ban modes in one MODE command (regression from 2.1-beta1)
- p10: properly ignore ACCOUNT subcommands other than R, M, and U
- Fix extraneous lowercasing of the network name in the `$account` exttarget, causing per-network matches to fail if the network name had capital letters
- inspircd: negotiate casemapping setting on link for InspIRCd 3. [issue#654](https://github.com/jlu5/PyLink/issues/654)
- ircs2s_common: fix crash when failing to extract KILL reason
### Documentation changes
- `relay-quickstart`: describe delinking another network from channels owned by the caller's network
- Refreshed mode list documentation
### Internal improvements
- Debug logs for mode parsers and other miscellaneous internals are now less noisy
- inspircd: warn when using InspIRCd 2 compat mode on an InspIRCd 3 uplink - some commands like KICK are not translated correctly in this mode
- classes: fix `SyntaxWarning: "is" with a literal` on Python 3.8
# PyLink 2.1-beta1 (2019-12-08)
### Feature changes
- **Declare support for UnrealIRCd 5.0.0-rc2+**. Since the S2S protocol has not changed significantly, no protocol changes are needed for this to work.
- UnrealIRCd 5.0.0-rc1 suffers from a [message routing bug](https://bugs.unrealircd.org/view.php?id=5469) and is not supported.
- clientbot: added an option to continuously rejoin channels the bot is not in. [issue#647](https://github.com/jlu5/PyLink/issues/647)
- changehost: added support for network-specific options. [issue#611](https://github.com/jlu5/PyLink/issues/611)
- changehost: listen for services account changes - this allows for consistent account based hostmasks for SASL gateways, etc.
- relay_clientbot: `rpm` now deals with duplicate nicks and nicks including spaces (e.g. on Discord)
- Relay now merges together network specific `clientbot_styles` options with global settings, instead of ignoring the latter if the network specific options are set. [issue#642](https://github.com/jlu5/PyLink/issues/642)
- ts6: add support for hiding PyLink servers (`(H)` in server description)
- commands: various improvements to the `showuser` command
### Bug fixes
- More fixes for InspIRCd 3 support:
- Fix crash on receiving SVSTOPIC on InspIRCd 3
- Fix incorrect handling of PING to PyLink subservers - this caused harmless but confusing "high latency" warnings
- inspircd: revert change disallowing `_` in hosts; this has been enabled by default for quite some time
- Fixed various edge cases in mode handling (+b-b ban cycles, casefolding mode arguments, etc.)
- automode: add better handling for protocols where setting IRC modes isn't possible
- automode: disable on networks where IRC modes aren't supported. [issue#638](https://github.com/jlu5/PyLink/issues/638)
### Internal improvements
- Added test cases for shared protocol code (mode parsers, state checks, etc.)
- Added more IRC parsing tests based off [ircdocs/parser-tests](https://github.com/ircdocs/parser-tests)
- Add `get_service_options` method to merge together global & local network options. [issue#642](https://github.com/jlu5/PyLink/issues/642)
- Enhancements to UnrealIRCd protocol module:
- unreal: read supported user modes on link when available (UnrealIRCd 4.2.3 and later)
- unreal: stop sending NETINFO on link; this suppresses protocol version/network name mismatch warnings
- unreal: declare support for msgbypass and timedban extbans
- Added new protocol capabilities: `has-irc-modes`, `can-manage-bot-channels`
- relay: handle acting extbans for ban exceptions `+e`, which is supported by InspIRCd and UnrealIRCd
# PyLink 2.1-alpha2 (2019-07-14)
**PyLink now requires Python 3.5 or later!**
This release includes all changes from PyLink 2.0.3, plus the following:
### Feature changes
- **Added cachetools as a core runtime dependency** (relay and expiringdict)
- **Added beta support for InspIRCd 3** ([issue#644](https://github.com/jlu5/PyLink/issues/644)):
- The target IRCd version can be configured via a `target_version` server option, which supports `insp20` (InspIRCd 2.0, default) and `insp3`.
- **Removed dependencies on ircmatch** ([issue#636](https://github.com/jlu5/PyLink/issues/636)) **and expiringdict** ([issue#445](https://github.com/jlu5/PyLink/issues/445))
- Increased Passlib requirement to 1.7.0+ to remove deprecated calls
- pylink-mkpasswd: use `hash()` instead of `encrypt()`
- Relay updates:
- Relay now tracks kill/mode/topic clashes and will stop relaying when it detects such a conflict ([issue#23](https://github.com/jlu5/PyLink/issues/23))
- Skip nick normalization on protocols where they aren't actually required (Clientbot, Discord)
- Support `@servicenick` as a fantasy trigger prefix (useful on Discord)
- commands: add a `shownet` command to show server info ([issue#578](https://github.com/jlu5/PyLink/issues/578))
- antispam: added optional quit/part message filtering ([issue#617](https://github.com/jlu5/PyLink/issues/617))
### Bug fixes
- SECURITY: only whitelist permissions for the defined `login:user` if a legacy account is enabled
- clientbot: fix crash when `MODES` is defined in ISUPPORT without a value (affects connections to Oragono)
- relay: fix KILL message formatting (regression from [issue#520](https://github.com/jlu5/PyLink/issues/520))
- relay: consistency fixes when handling hideoper mode changes ([issue#629](https://github.com/jlu5/PyLink/issues/629))
- exttargets: coerse services_account to string before matching ([issue#639](https://github.com/jlu5/PyLink/issues/639))
### Internal improvements
- **API Break:** Reworked `PyLinkNetworkCore.nick_to_uid()` specification to support duplicate nicks and user filtering
- Reworked most plugins (`commands`, `bots`, `opercmds`) to deal with duplicate nicks more robustly
- Revised handling of KILL and QUIT hooks: the `userdata` argument is now always defined
- If we receive both a KILL and a QUIT for any client, only the one received first will be sent as a hook.
- Added new protocol module capabilities: `freeform-nicks`, `virtual-server`
- Added `utils.match_text()`, a general glob matching function to replace ircmatch calls ([issue#636](https://github.com/jlu5/PyLink/issues/636))
- Editing hook payloads is now officially supported in plugin hook handlers ([issue#452](https://github.com/jlu5/PyLink/issues/452))
- ClientbotWrapperProtocol: override `_get_UID()` to only return non-virtual clients, ensuring separate namespaces between internal and external clients.
- ClientbotBaseProtocol: disallow `part()` from the main pseudoclient by default, as this may cause desyncs if not supported
- Moved IRCv3 message tags parser from `clientbot` to `ircs2s_common`
- Merged relay's `showchan`, `showuser` commands into the `commands` plugin, for better tracking of errors and duplicate nicks
# PyLink 2.1-alpha1 (2019-05-02)
This release focuses on internal improvements to better integrate with [pylink-discord](https://github.com/PyLink/pylink-discord). It includes all fixes from 2.0.2, plus the following:
#### Feature changes
- Various Relay improvements:
- Relay now sanitizes UTF-8 nicks and idents where not supported, optionally using the [unidecode](https://github.com/avian2/unidecode) module to decode Unicode names more cleanly to ASCII
- This introduces a new option `relay::use_unidecode` which is enabled by default when the module is installed
- The fallback character to replace invalid nick characters is now `-` instead of `|`
- Add protocol-level support for hiding users in Relay - this is used by pylink-discord to optionally hide invisible/offline users
- Decrease default log file size from 50 MiB to 20 MiB
#### Bug fixes
- changehost: only send a host change if new host != original
- This prevents duplicate host changes on InspIRCd, since it echoes back successful host changes
- clientbot: fix /names parsing errors on networks supporting colors in hosts. [issue#641](https://github.com/jlu5/PyLink/issues/641)
- inspircd: disallow `_` in hosts since CHGHOST does not treat it as valid
- relay: allow trailing .'s in subserver names (e.g. `relay.` is now an accepted server suffix)
- stats: hide login blocks in `/stats O` when not relevant to the caller's network
- unreal: work around a potential race when sending kills on join
#### Internal improvements
- log: use pylinkirc as logger name; this prevents other libraries' debug output from making it to the PyLink log by default
- clientbot: properly bounce kicks on networks not implementing them
- classes: remove channels, modes substitutions from `User.get_fields()`
- various: type-safety fixes to support numeric channel, server, and user IDs (they were previously always strings)
- SQUIT hooks now track a list of affected servers (SIDs) in the `affected_servers` field
- relay: minor optimizations
# PyLink 2.0.3 (2019-10-11)
Changes since 2.0.2:
#### Feature changes
- Switch to more secure password hashing defaults, using pbkdf2-sha256 as the default hash method
- Introduce a `login::cryptcontext_settings` option to further tweak passlib settings if desired
#### Bug fixes
- **SECURITY**: Only allow the defined `login:user` to take all permissions when legacy accounts are enabled
- clientbot: fix /names handling on networks with colours in hostnames
- clientbot: fix crash when MODES is defined in ISUPPORT but given no value (affects connections to Oragono)
- changehost: only send a host change if new host != original
- relay: fix inconsistent handling of the hideoper setting. [issue#629](https://github.com/jlu5/PyLink/issues/629)
- unreal: work around a potential race when sending kills on join
# PyLink 2.0.2 (2019-03-31)
Changes since 2.0.1:
#### Feature changes
- Antispam now supports filtering away Unicode lookalike characters when processing text
- Allow disabling dynamic channels via a new "join_empty_channels" option
- relay: add an explicit `forcetag` command, since IRC kills are now relayed between networks
#### Bug fixes
- launcher: fix crash when --no-pid is set
- relay: fix DB corruption when editing modedelta modes
- automode: fix sending joins to the wrong network when editing remote channels
#### Internal improvements
- relay: minor optimizations and cleanup
- Disable throttling on S2S links by default, since it usually isn't necessary there
# PyLink 2.0.1 (2018-10-06)
Changes since 2.0.0:
#### Feature changes
- Slashes (`/`) in hosts is now supported on UnrealIRCd.
- Added an `ignore_ts_errors` server option to suppress bogus TS warnings.
#### Bug fixes
- clientbot: fix desync when the bot is told to kick itself ([issue#377](https://github.com/jlu5/PyLink/issues/377))
- launcher: fix PID files not being read if psutil isn't installed
- relay_clientbot no longer relays list modes set during a server burst ([issue#627](https://github.com/jlu5/PyLink/issues/627))
- Fixed stray "bogus TS 0" warnings on some UnrealIRCd mode commands
#### Internal improvements
- unreal: bump protocol version to 4200 (UnrealIRCd 4.2.0)
- unreal: use SJOIN in `join()` to work around non-deterministic TS when forwarding to other servers
# PyLink 2.0.0 (2018-07-31)
Changes since 2.0-rc1:
- Fixed various bugs affecting Relay and Clientbot:
- handlers: don't delete the PyLink service UID if it leaves all cahnnels
- Relay sometimes sent an empty channel to kick() when relaying kills to kick
- Fix "Relay plugin unloaded" parts on Clientbot links causing a flood of "Clientbot was force parted" kicks
- setup.py: Work around TypeError in distutils.util.rfc822_escape() when installing on Python 3.4
For a summary of changes since 1.3.x, consult the release notes for PyLink 2.0-rc1 below.
# PyLink 2.0-rc1 (2018-07-18) # PyLink 2.0-rc1 (2018-07-18)
PyLink 2.0 comes with a ton of new features, refinements, and optimizations. Here is a summary of the most interesting changes - for detailed changelogs, consult the release notes for individual snapshots below. PyLink 2.0 comes with a ton of new features, refinements, and optimizations. Here is a summary of the most interesting changes - for detailed changelogs, consult the release notes for individual snapshots below.
@ -65,10 +345,10 @@ This release does *not* preserve compatibility with third-party plugins written
#### Internal improvements #### Internal improvements
- Reading from sockets now uses a select-based backend instead of one thread per network. - Reading from sockets now uses a select-based backend instead of one thread per network.
- Major optimizations to to user tracking that lets PyLink handle Relay networks of 500+ users. - Major optimizations to user tracking that lets PyLink handle Relay networks of 500+ users.
- Service bot handling was completely redone to minimize desyncs when mixing Relay and services. [issue#265](https://github.com/jlu5/PyLink/issues/265)
- 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. - 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`) - `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`)
- Service bot handling was completely redone to minimize desyncs when mixing Relay and services. [issue#265](https://github.com/jlu5/PyLink/issues/265)
### Changes in this RC build ### Changes in this RC build
@ -157,58 +437,58 @@ This release contains all changes from 2.0-alpha3 as well as the following:
This release contains all changes from 1.3.0, as well as the following: This release contains all changes from 1.3.0, as well as the following:
#### New features #### New features
- **Experimental daemonization support via `pylink -d`**. [issue#187](https://github.com/GLolol/PyLink/issues/187) - **Experimental daemonization support via `pylink -d`**. [issue#187](https://github.com/jlu5/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) - 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/jlu5/PyLink/issues/359)
- Clientbot now supports expansions such as `$nick` in autoperform. - 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 now translates STATUSMSG messages (e.g. `@#channel` messages) for target networks instead of passing them on as-is. [issue#570](https://github.com/jlu5/PyLink/issues/570)
- Relay endburst delay on InspIRCd networks is now configurable via the `servers::NETNAME::relay_endburst_delay` option. - 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 - 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. - Added `--trace / -t` options to the launcher for integration with Python's `trace` module.
#### Feature changes #### Feature changes
- **Reverted the commit making SIGHUP shutdown the PyLink daemon**. Now, SIGUSR1 and SIGHUP both trigger a rehash, while SIGTERM triggers a shutdown. - **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 `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/jlu5/PyLink/issues/565)
- The servermaps plugin now uses two permissions for `map` and `localmap`: `servermaps.map` and `servermaps.localmap` respectively - 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 - `showuser` and `showchan` now consistently report times in UTC
#### Bug fixes #### Bug fixes
- protocols/clientbot: fix errors when connecting to networks with mixed-case server names (e.g. AfterNET) - 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) - relay: fix KeyError when a local client is kicked from a claimed channel. [issue#572](https://github.com/jlu5/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) - Fix `irc.parse_modes()` incorrectly mangling modes changes like `+b-b *!*@test.host *!*@test.host` into `+b *!*@test.host`. [issue#573](https://github.com/jlu5/PyLink/issues/573)
- automode: fix handling of channels with multiple \#'s in them - 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. - 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) - 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 - relay_clientbot: fix `STATUSMSG` (`@#channel`) notices from being relayed to channels that it shouldn't
- Fixed various 2.0-alpha2 regressions: - Fixed various 2.0-alpha2 regressions:
- Relay now relays service client messages as PRIVMSG and P10 WALL\* commands as NOTICE - 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) - protocols/inspircd: fix supported modules list being corrupted when an indirectly linked server shuts down. [issue#567](https://github.com/jlu5/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) - networks: `remote` now properly errors if the target service is not available on a network. [issue#554](https://github.com/jlu5/PyLink/issues/554)
- commands: fix `showchan` displaying status prefixes in reverse - commands: fix `showchan` displaying status prefixes in reverse
- stats: route permission error replies to notice instead of PRIVMSG - stats: route permission error replies to notice instead of PRIVMSG
- This prevents "Unknown command" flood loops with services which poll `/stats` on link. - 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) - clientbot: fixed sending duplicate JOIN hooks and AWAY status updates. [issue#551](https://github.com/jlu5/PyLink/issues/551)
#### Internal improvements #### Internal improvements
- **Reading from sockets now uses select instead of one thread per network.** - **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)). - 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) - **API Break: significantly reworked channel handling for service bots**. [issue#265](https://github.com/jlu5/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*. - 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. - 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. - 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()`. - 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.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) - 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/jlu5/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. - 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. - 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`) - `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` - 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) - Rewritten CTCP plugin, now extending to all service bots. [issue#468](https://github.com/jlu5/PyLink/issues/468), [issue#407](https://github.com/jlu5/PyLink/issues/407)
- Relay no longer spams configured U-lines with "message dropped because you aren't in a common channel" errors - 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. - 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: hook handlers can now filter messages from lower-priority handlers by returning `False`. [issue#547](https://github.com/jlu5/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) - New API: added `irc.get_server_option()` to fetch server-specific config variables and global settings as a fallback. [issue#574](https://github.com/jlu5/PyLink/issues/574)
- automode: replace assert checks with proper exceptions - 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) - Renamed methods in log, utils, conf to snake case. [issue#523](https://github.com/jlu5/PyLink/issues/523)
- Remove `structures.DeprecatedAttributesObject`; it's vastly inefficient for what it accomplishes - Remove `structures.DeprecatedAttributesObject`; it's vastly inefficient for what it accomplishes
- clientbot: removed unreliable pre-/WHO join bursting with `userhost-in-names` - 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 - API change: `kick` and `kill` command funcitons now raise `NotImplementedError` when not supported by a protocol
@ -218,45 +498,45 @@ This release contains all changes from 1.3.0, as well as the following:
This release includes all changes from 1.2.2-dev, plus the following: This release includes all changes from 1.2.2-dev, plus the following:
#### New features #### 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) - relay_clientbot: add support for showing prefix modes in relay text, via a new `$mode_prefix` expansion. [issue#540](https://github.com/jlu5/PyLink/issues/540)
- Added new modedelta feature to Relay: - 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. - 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) - 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/jlu5/PyLink/issues/564)
- Added support for more channel modes in Relay: - Added support for more channel modes in Relay:
* blockcaps: inspircd +B, elemental-ircd +G * blockcaps: inspircd +B, elemental-ircd +G
* exemptchanops: inspircd +X * exemptchanops: inspircd +X
* filter: inspircd +g, unreal extban ~T:block ([issue#557](https://github.com/GLolol/PyLink/issues/557)) * filter: inspircd +g, unreal extban ~T:block ([issue#557](https://github.com/jlu5/PyLink/issues/557))
* hidequits: nefarious +Q, snircd +u * hidequits: nefarious +Q, snircd +u
* history: inspircd +H * history: inspircd +H
* largebanlist: ts6 +L * largebanlist: ts6 +L
* noamsg: snircd/nefarious +T * noamsg: snircd/nefarious +T
* blockhighlight: inspircd +V (extras module) * blockhighlight: inspircd +V (extras module)
* kicknorejoin: elemental-ircd +J ([issue#559](https://github.com/GLolol/PyLink/issues/559)) * kicknorejoin: elemental-ircd +J ([issue#559](https://github.com/jlu5/PyLink/issues/559))
* kicknorejoin_insp: inspircd +J (with argument; [issue#559](https://github.com/GLolol/PyLink/issues/559)) * kicknorejoin_insp: inspircd +J (with argument; [issue#559](https://github.com/jlu5/PyLink/issues/559))
* repeat: elemental-ircd +E ([issue#559](https://github.com/GLolol/PyLink/issues/559)) * repeat: elemental-ircd +E ([issue#559](https://github.com/jlu5/PyLink/issues/559))
* repeat_insp: inspircd +K (with argument; [issue#559](https://github.com/GLolol/PyLink/issues/559)) * repeat_insp: inspircd +K (with argument; [issue#559](https://github.com/jlu5/PyLink/issues/559))
- Added support for UnrealIRCd extban `~T` in Relay. [issue#557](https://github.com/GLolol/PyLink/issues/557) - Added support for UnrealIRCd extban `~T` in Relay. [issue#557](https://github.com/jlu5/PyLink/issues/557)
- p10: added proper support for STATUSMSG notices (i.e. messages to `@#channel` and the like) via WALLCHOPS/WALLHOPS/WALLVOICES - 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 - p10: added outgoing /knock support by sending it as a notice
- ts6: added incoming /knock handling - ts6: added incoming /knock handling
- relay: added support for relaying /knock - relay: added support for relaying /knock
#### Backwards incompatible changes #### 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) - **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/jlu5/PyLink/issues/543)
#### Bug fixes #### Bug fixes
- Fix default permissions not applying on startup (2.0-alpha1 regression). [issue#542](https://github.com/GLolol/PyLink/issues/542) - Fix default permissions not applying on startup (2.0-alpha1 regression). [issue#542](https://github.com/jlu5/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). - Fix rejoin-on-kill for the main PyLink bot not working (2.0-alpha1/[94e05a6](https://github.com/jlu5/PyLink/commit/94e05a623314e9b0607de4eb01fab28be2e0c7e1) regression).
- Clientbot fixes: - 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 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/jlu5/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) - Fix desync involving ghost users when a person leaves a channel, changes their nick, and rejoins. [issue#536](https://github.com/jlu5/PyLink/issues/536)
- Treat 0 as "no account" when parsing WHOX responses; this fixes incorrect "X is logged in as 0" output on WHOIS. - 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. - 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) - Fix long standing issues where relay would sometimes burst users multiple times on connect. [issue#529](https://github.com/jlu5/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)) - 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/jlu5/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 `$a:account` extbans being dropped by relay (they were being confused with `$a`). [issue#560](https://github.com/jlu5/PyLink/issues/560)
- Fix corrupt arguments when mixing the `remote` and `mode` commands. [issue#538](https://github.com/GLolol/PyLink/issues/538) - Fix corrupt arguments when mixing the `remote` and `mode` commands. [issue#538](https://github.com/jlu5/PyLink/issues/538)
- Fix lingering queue threads when networks disconnect. [issue#558](https://github.com/GLolol/PyLink/issues/558) - Fix lingering queue threads when networks disconnect. [issue#558](https://github.com/jlu5/PyLink/issues/558)
- The relay and global plugins now better handle empty / poorly formed config blocks. - 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: don't allow `spawnclient` on protocol modules with virtual clients (e.g. clientbot)
- bots: fix KeyError when trying to join previously nonexistent channels - bots: fix KeyError when trying to join previously nonexistent channels
@ -265,10 +545,10 @@ This release includes all changes from 1.2.2-dev, plus the following:
- `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. - `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. - ircs2s_common: add handling for `nick@servername` messages.
- `IRCNetwork` should no longer send multiple disconnect hooks for one disconnection. - `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) - protocols/ts6 no longer requires `SAVE` support from the uplink. [issue#545](https://github.com/jlu5/PyLink/issues/545)
- ts6, hybrid: miscellaneous cleanup - 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) - protocols/inspircd now tracks module (un)loads for `m_chghost.so` and friends. [issue#555](https://github.com/jlu5/PyLink/issues/555)
- Clientbot now logs failed attempts in joining channels. [issue#533](https://github.com/GLolol/PyLink/issues/533) - Clientbot now logs failed attempts in joining channels. [issue#533](https://github.com/jlu5/PyLink/issues/533)
# PyLink 2.0-alpha1 (2017-10-07) # PyLink 2.0-alpha1 (2017-10-07)
The "Eclectic" release. This release includes all changes from 1.2.1, plus the following: The "Eclectic" release. This release includes all changes from 1.2.1, plus the following:
@ -313,7 +593,7 @@ The "Eclectic" release. This release includes all changes from 1.2.1, plus the f
- 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) - 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 #### 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**: 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/jlu5/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**: 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**: 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. - **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.
@ -330,7 +610,7 @@ The "Eclectic" release. This release includes all changes from 1.2.1, plus the f
# PyLink 1.3.0 (2018-05-08) # 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: 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:
- Errors due to missing permissions now log to warning. [issue#593](https://github.com/GLolol/PyLink/issues/593) - Errors due to missing permissions now log to warning. [issue#593](https://github.com/jlu5/PyLink/issues/593)
- Documentation updates to advanced-relay-config.md and the FAQ - Documentation updates to advanced-relay-config.md and the FAQ
# PyLink 1.3-beta1 (2018-04-07) # PyLink 1.3-beta1 (2018-04-07)
@ -338,14 +618,14 @@ The 1.3 update focuses on backporting some commonly requested and useful feature
#### New features #### New features
- **Backported the launcher from 2.0-alpha2**: - **Backported the launcher from 2.0-alpha2**:
- Added support for daemonization via the `--daemon/-d` option. [issue#187](https://github.com/GLolol/PyLink/issues/187) - Added support for daemonization via the `--daemon/-d` option. [issue#187](https://github.com/jlu5/PyLink/issues/187)
- Added support for shutdown/restart/rehash via the command line. [issue#244](https://github.com/GLolol/PyLink/issues/244) - Added support for shutdown/restart/rehash via the command line. [issue#244](https://github.com/jlu5/PyLink/issues/244)
- The launcher now detects and removes stale PID files when `psutil` (an optional dependency) is installed, making restarting from crashes a more streamlined process. [issue#512](https://github.com/GLolol/PyLink/issues/512) - The launcher now detects and removes stale PID files when `psutil` (an optional dependency) is installed, making restarting from crashes a more streamlined process. [issue#512](https://github.com/jlu5/PyLink/issues/512)
- PID file checking is now enabled by default, with the `--check-pid/-c` option retained as a no-op option for compatibility with PyLink <= 1.2 - PID file checking is now enabled by default, with the `--check-pid/-c` option retained as a no-op option for compatibility with PyLink <= 1.2
- Following 2.0 changes, sending SIGUSR1 to the PyLink daemon now triggers a rehash (along with SIGHUP). - Following 2.0 changes, sending SIGUSR1 to the PyLink daemon now triggers a rehash (along with SIGHUP).
- Service bot idents, hosts, and realnames can now be configured globally and on a per-network basis. [issue#281](https://github.com/GLolol/PyLink/issues/281) - Service bot idents, hosts, and realnames can now be configured globally and on a per-network basis. [issue#281](https://github.com/jlu5/PyLink/issues/281)
- Relay server suffix is now configurable by network (`servers::<netname>::relay_server_suffix` option). [issue#462](https://github.com/GLolol/PyLink/issues/462) - Relay server suffix is now configurable by network (`servers::<netname>::relay_server_suffix` option). [issue#462](https://github.com/jlu5/PyLink/issues/462)
- Login blocks can now be restricted to specific networks, opered users, and hostmasks. [issue#502](https://github.com/GLolol/PyLink/issues/502) - Login blocks can now be restricted to specific networks, opered users, and hostmasks. [issue#502](https://github.com/jlu5/PyLink/issues/502)
- Relay now supports relaying more channel modes, including inspircd blockhighlight +V and exemptchanops +X (the whitelist was synced with 2.0-alpha3) - Relay now supports relaying more channel modes, including inspircd blockhighlight +V and exemptchanops +X (the whitelist was synced with 2.0-alpha3)
#### Bug fixes #### Bug fixes
@ -357,7 +637,7 @@ The 1.3 update focuses on backporting some commonly requested and useful feature
- global: ignore empty `global:` configuration blocks - global: ignore empty `global:` configuration blocks
#### Misc changes #### Misc changes
- Config loading now uses `yaml.safe_load()` instead of `yaml.load()` so that arbitrary code cannot be executed. [issue#589](https://github.com/GLolol/PyLink/issues/589) - Config loading now uses `yaml.safe_load()` instead of `yaml.load()` so that arbitrary code cannot be executed. [issue#589](https://github.com/jlu5/PyLink/issues/589)
- Significantly revised example-conf for wording and consistency. - Significantly revised example-conf for wording and consistency.
- protocols/unreal: bumped protocol version to 4017 (no changes needed) - protocols/unreal: bumped protocol version to 4017 (no changes needed)
@ -369,7 +649,7 @@ The "Dancer" release. Changes from 1.2.0:
- Fix wrong database and PID filenames if the config file name includes a period (".") - Fix wrong database and PID filenames if the config file name includes a period (".")
- automode: don't send empty mode lines if no users match the ACL - automode: don't send empty mode lines if no users match the ACL
- networks: check in "remote" that the remote network is actually connected - networks: check in "remote" that the remote network is actually connected
- Fix commonly reported crashes on `logging:` config syntax errors ([49136d5](https://github.com/GLolol/PyLink/commit/49136d5abd609fd5e3ba2ec2e42a0443118e62ab)) - Fix commonly reported crashes on `logging:` config syntax errors ([49136d5](https://github.com/jlu5/PyLink/commit/49136d5abd609fd5e3ba2ec2e42a0443118e62ab))
- Backported fixes from 2.0-dev: - Backported fixes from 2.0-dev:
- p10: fix wrong hook name for user introduction - p10: fix wrong hook name for user introduction
- clientbot: warn when an outgoing message is blocked (e.g. due to bans) (#497) - clientbot: warn when an outgoing message is blocked (e.g. due to bans) (#497)
@ -439,21 +719,21 @@ For a full list of changes since 1.1.x, consult the changelogs for the 1.2.x bet
The "Dynamo" release. This release includes all fixes from 1.1.2, plus the following: The "Dynamo" release. This release includes all fixes from 1.1.2, plus the following:
#### Feature changes #### Feature changes
- Added configurable encoding support via the `encoding` option in server config blocks ([#467](https://github.com/GLolol/PyLink/pull/467)). - Added configurable encoding support via the `encoding` option in server config blocks ([#467](https://github.com/jlu5/PyLink/pull/467)).
- **Certain configuration options were renamed / deprecated:** - **Certain configuration options were renamed / deprecated:**
- The `bot:` configuration block was renamed to `pylink:`, with the old name now deprecated. - The `bot:` configuration block was renamed to `pylink:`, with the old name now deprecated.
- `logging:stdout` is now `logging:console` (the previous name was a misnomer since text actually went to `stderr`). - `logging:stdout` is now `logging:console` (the previous name was a misnomer since text actually went to `stderr`).
- The `bot:prefix` option is deprecated: you should instead define the `prefixes` setting in a separate config block for each service you wish to customize (e.g. set `automode:prefix` and `games:prefix`) - The `bot:prefix` option is deprecated: you should instead define the `prefixes` setting in a separate config block for each service you wish to customize (e.g. set `automode:prefix` and `games:prefix`)
- Added new `$and` and `$network` exttargets - see the new [exttargets documentation page](https://github.com/GLolol/PyLink/blob/1.2-beta1/docs/exttargets.md) for how to use them. - Added new `$and` and `$network` exttargets - see the new [exttargets documentation page](https://github.com/jlu5/PyLink/blob/1.2-beta1/docs/exttargets.md) for how to use them.
- Hostmasks can now be negated in ban matching: e.g. `!*!*@localhost` now works. Previously, this negation was limited to exttargets only. - Hostmasks can now be negated in ban matching: e.g. `!*!*@localhost` now works. Previously, this negation was limited to exttargets only.
- `relay_clientbot` no longer colours network names by default. It is still possible to restore the old behaviour by defining [custom clientbot styles](https://github.com/GLolol/PyLink/blob/1.2-beta1/docs/advanced-relay-config.md#custom-clientbot-styles). - `relay_clientbot` no longer colours network names by default. It is still possible to restore the old behaviour by defining [custom clientbot styles](https://github.com/jlu5/PyLink/blob/1.2-beta1/docs/advanced-relay-config.md#custom-clientbot-styles).
- `relay_clientbot` no longer uses dark blue as a random colour choice, as it is difficult to read on clients with dark backgrounds. - `relay_clientbot` no longer uses dark blue as a random colour choice, as it is difficult to read on clients with dark backgrounds.
#### Bug fixes #### Bug fixes
- Fix service respawn on KILL not working at all - this was likely broken for a while but never noticed... - Fix service respawn on KILL not working at all - this was likely broken for a while but never noticed...
- Fix kick-on-rejoin not working on P10 IRCds when `joinmodes` is set. (a.k.a. acknowledge incoming KICKs with a PART per [the P10 specification](https://github.com/evilnet/nefarious2/blob/ed12d64/doc/p10.txt#L611-L618)) - Fix kick-on-rejoin not working on P10 IRCds when `joinmodes` is set. (a.k.a. acknowledge incoming KICKs with a PART per [the P10 specification](https://github.com/evilnet/nefarious2/blob/ed12d64/doc/p10.txt#L611-L618))
- servprotect: only track kills and saves to PyLink clients, not all kills on a network! - servprotect: only track kills and saves to PyLink clients, not all kills on a network!
- Fix `~#channel` prefix messages not working over relay on RFC1459-casemapping networks ([#464](https://github.com/GLolol/PyLink/issues/464)). - Fix `~#channel` prefix messages not working over relay on RFC1459-casemapping networks ([#464](https://github.com/jlu5/PyLink/issues/464)).
- Show errors when trying to use `showchan` on a secret channel in the same way as actually non-existent channels. Previously this error response forced replies as a private notice, potentially leaking the existence of secret/private channels. - Show errors when trying to use `showchan` on a secret channel in the same way as actually non-existent channels. Previously this error response forced replies as a private notice, potentially leaking the existence of secret/private channels.
- example-conf: fix reversed description for the password encryption setting. - example-conf: fix reversed description for the password encryption setting.
@ -633,7 +913,7 @@ The "Crunchy" release. This release includes all bug fixes from PyLink 1.0.4, al
- Documentation updates: add a permissions reference, document advanced relay config, etc. - Documentation updates: add a permissions reference, document advanced relay config, etc.
# PyLink 1.0.4 # PyLink 1.0.4
Tagged as **1.0.4** by [GLolol](https://github.com/GLolol) Tagged as **1.0.4** by [jlu5](https://github.com/jlu5)
The "Bonfire" release. The "Bonfire" release.
@ -707,8 +987,8 @@ The "Candescent" release.
- exec: Drop `raw` text logging to DEBUG to prevent information leakage (e.g. passwords on Clientbot) - exec: Drop `raw` text logging to DEBUG to prevent information leakage (e.g. passwords on Clientbot)
- Removed `update.sh` (my convenience script for locally building + running PyLink) - Removed `update.sh` (my convenience script for locally building + running PyLink)
# [PyLink 1.0.3](https://github.com/GLolol/PyLink/releases/tag/1.0.3) # [PyLink 1.0.3](https://github.com/jlu5/PyLink/releases/tag/1.0.3)
Tagged as **1.0.3** by [GLolol](https://github.com/GLolol) on 2016-11-20T04:51:11Z Tagged as **1.0.3** by [jlu5](https://github.com/jlu5) on 2016-11-20T04:51:11Z
The "Buoyant" release. The "Buoyant" release.
@ -726,8 +1006,8 @@ The "Buoyant" release.
#### Misc. changes #### Misc. changes
- Various spelling/grammar fixes in the example config. - Various spelling/grammar fixes in the example config.
# [PyLink 1.0.2](https://github.com/GLolol/PyLink/releases/tag/1.0.2) # [PyLink 1.0.2](https://github.com/jlu5/PyLink/releases/tag/1.0.2)
Tagged as **1.0.2** by [GLolol](https://github.com/GLolol) on 2016-10-15T05:52:37Z Tagged as **1.0.2** by [jlu5](https://github.com/jlu5) on 2016-10-15T05:52:37Z
The "Baluga" release. The "Baluga" release.
@ -743,8 +1023,8 @@ The "Baluga" release.
#### Internal fixes / improvements #### Internal fixes / improvements
- setup.py: reworded warnings if `git describe --tags` fails / fallback version is used. Also, the internal VCS version for non-Git builds is now `-nogit` instead of `-dirty`. - setup.py: reworded warnings if `git describe --tags` fails / fallback version is used. Also, the internal VCS version for non-Git builds is now `-nogit` instead of `-dirty`.
# [PyLink 1.0.1](https://github.com/GLolol/PyLink/releases/tag/1.0.1) # [PyLink 1.0.1](https://github.com/jlu5/PyLink/releases/tag/1.0.1)
Tagged as **1.0.1** by [GLolol](https://github.com/GLolol) on 2016-10-06T02:13:42Z Tagged as **1.0.1** by [jlu5](https://github.com/jlu5) on 2016-10-06T02:13:42Z
The "Beam" release. The "Beam" release.
@ -757,8 +1037,8 @@ The "Beam" release.
- relay: clobber colour codes in hosts - relay: clobber colour codes in hosts
- bots: allow JOIN/NICK/QUIT on ServiceBot clients - bots: allow JOIN/NICK/QUIT on ServiceBot clients
# [PyLink 1.0.0](https://github.com/GLolol/PyLink/releases/tag/1.0.0) # [PyLink 1.0.0](https://github.com/jlu5/PyLink/releases/tag/1.0.0)
Tagged as **1.0.0** by [GLolol](https://github.com/GLolol) on 2016-09-17T05:25:51Z Tagged as **1.0.0** by [jlu5](https://github.com/jlu5) on 2016-09-17T05:25:51Z
The "Benevolence" release. The "Benevolence" release.
@ -786,8 +1066,8 @@ The "Benevolence" release.
- Added a debug log example <sup><sup><sup>because nobody knew how to turn it on</sup></sup></sup> - Added a debug log example <sup><sup><sup>because nobody knew how to turn it on</sup></sup></sup>
- Fix inverted option description for Relay's `show_netsplits` option. - Fix inverted option description for Relay's `show_netsplits` option.
# [PyLink 1.0-beta1](https://github.com/GLolol/PyLink/releases/tag/1.0-beta1) # [PyLink 1.0-beta1](https://github.com/jlu5/PyLink/releases/tag/1.0-beta1)
Tagged as **1.0-beta1** by [GLolol](https://github.com/GLolol) on 2016-09-03T07:49:12Z Tagged as **1.0-beta1** by [jlu5](https://github.com/jlu5) on 2016-09-03T07:49:12Z
The "Badgers" release. Note: This is an **beta** build and may not be completely stable! The "Badgers" release. Note: This is an **beta** build and may not be completely stable!
@ -802,7 +1082,7 @@ The "Badgers" release. Note: This is an **beta** build and may not be completely
#### Feature changes #### Feature changes
- Irc: implement basic message queueing (1 message sent per X seconds, where X defaults to 0.01 for servers) . - Irc: implement basic message queueing (1 message sent per X seconds, where X defaults to 0.01 for servers) .
- This appears to also workaround sporadic SSL errors causing disconnects (https://github.com/GLolol/PyLink/issues/246) - This appears to also workaround sporadic SSL errors causing disconnects (https://github.com/jlu5/PyLink/issues/246)
- relay: CLAIM is now more resistant to things like `/OJOIN` abuse<sup><sup><sup>Seriously people, show some respect for your linked networks ;)</sup></sup></sup>. - relay: CLAIM is now more resistant to things like `/OJOIN` abuse<sup><sup><sup>Seriously people, show some respect for your linked networks ;)</sup></sup></sup>.
- core: New permissions system, used exclusively by Automode at this time. See `example-permissions.yml` in the Git tree for configuration options. - core: New permissions system, used exclusively by Automode at this time. See `example-permissions.yml` in the Git tree for configuration options.
- relay_clientbot now optionally supports PMs with users linked via Clientbot. This can be enabled via the `relay::allow_clientbot_pms` option, and provides the following behaviour: - relay_clientbot now optionally supports PMs with users linked via Clientbot. This can be enabled via the `relay::allow_clientbot_pms` option, and provides the following behaviour:
@ -824,8 +1104,8 @@ The "Badgers" release. Note: This is an **beta** build and may not be completely
#### Misc. changes #### Misc. changes
- Various to documentation update and installation instruction improvements. - Various to documentation update and installation instruction improvements.
# [PyLink 0.10-alpha1](https://github.com/GLolol/PyLink/releases/tag/0.10-alpha1) # [PyLink 0.10-alpha1](https://github.com/jlu5/PyLink/releases/tag/0.10-alpha1)
Tagged as **0.10-alpha1** by [GLolol](https://github.com/GLolol) on 2016-08-22T00:04:34Z Tagged as **0.10-alpha1** by [jlu5](https://github.com/jlu5) on 2016-08-22T00:04:34Z
The "Balloons" release. Note: This is an **alpha** build and may not be completely stable! This version includes all fixes from PyLink 0.9.2, with the following additions: The "Balloons" release. Note: This is an **alpha** build and may not be completely stable! This version includes all fixes from PyLink 0.9.2, with the following additions:
@ -869,8 +1149,8 @@ The "Balloons" release. Note: This is an **alpha** build and may not be complete
#### Misc. changes #### Misc. changes
- `FakeIRC` and `FakeProto` are removed (unused and not updated for 0.10 internal APIs) - `FakeIRC` and `FakeProto` are removed (unused and not updated for 0.10 internal APIs)
# [PyLink 0.9.2](https://github.com/GLolol/PyLink/releases/tag/0.9.2) # [PyLink 0.9.2](https://github.com/jlu5/PyLink/releases/tag/0.9.2)
Tagged as **0.9.2** by [GLolol](https://github.com/GLolol) on 2016-08-21T23:59:23Z Tagged as **0.9.2** by [jlu5](https://github.com/jlu5) on 2016-08-21T23:59:23Z
The "Acorn" release. The "Acorn" release.
@ -883,8 +1163,8 @@ The "Acorn" release.
- Relay now normalizes `/` to `.` in hostnames on IRCd-Hybrid. - Relay now normalizes `/` to `.` in hostnames on IRCd-Hybrid.
- Cloaked hosts for UnrealIRCd 3.2 users are now applied instead of the real host being visible. - Cloaked hosts for UnrealIRCd 3.2 users are now applied instead of the real host being visible.
# [PyLink 0.9.1](https://github.com/GLolol/PyLink/releases/tag/0.9.1) # [PyLink 0.9.1](https://github.com/jlu5/PyLink/releases/tag/0.9.1)
Tagged as **0.9.1** by [GLolol](https://github.com/GLolol) on 2016-08-07T03:05:01Z Tagged as **0.9.1** by [jlu5](https://github.com/jlu5) on 2016-08-07T03:05:01Z
### *Important*, backwards incompatible changes for those upgrading from 0.8.x! ### *Important*, backwards incompatible changes for those upgrading from 0.8.x!
- The configuration file is now **pylink.yml** by default, instead of **config.yml**. - The configuration file is now **pylink.yml** by default, instead of **config.yml**.
@ -914,8 +1194,8 @@ Tagged as **0.9.1** by [GLolol](https://github.com/GLolol) on 2016-08-07T03:05:0
#### Misc. changes #### Misc. changes
- Minor example configuration updates, including a mention of passwordless UnrealIRCd links by setting recvpass and sendpass to `*`. - Minor example configuration updates, including a mention of passwordless UnrealIRCd links by setting recvpass and sendpass to `*`.
# [PyLink 0.9.0](https://github.com/GLolol/PyLink/releases/tag/0.9.0) # [PyLink 0.9.0](https://github.com/jlu5/PyLink/releases/tag/0.9.0)
Tagged as **0.9.0** by [GLolol](https://github.com/GLolol) on 2016-07-25T05:49:55Z Tagged as **0.9.0** by [jlu5](https://github.com/jlu5) on 2016-07-25T05:49:55Z
### *Important*, backwards incompatible changes for those upgrading from 0.8.x! ### *Important*, backwards incompatible changes for those upgrading from 0.8.x!
- The configuration file is now **pylink.yml** by default, instead of **config.yml**. - The configuration file is now **pylink.yml** by default, instead of **config.yml**.
@ -946,8 +1226,8 @@ Tagged as **0.9.0** by [GLolol](https://github.com/GLolol) on 2016-07-25T05:49:5
- Redone version handling so `__init__.py` isn't committed anymore. - Redone version handling so `__init__.py` isn't committed anymore.
- `update.sh` now passes arguments to the `pylink` launcher. - `update.sh` now passes arguments to the `pylink` launcher.
# [PyLink 0.9-beta1](https://github.com/GLolol/PyLink/releases/tag/0.9-beta1) # [PyLink 0.9-beta1](https://github.com/jlu5/PyLink/releases/tag/0.9-beta1)
Tagged as **0.9-beta1** by [GLolol](https://github.com/GLolol) on 2016-07-14T02:11:07Z Tagged as **0.9-beta1** by [jlu5](https://github.com/jlu5) on 2016-07-14T02:11:07Z
### *Important*, backwards incompatible changes for those upgrading from 0.8.x ### *Important*, backwards incompatible changes for those upgrading from 0.8.x
- The configuration file is now **pylink.yml** by default, instead of **config.yml**. - The configuration file is now **pylink.yml** by default, instead of **config.yml**.
@ -985,8 +1265,8 @@ Tagged as **0.9-beta1** by [GLolol](https://github.com/GLolol) on 2016-07-14T02:
- Relay now creates relay clones with the current time as nick TS, instead of the origin user's TS. - Relay now creates relay clones with the current time as nick TS, instead of the origin user's TS.
- This has the effect of purposely losing nick collisions against local users, so that it's easier to reclaim nicks. - This has the effect of purposely losing nick collisions against local users, so that it's easier to reclaim nicks.
# [PyLink 0.9-alpha1](https://github.com/GLolol/PyLink/releases/tag/0.9-alpha1) # [PyLink 0.9-alpha1](https://github.com/jlu5/PyLink/releases/tag/0.9-alpha1)
Tagged as **0.9-alpha1** by [GLolol](https://github.com/GLolol) on 2016-07-09T07:27:47Z Tagged as **0.9-alpha1** by [jlu5](https://github.com/jlu5) on 2016-07-09T07:27:47Z
### Summary of changes from 0.8.x ### Summary of changes from 0.8.x
@ -997,7 +1277,7 @@ Tagged as **0.9-alpha1** by [GLolol](https://github.com/GLolol) on 2016-07-09T07
##### Added / changed / removed features ##### Added / changed / removed features
- New **`ctcp`** plugin, handling CTCP VERSION and PING ~~(and perhaps an easter egg?!)~~ - New **`ctcp`** plugin, handling CTCP VERSION and PING ~~(and perhaps an easter egg?!)~~
- New **`automode`** plugin, implementing basic channel ACL by assigning prefix modes like `+o` to hostmasks and exttargets. - New **`automode`** plugin, implementing basic channel ACL by assigning prefix modes like `+o` to hostmasks and exttargets.
- New exttarget support: see https://github.com/GLolol/PyLink/blob/0.9-alpha1/coremods/exttargets.py#L15 for a list of supported ones. - New exttarget support: see https://github.com/jlu5/PyLink/blob/0.9-alpha1/coremods/exttargets.py#L15 for a list of supported ones.
- Relay can now handle messages sent by users not in a target channel (e.g. for channels marked `-n`) - Relay can now handle messages sent by users not in a target channel (e.g. for channels marked `-n`)
- Relay subserver spawning is now always on - the `spawn_servers` option is removed - Relay subserver spawning is now always on - the `spawn_servers` option is removed
- Relay can now optionally show netsplits from remote networks, using a `show_netsplits` option in the `relay:` block - Relay can now optionally show netsplits from remote networks, using a `show_netsplits` option in the `relay:` block
@ -1035,8 +1315,8 @@ Tagged as **0.9-alpha1** by [GLolol](https://github.com/GLolol) on 2016-07-09T07
- protocols/nefarious,ts6,unreal: KILL handling (inbound & outbound) now supports kill paths and formats kill reasons properly - protocols/nefarious,ts6,unreal: KILL handling (inbound & outbound) now supports kill paths and formats kill reasons properly
- protocols: encapsulated (ENCAP) commands are now implicitly expanded, so protocol modules no longer need to bother with IF statement chains in a `handle_encap()` - protocols: encapsulated (ENCAP) commands are now implicitly expanded, so protocol modules no longer need to bother with IF statement chains in a `handle_encap()`
# [PyLink 0.8-alpha4](https://github.com/GLolol/PyLink/releases/tag/0.8-alpha4) # [PyLink 0.8-alpha4](https://github.com/jlu5/PyLink/releases/tag/0.8-alpha4)
Tagged as **0.8-alpha4** by [GLolol](https://github.com/GLolol) on 2016-06-30T18:56:42Z Tagged as **0.8-alpha4** by [jlu5](https://github.com/jlu5) on 2016-06-30T18:56:42Z
Major changes in this snapshot release: Major changes in this snapshot release:
@ -1051,10 +1331,10 @@ Major changes in this snapshot release:
- Example conf: fix various typos (0edb516, cd4bf55) and be more clear about link blocks only being examples - Example conf: fix various typos (0edb516, cd4bf55) and be more clear about link blocks only being examples
- Various freezes and crash bugs fixed (dd08c01, 1ad8b2e, 504a9be, 5f2da1c) - Various freezes and crash bugs fixed (dd08c01, 1ad8b2e, 504a9be, 5f2da1c)
Full diff: https://github.com/GLolol/PyLink/compare/0.8-alpha3...0.8-alpha4 Full diff: https://github.com/jlu5/PyLink/compare/0.8-alpha3...0.8-alpha4
# [PyLink 0.8-alpha3](https://github.com/GLolol/PyLink/releases/tag/0.8-alpha3) # [PyLink 0.8-alpha3](https://github.com/jlu5/PyLink/releases/tag/0.8-alpha3)
Tagged as **0.8-alpha3** by [GLolol](https://github.com/GLolol) on 2016-06-01T02:58:49Z Tagged as **0.8-alpha3** by [jlu5](https://github.com/jlu5) on 2016-06-01T02:58:49Z
- relay: support relaying a few more channel modes (flood, joinflood, freetarget, noforwards, and noinvite) - relay: support relaying a few more channel modes (flood, joinflood, freetarget, noforwards, and noinvite)
- Introduce a new (WIP) API to create simple service bots (#216). - Introduce a new (WIP) API to create simple service bots (#216).
@ -1065,8 +1345,8 @@ Tagged as **0.8-alpha3** by [GLolol](https://github.com/GLolol) on 2016-06-01T02
- New `games` plugin, currently implementing eightball, dice, and fml. - New `games` plugin, currently implementing eightball, dice, and fml.
- Various fixes to the Nefarious protocol module (89ed92b46a4376abf69698b76955fec010a230b4...c82cc9d822ad46f441de3f2f820d5203b6e70516, #209, #210). - Various fixes to the Nefarious protocol module (89ed92b46a4376abf69698b76955fec010a230b4...c82cc9d822ad46f441de3f2f820d5203b6e70516, #209, #210).
# [PyLink 0.8-alpha2](https://github.com/GLolol/PyLink/releases/tag/0.8-alpha2) # [PyLink 0.8-alpha2](https://github.com/jlu5/PyLink/releases/tag/0.8-alpha2)
Tagged as **0.8-alpha2** by [GLolol](https://github.com/GLolol) on 2016-05-08T04:40:17Z Tagged as **0.8-alpha2** by [jlu5](https://github.com/jlu5) on 2016-05-08T04:40:17Z
- protocols/nefarious: fix incorrect decoding of IPv6 addresses (0e0d96e) - protocols/nefarious: fix incorrect decoding of IPv6 addresses (0e0d96e)
- protocols/(hybrid|nefarious): add missing BURST/SJOIN->JOIN hook mappings, fixing problems with relay missing users after a netjoin - protocols/(hybrid|nefarious): add missing BURST/SJOIN->JOIN hook mappings, fixing problems with relay missing users after a netjoin
@ -1076,10 +1356,10 @@ Tagged as **0.8-alpha2** by [GLolol](https://github.com/GLolol) on 2016-05-08T04
- relay: Fix various race conditions, especially when multiple networks happen to lose connection simultaneously - relay: Fix various race conditions, especially when multiple networks happen to lose connection simultaneously
- API changes: many commands from `utils` were split into either `Irc()` or a new `structures` module (#199) - API changes: many commands from `utils` were split into either `Irc()` or a new `structures` module (#199)
[Full diff](https://github.com/GLolol/PyLink/compare/0.8-alpha1...0.8-alpha2) [Full diff](https://github.com/jlu5/PyLink/compare/0.8-alpha1...0.8-alpha2)
# [PyLink 0.8-alpha1](https://github.com/GLolol/PyLink/releases/tag/0.8-alpha1) # [PyLink 0.8-alpha1](https://github.com/jlu5/PyLink/releases/tag/0.8-alpha1)
Tagged as **0.8-alpha1** by [GLolol](https://github.com/GLolol) on 2016-04-23T03:14:21Z Tagged as **0.8-alpha1** by [jlu5](https://github.com/jlu5) on 2016-04-23T03:14:21Z
- New protocol support: IRCd-Hybrid 8.x and Nefarious IRCu - New protocol support: IRCd-Hybrid 8.x and Nefarious IRCu
- Track user IPs of UnrealIRCd 3.2 users (#196) - Track user IPs of UnrealIRCd 3.2 users (#196)
@ -1087,10 +1367,10 @@ Tagged as **0.8-alpha1** by [GLolol](https://github.com/GLolol) on 2016-04-23T03
- Improved mode support for Charybdis (#203) - Improved mode support for Charybdis (#203)
- Fix disconnect logic during ping timeouts - Fix disconnect logic during ping timeouts
[Full diff](https://github.com/GLolol/PyLink/compare/0.7.2-dev...0.8-alpha1) [Full diff](https://github.com/jlu5/PyLink/compare/0.7.2-dev...0.8-alpha1)
# [PyLink 0.7.2-dev](https://github.com/GLolol/PyLink/releases/tag/0.7.2-dev) # [PyLink 0.7.2-dev](https://github.com/jlu5/PyLink/releases/tag/0.7.2-dev)
Tagged as **0.7.2-dev** by [GLolol](https://github.com/GLolol) on 2016-04-19T14:03:50Z Tagged as **0.7.2-dev** by [jlu5](https://github.com/jlu5) on 2016-04-19T14:03:50Z
Bug fix release: Bug fix release:
@ -1184,8 +1464,8 @@ Bug fix release:
- 1d4350c4fd00e7f8012781992ab73a1b73f396d2 classes: provide IrcChannel objects with their own name using KeyedDefaultdict - 1d4350c4fd00e7f8012781992ab73a1b73f396d2 classes: provide IrcChannel objects with their own name using KeyedDefaultdict
- 544d6e10418165415c8ffe2b5fbe59fcffd65b0f utils: add KeyedDefaultdict - 544d6e10418165415c8ffe2b5fbe59fcffd65b0f utils: add KeyedDefaultdict
# [PyLink 0.7.1-dev](https://github.com/GLolol/PyLink/releases/tag/0.7.1-dev) # [PyLink 0.7.1-dev](https://github.com/jlu5/PyLink/releases/tag/0.7.1-dev)
Tagged as **0.7.1-dev** by [GLolol](https://github.com/GLolol) on 2016-03-31T01:42:41Z Tagged as **0.7.1-dev** by [jlu5](https://github.com/jlu5) on 2016-03-31T01:42:41Z
Bugfix release. Lingering errata which you may still encounter: #183. Bugfix release. Lingering errata which you may still encounter: #183.
@ -1198,8 +1478,8 @@ Bugfix release. Lingering errata which you may still encounter: #183.
- 9cd1635f68dafee47f147de43b258014d14da6e2 unreal: fix wrong variable name in handle_umode2 - 9cd1635f68dafee47f147de43b258014d14da6e2 unreal: fix wrong variable name in handle_umode2
- 2169a9be28331c6207865d50912cd671ff3c34a2 utils: actually abort when mode target is invalid - 2169a9be28331c6207865d50912cd671ff3c34a2 utils: actually abort when mode target is invalid
# [PyLink 0.7.0-dev](https://github.com/GLolol/PyLink/releases/tag/0.7.0-dev) # [PyLink 0.7.0-dev](https://github.com/jlu5/PyLink/releases/tag/0.7.0-dev)
Tagged as **0.7.0-dev** by [GLolol](https://github.com/GLolol) on 2016-03-21T19:09:12Z Tagged as **0.7.0-dev** by [jlu5](https://github.com/jlu5) on 2016-03-21T19:09:12Z
### Changes from 0.6.1-dev: ### Changes from 0.6.1-dev:
@ -1252,8 +1532,8 @@ Tagged as **0.7.0-dev** by [GLolol](https://github.com/GLolol) on 2016-03-21T19:
- 4b939ea641284aa9bbb796adc58d273f080e59ee ts6: rewrite end-of-burst code (EOB is literally just a PING in ts6) - 4b939ea641284aa9bbb796adc58d273f080e59ee ts6: rewrite end-of-burst code (EOB is literally just a PING in ts6)
- 5a68dc1bc5f880d1117ca81e729f90fb5e1fce38 Irc: don't call initVars() on IRC object initialization - 5a68dc1bc5f880d1117ca81e729f90fb5e1fce38 Irc: don't call initVars() on IRC object initialization
# [PyLink 0.6.1-dev](https://github.com/GLolol/PyLink/releases/tag/0.6.1-dev) # [PyLink 0.6.1-dev](https://github.com/jlu5/PyLink/releases/tag/0.6.1-dev)
Tagged as **0.6.1-dev** by [GLolol](https://github.com/GLolol) on 2016-03-02T05:15:22Z Tagged as **0.6.1-dev** by [jlu5](https://github.com/jlu5) on 2016-03-02T05:15:22Z
* Bug fix release. * Bug fix release.
- unreal: fix handing of users connecting via IPv4 3c3ae10 - unreal: fix handing of users connecting via IPv4 3c3ae10
@ -1261,8 +1541,8 @@ Tagged as **0.6.1-dev** by [GLolol](https://github.com/GLolol) on 2016-03-02T05:
- inspircd, ts6: don't crash when receiving an unrecognized UID 341c208 - inspircd, ts6: don't crash when receiving an unrecognized UID 341c208
- inspircd: format kill reasons like `Killed (sourcenick (reason))` properly. - inspircd: format kill reasons like `Killed (sourcenick (reason))` properly.
# [PyLink 0.6.0-dev](https://github.com/GLolol/PyLink/releases/tag/0.6.0-dev) # [PyLink 0.6.0-dev](https://github.com/jlu5/PyLink/releases/tag/0.6.0-dev)
Tagged as **0.6.0-dev** by [GLolol](https://github.com/GLolol) on 2016-01-23T18:24:10Z Tagged as **0.6.0-dev** by [jlu5](https://github.com/jlu5) on 2016-01-23T18:24:10Z
Notable changes in this release: Notable changes in this release:
@ -1282,10 +1562,10 @@ Notable changes in this release:
- protocols: allow changing remote users' hosts in updateClient (741fed9). - protocols: allow changing remote users' hosts in updateClient (741fed9).
- Speed up and clean up shutdown sequence, fixing hangs due to sockets not shutting down cleanly (#152). - Speed up and clean up shutdown sequence, fixing hangs due to sockets not shutting down cleanly (#152).
- protocols/unreal: Support cloaking with user mode `+x` (#136). - protocols/unreal: Support cloaking with user mode `+x` (#136).
- Various bug fixes - see https://github.com/GLolol/PyLink/compare/0.5-dev...0.6.0-dev for a full diff. - Various bug fixes - see https://github.com/jlu5/PyLink/compare/0.5-dev...0.6.0-dev for a full diff.
# [PyLink 0.5-dev](https://github.com/GLolol/PyLink/releases/tag/0.5-dev) # [PyLink 0.5-dev](https://github.com/jlu5/PyLink/releases/tag/0.5-dev)
Tagged as **0.5-dev** by [GLolol](https://github.com/GLolol) on 2015-12-06T17:54:02Z Tagged as **0.5-dev** by [jlu5](https://github.com/jlu5) on 2015-12-06T17:54:02Z
The "We're getting somewhere..." release. The "We're getting somewhere..." release.
@ -1311,27 +1591,27 @@ The "We're getting somewhere..." release.
- protocols/unreal: **Add (experimental) support for UnrealIRCd 4.0.x!** - protocols/unreal: **Add (experimental) support for UnrealIRCd 4.0.x!**
- plugins: More complete INFO logging: plugin loading/unloading, unknown commands called, successful operups - plugins: More complete INFO logging: plugin loading/unloading, unknown commands called, successful operups
Full diff:https://github.com/GLolol/PyLink/compare/0.4.6-dev...0.5-dev Full diff:https://github.com/jlu5/PyLink/compare/0.4.6-dev...0.5-dev
# [PyLink 0.4.6-dev](https://github.com/GLolol/PyLink/releases/tag/0.4.6-dev) # [PyLink 0.4.6-dev](https://github.com/jlu5/PyLink/releases/tag/0.4.6-dev)
Tagged as **0.4.6-dev** by [GLolol](https://github.com/GLolol) on 2015-10-01T23:44:20Z Tagged as **0.4.6-dev** by [jlu5](https://github.com/jlu5) on 2015-10-01T23:44:20Z
Bugfix release: Bugfix release:
- f20e6775770b7a118a697c8ae08364d850cdf116 relay: fix PMs across the relay (7d919e6 regression) - f20e6775770b7a118a697c8ae08364d850cdf116 relay: fix PMs across the relay (7d919e6 regression)
- 55d9eb240f037a3378a92ab7661b31011398f565 classes.Irc: prettier __repr__ - 55d9eb240f037a3378a92ab7661b31011398f565 classes.Irc: prettier __repr__
# [PyLink 0.4.5-dev](https://github.com/GLolol/PyLink/releases/tag/0.4.5-dev) # [PyLink 0.4.5-dev](https://github.com/jlu5/PyLink/releases/tag/0.4.5-dev)
Tagged as **0.4.5-dev** by [GLolol](https://github.com/GLolol) on 2015-09-30T04:14:22Z Tagged as **0.4.5-dev** by [jlu5](https://github.com/jlu5) on 2015-09-30T04:14:22Z
The "fancy stuff!" release. The "fancy stuff!" release.
New features including in-place config reloading (rehashing) (#89), FANTASY support (#111), and plugin (re/un)loading without a restart. New features including in-place config reloading (rehashing) (#89), FANTASY support (#111), and plugin (re/un)loading without a restart.
Full diff since 0.4.0-dev: https://github.com/GLolol/PyLink/compare/0.4.0-dev...0.4.5-dev Full diff since 0.4.0-dev: https://github.com/jlu5/PyLink/compare/0.4.0-dev...0.4.5-dev
# [PyLink 0.3.50-dev](https://github.com/GLolol/PyLink/releases/tag/0.3.50-dev) # [PyLink 0.3.50-dev](https://github.com/jlu5/PyLink/releases/tag/0.3.50-dev)
Tagged as **0.3.50-dev** by [GLolol](https://github.com/GLolol) on 2015-09-19T18:28:24Z Tagged as **0.3.50-dev** by [jlu5](https://github.com/jlu5) on 2015-09-19T18:28:24Z
Many updates to core, preparing for an (eventual) 0.4.x release. Commits: Many updates to core, preparing for an (eventual) 0.4.x release. Commits:
@ -1378,10 +1658,10 @@ Many updates to core, preparing for an (eventual) 0.4.x release. Commits:
- 3d621b0 Move checkAuthenticated() to utils, and give it and isOper() toggles for allowing oper/PyLink logins - 3d621b0 Move checkAuthenticated() to utils, and give it and isOper() toggles for allowing oper/PyLink logins
- 090fa85 Move Irc() from main.py to classes.py - 090fa85 Move Irc() from main.py to classes.py
# [PyLink 0.3.1-dev](https://github.com/GLolol/PyLink/releases/tag/0.3.1-dev) # [PyLink 0.3.1-dev](https://github.com/jlu5/PyLink/releases/tag/0.3.1-dev)
Tagged as **0.3.1-dev** by [GLolol](https://github.com/GLolol) on 2015-09-03T06:56:48Z Tagged as **0.3.1-dev** by [jlu5](https://github.com/jlu5) on 2015-09-03T06:56:48Z
Bugfix release + LINKACL support for relay. [Commits since 0.3.0-dev](https://github.com/GLolol/PyLink/compare/0.3.0-dev...0.3.1-dev): Bugfix release + LINKACL support for relay. [Commits since 0.3.0-dev](https://github.com/jlu5/PyLink/compare/0.3.0-dev...0.3.1-dev):
- 043fccf4470bfbc8041056f5dbb694be079a45a5 Fix previous commit (Closes #100) - 043fccf4470bfbc8041056f5dbb694be079a45a5 Fix previous commit (Closes #100)
- 708d94916477f53ddc79a90c4ff321f636c01348 relay: join remote users before sending ours - 708d94916477f53ddc79a90c4ff321f636c01348 relay: join remote users before sending ours
@ -1393,13 +1673,13 @@ Bugfix release + LINKACL support for relay. [Commits since 0.3.0-dev](https://gi
- 3523f8f7663e618829dccfbec6eccfaf0ec87cc5 LINKACL support - 3523f8f7663e618829dccfbec6eccfaf0ec87cc5 LINKACL support
- 51389b96e26224aab262b7b090032d0b745e9590 relay: LINKACL command (Closes #88) - 51389b96e26224aab262b7b090032d0b745e9590 relay: LINKACL command (Closes #88)
# [PyLink 0.2.5-dev](https://github.com/GLolol/PyLink/releases/tag/0.2.5-dev) # [PyLink 0.2.5-dev](https://github.com/jlu5/PyLink/releases/tag/0.2.5-dev)
Tagged as **0.2.5-dev** by [GLolol](https://github.com/GLolol) on 2015-08-16T05:39:34Z Tagged as **0.2.5-dev** by [jlu5](https://github.com/jlu5) on 2015-08-16T05:39:34Z
See the diff for this development build: https://github.com/GLolol/PyLink/compare/0.2.3-dev...0.2.5-dev See the diff for this development build: https://github.com/jlu5/PyLink/compare/0.2.3-dev...0.2.5-dev
# [PyLink 0.2.3-dev](https://github.com/GLolol/PyLink/releases/tag/0.2.3-dev) # [PyLink 0.2.3-dev](https://github.com/jlu5/PyLink/releases/tag/0.2.3-dev)
Tagged as **0.2.3-dev** by [GLolol](https://github.com/GLolol) on 2015-07-26T06:11:20Z Tagged as **0.2.3-dev** by [jlu5](https://github.com/jlu5) on 2015-07-26T06:11:20Z
The "prevent PyLink from wrecking my server's CPU" release. The "prevent PyLink from wrecking my server's CPU" release.
@ -1423,10 +1703,10 @@ Mostly bug fixes here, with a couple of scripts added (`start-cpulimit.sh` and `
- ts6: fix `JOIN` handling and `parse_as` key handling in hooks (ddefd38) - ts6: fix `JOIN` handling and `parse_as` key handling in hooks (ddefd38)
- relay: only wait for `irc.connected` once per network (4d7d7ce) - relay: only wait for `irc.connected` once per network (4d7d7ce)
Full diff: https://github.com/GLolol/PyLink/compare/0.2.2-dev...0.2.3-dev Full diff: https://github.com/jlu5/PyLink/compare/0.2.2-dev...0.2.3-dev
# [PyLink 0.2.2-dev](https://github.com/GLolol/PyLink/releases/tag/0.2.2-dev) # [PyLink 0.2.2-dev](https://github.com/jlu5/PyLink/releases/tag/0.2.2-dev)
Tagged as **0.2.2-dev** by [GLolol](https://github.com/GLolol) on 2015-07-24T18:09:44Z Tagged as **0.2.2-dev** by [jlu5](https://github.com/jlu5) on 2015-07-24T18:09:44Z
The "please don't break again :( " release. The "please don't break again :( " release.
@ -1437,10 +1717,10 @@ The "please don't break again :( " release.
...And of course, lots and lots of bug fixes; I won't bother to list them all. ...And of course, lots and lots of bug fixes; I won't bother to list them all.
Full diff: https://github.com/GLolol/PyLink/compare/0.2.0-dev...0.2.2-dev Full diff: https://github.com/jlu5/PyLink/compare/0.2.0-dev...0.2.2-dev
# [PyLink 0.2.0-dev](https://github.com/GLolol/PyLink/releases/tag/0.2.0-dev) # [PyLink 0.2.0-dev](https://github.com/jlu5/PyLink/releases/tag/0.2.0-dev)
Tagged as **0.2.0-dev** by [GLolol](https://github.com/GLolol) on 2015-07-23T04:44:17Z Tagged as **0.2.0-dev** by [jlu5](https://github.com/jlu5) on 2015-07-23T04:44:17Z
Many changes in this development release, including: Many changes in this development release, including:
@ -1456,10 +1736,10 @@ Many changes in this development release, including:
And of course, many, many bug fixes! (relay should now work properly with more than 2 networks, for example...) And of course, many, many bug fixes! (relay should now work properly with more than 2 networks, for example...)
Full diff: https://github.com/GLolol/PyLink/compare/0.1.6-dev...0.2.0-dev Full diff: https://github.com/jlu5/PyLink/compare/0.1.6-dev...0.2.0-dev
# [PyLink 0.1.6-dev](https://github.com/GLolol/PyLink/releases/tag/0.1.6-dev) # [PyLink 0.1.6-dev](https://github.com/jlu5/PyLink/releases/tag/0.1.6-dev)
Tagged as **0.1.6-dev** by [GLolol](https://github.com/GLolol) on 2015-07-20T06:09:40Z Tagged as **0.1.6-dev** by [jlu5](https://github.com/jlu5) on 2015-07-20T06:09:40Z
### Bug fixes and improvements from 0.1.5-dev ### Bug fixes and improvements from 0.1.5-dev
@ -1471,10 +1751,10 @@ Tagged as **0.1.6-dev** by [GLolol](https://github.com/GLolol) on 2015-07-20T06:
- utils: add `getHostmask()` (1b09a00) - utils: add `getHostmask()` (1b09a00)
- various: Log command usage, `exec` usage, successful logins, and access denied errors in `admin.py`'s commands (57e9bf6) - various: Log command usage, `exec` usage, successful logins, and access denied errors in `admin.py`'s commands (57e9bf6)
Full diff: https://github.com/GLolol/PyLink/compare/0.1.5-dev...0.1.6-dev Full diff: https://github.com/jlu5/PyLink/compare/0.1.5-dev...0.1.6-dev
# [PyLink 0.1.5-dev](https://github.com/GLolol/PyLink/releases/tag/0.1.5-dev) # [PyLink 0.1.5-dev](https://github.com/jlu5/PyLink/releases/tag/0.1.5-dev)
Tagged as **0.1.5-dev** by [GLolol](https://github.com/GLolol) on 2015-07-18T20:01:39Z Tagged as **0.1.5-dev** by [jlu5](https://github.com/jlu5) on 2015-07-18T20:01:39Z
### New features ### New features
@ -1505,10 +1785,10 @@ Tagged as **0.1.5-dev** by [GLolol](https://github.com/GLolol) on 2015-07-18T20:
- commands: remove `debug` command; it's useless now that `exec`, `showchan`, and `showuser` exist (50665ec) - commands: remove `debug` command; it's useless now that `exec`, `showchan`, and `showuser` exist (50665ec)
- admin: `tell` command has been removed. Rationale: limited usefulness; doesn't wrap long messages properly. (4553eda) - admin: `tell` command has been removed. Rationale: limited usefulness; doesn't wrap long messages properly. (4553eda)
You can view the full diff here: https://github.com/GLolol/PyLink/compare/0.1.0-dev...0.1.5-dev You can view the full diff here: https://github.com/jlu5/PyLink/compare/0.1.0-dev...0.1.5-dev
# [PyLink 0.1.0-dev](https://github.com/GLolol/PyLink/releases/tag/0.1.0-dev) # [PyLink 0.1.0-dev](https://github.com/jlu5/PyLink/releases/tag/0.1.0-dev)
Tagged as **0.1.0-dev** by [GLolol](https://github.com/GLolol) on 2015-07-16T06:27:12Z Tagged as **0.1.0-dev** by [jlu5](https://github.com/jlu5) on 2015-07-16T06:27:12Z
PyLink's first pre-alpha development snapshot. PyLink's first pre-alpha development snapshot.

View File

@ -1 +1 @@
2.0-rc1 3.1.0

File diff suppressed because it is too large Load Diff

12
conf.py
View File

@ -10,13 +10,17 @@ try:
except ImportError: except ImportError:
raise ImportError("PyLink requires PyYAML to function; please install it and try again.") raise ImportError("PyLink requires PyYAML to function; please install it and try again.")
import sys
import os.path
import logging import logging
import os.path
import sys
from collections import defaultdict from collections import defaultdict
from . import world from . import world
__all__ = ['ConfigurationError', 'conf', 'confname', 'validate', 'load_conf',
'get_database_name']
class ConfigurationError(RuntimeError): class ConfigurationError(RuntimeError):
"""Error when config conditions aren't met.""" """Error when config conditions aren't met."""
@ -29,7 +33,7 @@ conf = {'bot':
}, },
'logging': 'logging':
{ {
'stdout': 'INFO' 'console': 'INFO'
}, },
'servers': 'servers':
# Wildcard defaultdict! This means that # Wildcard defaultdict! This means that
@ -141,7 +145,7 @@ def get_database_name(dbname):
This returns '<dbname>.db' if the running config name is PyLink's default 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, (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 if this is called from an instance running as 'pylink testing.yml', it
would return '<dbname>-testing.db'.""" would return '<dbname>-testing.db'."""
if confname != 'pylink': if confname != 'pylink':
dbname += '-%s' % confname dbname += '-%s' % confname

View File

@ -1,15 +1,18 @@
""" """
control.py - Implements SHUTDOWN and REHASH functionality. control.py - Implements SHUTDOWN and REHASH functionality.
""" """
import signal
import os
import threading
import sys
import atexit import atexit
import os
import signal
import threading
from pylinkirc import conf, utils, world # Do not import classes, it'll import loop
from pylinkirc.log import _get_console_log_level, _make_file_logger, _stop_file_loggers, log
from . import login
__all__ = ['remove_network', 'shutdown', 'rehash']
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
def remove_network(ircobj): def remove_network(ircobj):
"""Removes a network object from the pool.""" """Removes a network object from the pool."""
@ -71,7 +74,10 @@ def shutdown(irc=None):
for ircobj in world.networkobjects.copy().values(): for ircobj in world.networkobjects.copy().values():
# Disconnect all our networks. # Disconnect all our networks.
try:
remove_network(ircobj) remove_network(ircobj)
except NotImplementedError:
continue
log.info("Waiting for remaining threads to stop; this may take a few seconds. If PyLink freezes " log.info("Waiting for remaining threads to stop; this may take a few seconds. If PyLink freezes "
"at this stage, press Ctrl-C to force a shutdown.") "at this stage, press Ctrl-C to force a shutdown.")
@ -104,10 +110,15 @@ def rehash():
log.debug('rehash: updating console log level') log.debug('rehash: updating console log level')
world.console_handler.setLevel(_get_console_log_level()) world.console_handler.setLevel(_get_console_log_level())
login._make_cryptcontext() # refresh password hashing settings
for network, ircobj in world.networkobjects.copy().items(): for network, ircobj in world.networkobjects.copy().items():
# Server was removed from the config file, disconnect them. # Server was removed from the config file, disconnect them.
log.debug('rehash: checking if %r is in new conf still.', network) log.debug('rehash: checking if %r is still in new conf.', network)
if ircobj.has_cap('virtual-server') or hasattr(ircobj, 'virtual_parent'):
log.debug('rehash: not removing network %r since it is a virtual server.', network)
continue
if network not in new_conf['servers']: if network not in new_conf['servers']:
log.debug('rehash: removing connection to %r (removed from config).', network) log.debug('rehash: removing connection to %r (removed from config).', network)
remove_network(ircobj) remove_network(ircobj)

View File

@ -4,12 +4,14 @@ corecommands.py - Implements core PyLink commands.
import gc import gc
import sys import sys
import importlib
from . import control, login, permissions from pylinkirc import utils, world
from pylinkirc import utils, world, conf
from pylinkirc.log import log from pylinkirc.log import log
from . import control, permissions
__all__ = []
# Essential, core commands go here so that the "commands" plugin with less-important, # Essential, core commands go here so that the "commands" plugin with less-important,
# but still generic functions can be reloaded. # but still generic functions can be reloaded.

View File

@ -5,6 +5,9 @@ exttargets.py - Implements extended targets like $account:xyz, $oper, etc.
from pylinkirc import world from pylinkirc import world
from pylinkirc.log import log from pylinkirc.log import log
__all__ = []
def bind(func): def bind(func):
""" """
Binds an exttarget with the given name. Binds an exttarget with the given name.
@ -41,10 +44,10 @@ def account(irc, host, uid):
homenet, realuid) homenet, realuid)
return False return False
slogin = irc.to_lower(userobj.services_account) slogin = irc.to_lower(str(userobj.services_account))
# Split the given exttarget host into parts, so we know how many to look for. # Split the given exttarget host into parts, so we know how many to look for.
groups = list(map(irc.to_lower, host.split(':'))) groups = host.split(':')
log.debug('(%s) exttargets.account: groups to match: %s', irc.name, groups) log.debug('(%s) exttargets.account: groups to match: %s', irc.name, groups)
if len(groups) == 1: if len(groups) == 1:
@ -52,12 +55,12 @@ def account(irc, host, uid):
return bool(slogin) return bool(slogin)
elif len(groups) == 2: elif len(groups) == 2:
# Second scenario. Return True if the user's account matches the one given. # Second scenario. Return True if the user's account matches the one given.
return slogin == groups[1] and homenet == irc.name return slogin == irc.to_lower(groups[1]) and homenet == irc.name
else: else:
# Third or fourth scenario. If there are more than 3 groups, the rest are ignored. # Third or fourth scenario. If there are more than 3 groups, the rest are ignored.
# In other words: Return True if the user is logged in, the query matches either '*' or the # In other words: Return True if the user is logged in, the query matches either '*' or the
# user's login, and the user is connected on the network requested. # user's login, and the user is connected on the network requested.
return slogin and (groups[1] in ('*', slogin)) and (homenet == groups[2]) return slogin and (irc.to_lower(groups[1]) in ('*', slogin)) and (homenet == groups[2])
@bind @bind
def ircop(irc, host, uid): def ircop(irc, host, uid):
@ -76,8 +79,8 @@ def ircop(irc, host, uid):
# 1st scenario. # 1st scenario.
return irc.is_oper(uid) return irc.is_oper(uid)
else: else:
# 2nd scenario. Use match_host (ircmatch) to match the opertype glob to the opertype. # 2nd scenario. Match the opertype glob to the opertype.
return irc.match_host(groups[1], irc.users[uid].opertype) return irc.match_text(groups[1], irc.users[uid].opertype)
@bind @bind
def server(irc, host, uid): def server(irc, host, uid):
@ -96,7 +99,7 @@ def server(irc, host, uid):
sid = irc.get_server(uid) sid = irc.get_server(uid)
query = groups[1] query = groups[1]
# Return True if the SID matches the query or the server's name glob matches it. # Return True if the SID matches the query or the server's name glob matches it.
return sid == query or irc.match_host(query, irc.get_friendly_name(sid)) return sid == query or irc.match_text(query, irc.get_friendly_name(sid))
# $server alone is invalid. Don't match anything. # $server alone is invalid. Don't match anything.
return False return False
@ -205,7 +208,7 @@ def realname(irc, host, uid):
""" """
groups = host.split(':') groups = host.split(':')
if len(groups) >= 2: if len(groups) >= 2:
return irc.match_host(groups[1], irc.users[uid].realname) return irc.match_text(groups[1], irc.users[uid].realname)
@bind @bind
def service(irc, host, uid): def service(irc, host, uid):
@ -222,5 +225,5 @@ def service(irc, host, uid):
groups = host.split(':') groups = host.split(':')
if len(groups) >= 2: if len(groups) >= 2:
return irc.match_host(groups[1], irc.users[uid].service) return irc.match_text(groups[1], irc.users[uid].service)
return True # It *is* a service bot because of the check at the top. return True # It *is* a service bot because of the check at the top.

View File

@ -3,9 +3,12 @@ handlers.py - Implements miscellaneous IRC command handlers (WHOIS, services log
""" """
import time import time
from pylinkirc import utils, conf from pylinkirc import conf, utils
from pylinkirc.log import log from pylinkirc.log import log
__all__ = []
def handle_whois(irc, source, command, args): def handle_whois(irc, source, command, args):
"""Handle WHOIS queries.""" """Handle WHOIS queries."""
target = args['target'] target = args['target']
@ -17,7 +20,7 @@ def handle_whois(irc, source, command, args):
server = irc.get_server(target) server = irc.get_server(target)
if user is None: # User doesn't exist if user is None: # User doesn't exist
# <- :42X 401 7PYAAAAAB GL- :No such nick/channel # <- :42X 401 7PYAAAAAB jlu5- :No such nick/channel
nick = target nick = target
f(401, source, "%s :No such nick/channel" % nick) f(401, source, "%s :No such nick/channel" % nick)
else: else:
@ -83,7 +86,7 @@ def handle_whois(irc, source, command, args):
n = 'n' if opertype[0].lower() in 'aeiou' else '' n = 'n' if opertype[0].lower() in 'aeiou' else ''
# Remove the "(on $network)" bit in relay oper types if the target network is the # 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 # same - this prevents duplicate text such as "jlu5/ovd is a Network Administrator
# (on OVERdrive-IRC) on OVERdrive-IRC" from showing. # (on OVERdrive-IRC) on OVERdrive-IRC" from showing.
# XXX: does this post-processing really belong here? # XXX: does this post-processing really belong here?
opertype = opertype.replace(' (on %s)' % irc.get_full_network_name(), '') opertype = opertype.replace(' (on %s)' % irc.get_full_network_name(), '')
@ -106,7 +109,7 @@ def handle_whois(irc, source, command, args):
# Show botmode info in WHOIS. # Show botmode info in WHOIS.
f(335, source, "%s :is a bot" % nick) f(335, source, "%s :is a bot" % nick)
# :charybdis.midnight.vpn 317 GL GL 1946 1499867833 :seconds idle, signon time # :charybdis.midnight.vpn 317 jlu5 jlu5 1946 1499867833 :seconds idle, signon time
if irc.get_service_bot(target) and conf.conf['pylink'].get('whois_show_startup_time', True): 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)) f(317, source, "%s 0 %s :seconds idle (placeholder), signon time" % (nick, irc.start_ts))
@ -175,9 +178,9 @@ def _state_cleanup_core(irc, source, channel):
log.debug('(%s) state_cleanup: removing channel %s since we have left', irc.name, channel) log.debug('(%s) state_cleanup: removing channel %s since we have left', irc.name, channel)
del irc._channels[channel] del irc._channels[channel]
# Delete users no longer sharing a channel with us. # Delete external users no longer sharing a channel with us.
if not irc.users[source].channels: if (not irc.users[source].channels) and (not irc.is_internal_client(source)):
log.debug('(%s) state_cleanup: removing user %s/%s who no longer shares a channel with us', log.debug('(%s) state_cleanup: removing external user %s/%s who no longer shares a channel with us',
irc.name, source, irc.users[source].nick) irc.name, source, irc.users[source].nick)
irc._remove_client(source) irc._remove_client(source)

View File

@ -2,21 +2,36 @@
login.py - Implement core login abstraction. login.py - Implement core login abstraction.
""" """
from pylinkirc import conf, utils, world from pylinkirc import conf, utils
from pylinkirc.log import log from pylinkirc.log import log
__all__ = ['pwd_context', 'check_login', 'verify_hash']
# PyLink's global password context
pwd_context = None
_DEFAULT_CRYPTCONTEXT_SETTINGS = {
'schemes': ["pbkdf2_sha256", "sha512_crypt"]
}
def _make_cryptcontext():
try: try:
from passlib.context import CryptContext from passlib.context import CryptContext
except ImportError: except ImportError:
CryptContext = None
log.warning("Hashed passwords are disabled because passlib is not installed. Please install " log.warning("Hashed passwords are disabled because passlib is not installed. Please install "
"it (pip3 install passlib) and restart for this feature to work.") "it (pip3 install passlib) and rehash for this feature to work.")
return
pwd_context = None context_settings = conf.conf.get('login', {}).get('cryptcontext_settings') or _DEFAULT_CRYPTCONTEXT_SETTINGS
if CryptContext: global pwd_context
pwd_context = CryptContext(["sha512_crypt", "sha256_crypt"], if pwd_context is None:
sha256_crypt__default_rounds=180000, log.debug("Initialized new CryptContext with settings: %s", context_settings)
sha512_crypt__default_rounds=90000) pwd_context = CryptContext(**context_settings)
else:
log.debug("Updated CryptContext with settings: %s", context_settings)
pwd_context.update(**context_settings)
_make_cryptcontext() # This runs at startup and in rehash (control.py)
def _get_account(accountname): def _get_account(accountname):
""" """

View File

@ -3,13 +3,16 @@ permissions.py - Permissions Abstraction for PyLink IRC Services.
""" """
from collections import defaultdict from collections import defaultdict
import threading
from pylinkirc import conf, utils
from pylinkirc.log import log
__all__ = ['default_permissions', 'add_default_permissions',
'remove_default_permissions', 'check_permissions']
# Global variables: these store mappings of hostmasks/exttargets to lists of permissions each target has. # Global variables: these store mappings of hostmasks/exttargets to lists of permissions each target has.
default_permissions = defaultdict(set) default_permissions = defaultdict(set)
from pylinkirc import conf, utils
from pylinkirc.log import log
def add_default_permissions(perms): def add_default_permissions(perms):
"""Adds default permissions to the index.""" """Adds default permissions to the index."""
@ -32,7 +35,8 @@ def check_permissions(irc, uid, perms, also_show=[]):
""" """
# For old (< 1.1 login blocks): # For old (< 1.1 login blocks):
# If the user is logged in, they automatically have all permissions. # If the user is logged in, they automatically have all permissions.
if irc.match_host('$pylinkacc', uid) and conf.conf['login'].get('user'): olduser = conf.conf['login'].get('user')
if olduser and irc.match_host('$pylinkacc:%s' % olduser, uid):
log.debug('permissions: overriding permissions check for old-style admin user %s', log.debug('permissions: overriding permissions check for old-style admin user %s',
irc.get_hostmask(uid)) irc.get_hostmask(uid))
return True return True

View File

@ -2,9 +2,12 @@
service_support.py - Implements handlers for the PyLink ServiceBot API. service_support.py - Implements handlers for the PyLink ServiceBot API.
""" """
from pylinkirc import utils, world, conf from pylinkirc import conf, utils, world
from pylinkirc.log import log from pylinkirc.log import log
__all__ = []
def spawn_service(irc, source, command, args): def spawn_service(irc, source, command, args):
"""Handles new service bot introductions.""" """Handles new service bot introductions."""
@ -141,6 +144,8 @@ def _services_dynamic_part(irc, channel):
if irc.has_cap('visible-state-only'): if irc.has_cap('visible-state-only'):
# No-op on bot-only servers. # No-op on bot-only servers.
return return
if irc.serverdata.get('join_empty_channels', conf.conf['pylink'].get('join_empty_channels', False)):
return
# If all remaining users in the channel are service bots, make them all part. # 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): if all(irc.get_service_bot(u) for u in irc.channels[channel].users):

View File

@ -14,9 +14,9 @@ a:
### Custom Clientbot Styles ### 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>`. 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). 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 PyLink 2.1, you can also override options per network by defining them in the form `servers::<network name>::relay_clientbot_styles::<event names>`
See below for a list of supported events and their default values (as of 2.0-beta1). See below for a list of supported events and their default values (as of PyLink 2.1).
A common use case for this feature is to turn off or adjust colors/formatting; this is explicitly documented [below](#disabling-colorscontrol-codes). A common use case for this feature is to turn off or adjust colors/formatting; this is explicitly documented [below](#disabling-colorscontrol-codes).
@ -44,16 +44,17 @@ To disable relaying for any specific event, set the template string to an empty
|Event name|Default value| |Event name|Default value|
| :---: | :--- | | :---: | :--- |
MESSAGE | \x02[$colored\_netname]\x02 <$colored\_sender> $text MESSAGE | \x02[$netname]\x02 <$colored\_sender> $text
KICK | \x02[$colored\_netname]\x02 - $colored_sender$sender\_identhost has kicked $target_nick from $channel ($text) KICK | \x02[$netname]\x02 - $colored_sender$sender\_identhost has kicked $target_nick from $channel ($text)
PART | \x02[$colored\_netname]\x02 - $colored_sender$sender\_identhost has left $channel ($text) PART | \x02[$netname]\x02 - $colored_sender$sender\_identhost has left $channel ($text)
JOIN | \x02[$colored\_netname]\x02 - $colored_sender$sender\_identhost has joined $channel JOIN | \x02[$netname]\x02 - $colored_sender$sender\_identhost has joined $channel
NICK | \x02[$colored\_netname]\x02 - $colored_sender$sender\_identhost is now known as $newnick NICK | \x02[$netname]\x02 - $colored_sender$sender\_identhost is now known as $newnick
QUIT | \x02[$colored\_netname]\x02 - $colored_sender$sender\_identhost has quit ($text) QUIT | \x02[$netname]\x02 - $colored_sender$sender\_identhost has quit ($text)
ACTION | \x02[$colored\_netname]\x02 * $colored\_sender $text ACTION | \x02[$netname]\x02 * $colored\_sender $text
NOTICE | \x02[$colored\_netname]\x02 - Notice from $colored\_sender: $text NOTICE | \x02[$netname]\x02 - Notice from $colored\_sender: $text
SQUIT | \x02[$colored\_netname]\x02 - Netsplit lost users: $colored\_nicks SQUIT | \x02[$netname]\x02 - Netsplit lost users: $colored\_nicks
SJOIN | \x02[$colored\_netname]\x02 - Netjoin gained 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 PM | PM from $sender on $netname: $text
PNOTICE | <$sender> $text PNOTICE | <$sender> $text
@ -75,6 +76,7 @@ This is a example clientbot_styles config block, which you can copy *into* your
JOIN: "[$netname] - $sender$sender_identhost has joined $channel" JOIN: "[$netname] - $sender$sender_identhost has joined $channel"
KICK: "[$netname] - $sender$sender_identhost has kicked $target_nick from $channel ($text)" KICK: "[$netname] - $sender$sender_identhost has kicked $target_nick from $channel ($text)"
MESSAGE: "[$netname] <$sender> $text" MESSAGE: "[$netname] <$sender> $text"
MODE: "[$netname] - $sender$sender_identhost sets mode $modes on $channel"
NICK: "[$netname] - $sender$sender_identhost is now known as $newnick" NICK: "[$netname] - $sender$sender_identhost is now known as $newnick"
NOTICE: "[$netname] - Notice from $sender: $text" NOTICE: "[$netname] - Notice from $sender: $text"
PART: "[$netname] - $sender$sender_identhost has left $channel ($text)" PART: "[$netname] - $sender$sender_identhost has left $channel ($text)"

View File

@ -1,5 +1,5 @@
This folder contains tables of named modes defined by PyLink modules. The following are HTML versions of the raw .csv data: 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 channel modes](https://raw.githack.com/jlu5/PyLink/devel/docs/modelists/channel-modes.html)
- [Supported named user modes](https://raw.githack.com/GLolol/PyLink/devel/docs/modelists/user-modes.html) - [Supported named user modes](https://raw.githack.com/jlu5/PyLink/devel/docs/modelists/user-modes.html)
- [Supported extbans](https://raw.githack.com/GLolol/PyLink/devel/docs/modelists/extbans.html) - [Supported extbans](https://raw.githack.com/jlu5/PyLink/devel/docs/modelists/extbans.html)

View File

@ -1,68 +1,71 @@
Channel Mode / IRCd,rfc1459,hybrid,inspircd,ngircd,p10/ircu,p10/nefarious,p10/snircd,ts6/charybdis,ts6/chatircd,ts6/elemental,ts6/ratbox,unreal Channel Mode / IRCd,rfc1459,hybrid,inspircd/insp20,inspircd/insp3,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 admin,,,"a (m_customprefix, m_chanprotect)",a (customprefix),a,,,,,a (when enabled),a (when enabled),,a
adminonly,,,,,,a,,A (ext/chm_adminonly),A (ext/chm_adminonly),A (ext/chm_adminonly.so),, adminonly,,,,,,,a,,A (ext/chm_adminonly),A (ext/chm_adminonly),A (ext/chm_adminonly.so),,
allowinvite,,,A (m_allowinvite),,,,,g,g,g,, allowinvite,,,A (m_allowinvite),A (allowinvite),,,,,g,g,g,,
auditorium,,,u (m_auditorium),,,,,,,,, auditorium,,,u (m_auditorium),u (auditorium),,,,,,,,,
autoop,,,w (m_autoop),,,,,,,,, autoop,,,w (m_autoop),w (autoop),,,,,,,,,
ban,b,b,b,b,b,b,b,b,b,b,b,b ban,b,b,b,b,b,b,b,b,b,b,b,b,b
banexception,,e,e (m_banexception),e,,e,,e,e,e,e,e banexception,,e,e (m_banexception),e (banexception),e,,e,,e,e,e,e,e
blockcaps,,,B (m_blockcaps),,,,,,,G (ext/chm_nocaps.so),, blockcaps,,,B (m_blockcaps),"B (anticaps, blockcaps)",,,,,,,G (ext/chm_nocaps.so),,
blockcolor,,c,c (m_blockcolor),,c,c,c,,,,,c blockcolor,,c,c (m_blockcolor),c (blockcolor),,c,c,c,,,,,c (chanmodes/nocolor)
blockhighlight,,,V (extras/m_blockhighlight),,,,,,,,, blockhighlight,,,V (contrib/m_blockhighlight),V (contrib/blockhighlight),,,,,,,,,
delayjoin,,,D (m_delayjoin),,D,D,D,,,,,D censor,,,G (m_censor),G (censor),,,,,,,,,G (chanmodes/censor)
exemptchanops,,,X (m_exemptchanops),,,,,,,,, delayjoin,,,D (m_delayjoin),D (delayjoin),,D,D,D,,,,,D (chanmodes/delayjoin)
filter,,,g (m_filter),,,,,,,,, delaymsg,,,d (m_delaymsg),d (delaymsg),,,,,,,,,
flood,,,f (m_messageflood),,,,,,,,, exemptchanops,,,X (m_exemptchanops),X (exemptchanops),,,,,,,,,
flood_unreal,,,,,,,,,,,,f filter,,,g (m_filter),g (filter),,,,,,,,,(via extban ~T:block:)
freetarget,,,,,,,,F,F,F,, flood,,,f (m_messageflood),f (messageflood),,,,,,,,,
had_delayjoin,,,,,d,d,d,,,,, flood_unreal,,,,,,,,,,,,,f (chanmodes/floodprot)
halfop,,h,"h (m_customprefix, m_halfop)",h,,,,,h (when enabled),h (when enabled),,h freetarget,,,,,,,,,F,F,F,,
hiddenbans,,,,,,,,,,u,, had_delayjoin,,,,,,d,d,d,,,,,
hidequits,,,,,,Q,u,,,,, halfop,,h,"h (m_customprefix, m_halfop)",h (customprefix),h,,,,,h (when enabled),h (when enabled),,h
history,,,H (m_chanhistory),,,,,,,,, hiddenbans,,,,,,,,,,,u,,
invex,,I,I (m_inviteexception),I,,,,I,I,I,I,I hidequits,,,,,,,Q,u,,,,,
inviteonly,i,i,i,i,i,i,i,i,i,i,i,i history,,,H (m_chanhistory),H (chanhistory),,,,,,,,,
issecure,,,,,,,,,,,,Z invex,,I,I (m_inviteexception),I (inviteexception),I,,,,I,I,I,I,I
joinflood,,,j (m_joinflood),,,,,j,j,j,, inviteonly,i,i,i,i,i,i,i,i,i,i,i,i,i
key,k,k,k,k,k,k,k,k,k,k,k,k issecure,,,,,,,,,,,,,Z (chanmodes/issecure)
kicknorejoin,,,,,,,,,,J,, joinflood,,,j (m_joinflood),j (joinflood),,,,,j,j,j,,
kicknorejoin_insp,,,J (m_kicknorejoin),,,,,,,,, key,k,k,k,k,k,k,k,k,k,k,k,k,k
largebanlist,,,,,,,,L,L,L,, kicknorejoin,,,,,,,,,,,J,,
limit,l,l,l,l,l,l,l,l,l,l,l,l kicknorejoin_insp,,,J (m_kicknorejoin),J (kicknorejoin),,,,,,,,,
moderated,m,m,m,m,m,m,m,m,m,m,m,m largebanlist,,,,,,,,,L,L,L,,
netadminonly,,,,,,,,,N (ext/chm_netadminonly),,, limit,l,l,l,l,l,l,l,l,l,l,l,l,l
nickflood,,,F (m_nickflood),,,,,,,,, moderated,m,m,m,m,m,m,m,m,m,m,m,m,m
noamsg,,,,,,T,T,,,,, netadminonly,,,,,,,,,,N (ext/chm_netadminonly),,,
noctcp,,C,C (m_noctcp),,C,C,C,C,C,C,,C nickflood,,,F (m_nickflood),F (nickflood),,,,,,,,,
noextmsg,n,n,n,n,,n,,n,n,n,n,n noamsg,,,,,,,T,T,,,,,
noforwards,,,,,,,,Q,Q,Q,, noctcp,,C,C (m_noctcp),C (noctcp),,C,C,C,C,C,C,,C (chanmodes/noctcp)
noinvite,,,,V,,,,,,,,V noextmsg,n,n,n,n,n,n,n,n,n,n,n,n,n
nokick,,,Q (m_nokicks),Q,,,,,,E,,Q noforwards,,,,,,,,,Q,Q,Q,,
noknock,,p*,K (m_knock),,,,,p*,p*,p*,p*,K noinvite,,,,,V,,,,,,,,V (chanmodes/noinvite)
nonick,,,N (m_nonicks),N,,,,,,d,,N nokick,,,Q (m_nokicks),Q (nokicks),Q,,,,,,E,,Q (chanmodes/nokick)
nonotice,,,T (m_nonotice),,,N,N,T (ext/chm_nonotice),T (ext/chm_nonotice),T,,T noknock,,p*,K (m_knock),K (knock),,,,,p*,p*,p*,p*,K (chanmodes/noknock)
official-join,,,Y (m_ojoin),,,,,,,,, nonick,,,N (m_nonicks),N (nonicks),N,,,,,,d,,N (chanmodes/nonickchange)
op,o,o,o,o,o,o,o,o,o,o,o,o nonotice,,,T (m_nonotice),T (nonotice),,,N,N,T (ext/chm_nonotice),T (ext/chm_nonotice),T,,T (chanmodes/nonotice)
operonly,,O,O (m_operchans),O,,O,,O (ext/chm_operonly),O (ext/chm_operonly),O (ext/chm_operonly.so),,O official-join,,,Y (m_ojoin),Y (ojoin),,,,,,,,,
oplevel_apass,,,,,A,A,A,,,,, op,o,o,o,o,o,o,o,o,o,o,o,o,o
oplevel_upass,,,,,U,U,U,,,,, operonly,,O,O (m_operchans),O (operchans),O,,O,,O (ext/chm_operonly),O (ext/chm_operonly),O (ext/chm_operonly.so),,O (chanmodes/operonly)
opmoderated,,,U (extras/m_opmoderated),,,,,z,z,z,, oplevel_apass,,,,,,A,A,A,,,,,
owner,,,"q (m_customprefix, m_chanprotect)",q,,,,,y (when enabled),y (when enabled),,q oplevel_upass,,,,,,U,U,U,,,,,
paranoia,,p*,,,,,,,,,, opmoderated,,,U (contrib/m_opmoderated),,,,,,z,z,z,,
permanent,,,P (m_permchannels),P,,z,,P,P,P,,P owner,,,"q (m_customprefix, m_chanprotect)",q (customprefix),q,,,,,y (when enabled),y (when enabled),,q
private,p,p*,p,p,p,p,p,p*,p*,p*,p*,p paranoia,,p*,,,,,,,,,,,
quiet,,,(via extban m:),,,(via extban ~q:),,q,q,q,,(via extban ~q:) permanent,,,P (m_permchannels),P (permchannels),P,,z,,P,P,P,,P (chanmodes/permanent)
redirect,,,L (m_redirect),,,L,,f,f,f,,L private,p,p*,p,p,p,p,p,p,p*,p*,p*,p*,p
registered,,r,r (m_services_account),r,R,R,R,,,,,r quiet,,,(via extban m:),(via extban m:),,,(via extban ~q:),,q,q,q,,(via extban ~q:)
regmoderated,,M,M (m_services_account),M,,M,M,,,,,M redirect,,,L (m_redirect),L (redirect),,,L,,f,f,f,,L (chanmodes/link)
regonly,,R,R (m_services_account),R,r,r,r,r,r,r,r,R registered,,r,r (m_services_account),r (services_account),r,R,R,R,,,,,r
repeat,,,,,,,,,,K (ext/chm_norepeat.c),, regmoderated,,M,M (m_services_account),M (services_account),M,,M,M,,,,,M (chanmodes/regonlyspeak)
repeat_insp,,,E (m_repeat),,,,,,,,, regonly,,R,R (m_services_account),R (services_account),R,r,r,r,r,r,r,r,R (chanmodes/regonly)
secret,s,s,s,s,s,s,s,s,s,s,s,s repeat,,,,,,,,,,,K (ext/chm_norepeat.c),,
sslonly,,S,z (m_sslmodes),z,,,,S (ext/chm_sslonly),S (ext/chm_sslonly),S (ext/chm_sslonly.c),S,z repeat_insp,,,,E (repeat),,,,,,,,,
stripcolor,,,S (m_stripcolor),,,S,,c,c,c,,S secret,s,s,s,s,s,s,s,s,s,s,s,s,s
topiclock,t,t,t,t,t,t,t,t,t,t,t,t sslonly,,S,z (m_sslmodes),z (sslmodes),z,,,,S (ext/chm_sslonly),S (ext/chm_sslonly),S (ext/chm_sslonly.c),S,z (chanmodes/secureonly)
voice,v,v,v,v,v,v,v,v,v,v,v,v stripcolor,,,S (m_stripcolor),S (stripcolor),,,S,,c,c,c,,S (chanmodes/stripcolor)
,,,,,,,,,,,, topiclock,t,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,v
"* Mode +p corresponds to both “noknock” and “private” on TS6 IRCds, as well as “paranoia” on hybrid.",,,,,,,,,,,, ,,,,,,,,,,,,,
----,,,,,,,,,,,,,
<b>Note</b>: Channel modes for InspIRCd and UnrealIRCd are automatically negotiated on connect; this may not be a complete list.,,,,,,,,,,,,,
"* Mode +p corresponds to both “noknock” and “private” on TS6 IRCds, as well as “paranoia” on hybrid.",,,,,,,,,,,,,

1 Channel Mode / IRCd rfc1459 hybrid inspircd inspircd/insp20 inspircd/insp3 ngircd p10/ircu p10/nefarious p10/snircd ts6/charybdis ts6/chatircd ts6/elemental ts6/ratbox unreal
2 admin a (m_customprefix, m_chanprotect) a (customprefix) a a (when enabled) a (when enabled) a
3 adminonly a A (ext/chm_adminonly) A (ext/chm_adminonly) A (ext/chm_adminonly.so)
4 allowinvite A (m_allowinvite) A (allowinvite) g g g
5 auditorium u (m_auditorium) u (auditorium)
6 autoop w (m_autoop) w (autoop)
7 ban b b b b b b b b b b b b b
8 banexception e e (m_banexception) e (banexception) e e e e e e e
9 blockcaps B (m_blockcaps) B (anticaps, blockcaps) G (ext/chm_nocaps.so)
10 blockcolor c c (m_blockcolor) c (blockcolor) c c c c c (chanmodes/nocolor)
11 blockhighlight V (extras/m_blockhighlight) V (contrib/m_blockhighlight) V (contrib/blockhighlight)
12 delayjoin censor D (m_delayjoin) G (m_censor) G (censor) D D D D G (chanmodes/censor)
13 exemptchanops delayjoin X (m_exemptchanops) D (m_delayjoin) D (delayjoin) D D D D (chanmodes/delayjoin)
14 filter delaymsg g (m_filter) d (m_delaymsg) d (delaymsg)
15 flood exemptchanops f (m_messageflood) X (m_exemptchanops) X (exemptchanops)
16 flood_unreal filter g (m_filter) g (filter) f (via extban ~T:block:)
17 freetarget flood f (m_messageflood) f (messageflood) F F F
18 had_delayjoin flood_unreal d d d f (chanmodes/floodprot)
19 halfop freetarget h h (m_customprefix, m_halfop) h F h (when enabled) F h (when enabled) F h
20 hiddenbans had_delayjoin d d d u
21 hidequits halfop h h (m_customprefix, m_halfop) h (customprefix) h Q u h (when enabled) h (when enabled) h
22 history hiddenbans H (m_chanhistory) u
23 invex hidequits I I (m_inviteexception) I Q u I I I I I
24 inviteonly history i i i H (m_chanhistory) H (chanhistory) i i i i i i i i i
25 issecure invex I I (m_inviteexception) I (inviteexception) I I I I I Z I
26 joinflood inviteonly i i j (m_joinflood) i i i i i i j i j i j i i i
27 key issecure k k k k k k k k k k k k Z (chanmodes/issecure)
28 kicknorejoin joinflood j (m_joinflood) j (joinflood) j j J j
29 kicknorejoin_insp key k k J (m_kicknorejoin) k k k k k k k k k k k
30 largebanlist kicknorejoin L L L J
31 limit kicknorejoin_insp l l l J (m_kicknorejoin) J (kicknorejoin) l l l l l l l l l
32 moderated largebanlist m m m m m m m m L m L m L m m
33 netadminonly limit l l l l l l l l l N (ext/chm_netadminonly) l l l l
34 nickflood moderated m m F (m_nickflood) m m m m m m m m m m m
35 noamsg netadminonly T T N (ext/chm_netadminonly)
36 noctcp nickflood C C (m_noctcp) F (m_nickflood) F (nickflood) C C C C C C C
37 noextmsg noamsg n n n n n T T n n n n n
38 noforwards noctcp C C (m_noctcp) C (noctcp) C C C Q C Q C Q C C (chanmodes/noctcp)
39 noinvite noextmsg n n n n V n n n n n n n n V n
40 nokick noforwards Q (m_nokicks) Q Q Q E Q Q
41 noknock noinvite p* K (m_knock) V p* p* p* p* K V (chanmodes/noinvite)
42 nonick nokick N (m_nonicks) Q (m_nokicks) Q (nokicks) N Q d E N Q (chanmodes/nokick)
43 nonotice noknock p* T (m_nonotice) K (m_knock) K (knock) N N T (ext/chm_nonotice) p* T (ext/chm_nonotice) p* T p* p* T K (chanmodes/noknock)
44 official-join nonick Y (m_ojoin) N (m_nonicks) N (nonicks) N d N (chanmodes/nonickchange)
45 op nonotice o o o T (m_nonotice) T (nonotice) o o o N o N o T (ext/chm_nonotice) o T (ext/chm_nonotice) o T o o T (chanmodes/nonotice)
46 operonly official-join O O (m_operchans) Y (m_ojoin) Y (ojoin) O O O (ext/chm_operonly) O (ext/chm_operonly) O (ext/chm_operonly.so) O
47 oplevel_apass op o o o o o A o A o A o o o o o o
48 oplevel_upass operonly O O (m_operchans) O (operchans) O U U O U O (ext/chm_operonly) O (ext/chm_operonly) O (ext/chm_operonly.so) O (chanmodes/operonly)
49 opmoderated oplevel_apass U (extras/m_opmoderated) A A A z z z
50 owner oplevel_upass q (m_customprefix, m_chanprotect) q U U U y (when enabled) y (when enabled) q
51 paranoia opmoderated p* U (contrib/m_opmoderated) z z z
52 permanent owner P (m_permchannels) q (m_customprefix, m_chanprotect) q (customprefix) P q z P P y (when enabled) P y (when enabled) P q
53 private paranoia p p* p p p p p p* p* p* p* p
54 quiet permanent (via extban m:) P (m_permchannels) P (permchannels) P (via extban ~q:) z q P q P q P (via extban ~q:) P (chanmodes/permanent)
55 redirect private p p* L (m_redirect) p p p p L p p f p* f p* f p* p* L p
56 registered quiet r r (m_services_account) (via extban m:) (via extban m:) r R R (via extban ~q:) R q q q r (via extban ~q:)
57 regmoderated redirect M M (m_services_account) L (m_redirect) L (redirect) M M L M f f f M L (chanmodes/link)
58 regonly registered R r R (m_services_account) r (m_services_account) r (services_account) R r r R r R r R r r r r R r
59 repeat regmoderated M M (m_services_account) M (services_account) M M M K (ext/chm_norepeat.c) M (chanmodes/regonlyspeak)
60 repeat_insp regonly R E (m_repeat) R (m_services_account) R (services_account) R r r r r r r r R (chanmodes/regonly)
61 secret repeat s s s s s s s s s s K (ext/chm_norepeat.c) s s
62 sslonly repeat_insp S z (m_sslmodes) E (repeat) z S (ext/chm_sslonly) S (ext/chm_sslonly) S (ext/chm_sslonly.c) S z
63 stripcolor secret s s S (m_stripcolor) s s s s S s s c s c s c s s S s
64 topiclock sslonly t t S t z (m_sslmodes) z (sslmodes) t z t t t t S (ext/chm_sslonly) t S (ext/chm_sslonly) t S (ext/chm_sslonly.c) t S t z (chanmodes/secureonly)
65 voice stripcolor v v v S (m_stripcolor) S (stripcolor) v v v S v v c v c v c v v S (chanmodes/stripcolor)
66 topiclock t t t t t t t t t t t t t
67 ---- voice v v v v v v v v v v v v v
68 * Mode +p corresponds to both “noknock” and “private” on TS6 IRCds, as well as “paranoia” on hybrid.
69 ----
70 <b>Note</b>: Channel modes for InspIRCd and UnrealIRCd are automatically negotiated on connect; this may not be a complete list.
71 * Mode +p corresponds to both “noknock” and “private” on TS6 IRCds, as well as “paranoia” on hybrid.

View File

@ -44,7 +44,7 @@ td:first-child, th[scope="row"] {
} }
.tablecell-planned, .tablecell-yes2 { .tablecell-planned, .tablecell-yes2 {
background-color: #92E8DF background-color: #B1FCDE
} }
.tablecell-partial { .tablecell-partial {
@ -55,17 +55,6 @@ td:first-child, th[scope="row"] {
background-color: #DCB1FC background-color: #DCB1FC
} }
.tablecell-caveats {
background-color: #F0C884
}
.tablecell-caveats2 {
background-color: #ED9A80
}
.tablecell-no-padding {
padding: initial;
}
</style> </style>
</head> </head>
@ -75,7 +64,8 @@ td:first-child, th[scope="row"] {
<th scope="col">Channel Mode / IRCd</th> <th scope="col">Channel Mode / IRCd</th>
<th scope="col">rfc1459</th> <th scope="col">rfc1459</th>
<th scope="col">hybrid</th> <th scope="col">hybrid</th>
<th scope="col">inspircd</th> <th scope="col">inspircd/insp20</th>
<th scope="col">inspircd/insp3</th>
<th scope="col">ngircd</th> <th scope="col">ngircd</th>
<th scope="col">p10/ircu</th> <th scope="col">p10/ircu</th>
<th scope="col">p10/nefarious</th> <th scope="col">p10/nefarious</th>
@ -88,197 +78,203 @@ td:first-child, th[scope="row"] {
</tr> </tr>
<tr> <tr>
<th scope="row">admin</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+a<br><span class="note">(m_customprefix, m_chanprotect)</span></td><td class="tablecell-yes2">+a<br><span class="note">(customprefix)</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-yes2">+a<br><span class="note">(when enabled)</span></td><td class="tablecell-yes2">+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> <tr>
<th scope="row">adminonly</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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-yes2">+A<br><span class="note">(ext/chm_adminonly)</span></td><td class="tablecell-yes2">+A<br><span class="note">(ext/chm_adminonly)</span></td><td class="tablecell-yes2">+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> <tr>
<th scope="row">allowinvite</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+A<br><span class="note">(m_allowinvite)</span></td><td class="tablecell-yes2">+A<br><span class="note">(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> <tr>
<th scope="row">auditorium</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+u<br><span class="note">(m_auditorium)</span></td><td class="tablecell-yes2">+u<br><span class="note">(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> <tr>
<th scope="row">autoop</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+w<br><span class="note">(m_autoop)</span></td><td class="tablecell-yes2">+w<br><span class="note">(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> <tr>
<th scope="row">ban</th> <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> <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><td class="tablecell-yes">+b</td></tr>
<tr> <tr>
<th scope="row">banexception</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+e</td><td class="tablecell-yes2">+e<br><span class="note">(m_banexception)</span></td><td class="tablecell-yes2">+e<br><span class="note">(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> <tr>
<th scope="row">blockcaps</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+B<br><span class="note">(m_blockcaps)</span></td><td class="tablecell-yes2">+B<br><span class="note">(anticaps, 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-yes2">+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> <tr>
<th scope="row">blockcolor</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-yes2">+c<br><span class="note">(m_blockcolor)</span></td><td class="tablecell-yes2">+c<br><span class="note">(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-yes2">+c<br><span class="note">(chanmodes/nocolor)</span></td></tr>
<tr> <tr>
<th scope="row">blockhighlight</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+V<br><span class="note">(contrib/m_blockhighlight)</span></td><td class="tablecell-yes2">+V<br><span class="note">(contrib/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">censor</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+G<br><span class="note">(m_censor)</span></td><td class="tablecell-yes2">+G<br><span class="note">(censor)</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-yes2">+G<br><span class="note">(chanmodes/censor)</span></td></tr>
<tr> <tr>
<th scope="row">delayjoin</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+D<br><span class="note">(m_delayjoin)</span></td><td class="tablecell-yes2">+D<br><span class="note">(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-yes2">+D<br><span class="note">(chanmodes/delayjoin)</span></td></tr>
<tr>
<th scope="row">delaymsg</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+d<br><span class="note">(m_delaymsg)</span></td><td class="tablecell-yes2">+d<br><span class="note">(delaymsg)</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> <tr>
<th scope="row">exemptchanops</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+X<br><span class="note">(m_exemptchanops)</span></td><td class="tablecell-yes2">+X<br><span class="note">(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> <tr>
<th scope="row">filter</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+g<br><span class="note">(m_filter)</span></td><td class="tablecell-yes2">+g<br><span class="note">(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-partial">(via extban ~T:block:)</td></tr>
<tr> <tr>
<th scope="row">flood</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+f<br><span class="note">(m_messageflood)</span></td><td class="tablecell-yes2">+f<br><span class="note">(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> <tr>
<th scope="row">flood_unreal</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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">+f<br><span class="note">(chanmodes/floodprot)</span></td></tr>
<tr> <tr>
<th scope="row">freetarget</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <tr>
<th scope="row">had_delayjoin</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <tr>
<th scope="row">halfop</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+h</td><td class="tablecell-yes2">+h<br><span class="note">(m_customprefix, m_halfop)</span></td><td class="tablecell-yes2">+h<br><span class="note">(customprefix)</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-yes2">+h<br><span class="note">(when enabled)</span></td><td class="tablecell-yes2">+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> <tr>
<th scope="row">hiddenbans</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <tr>
<th scope="row">hidequits</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <tr>
<th scope="row">history</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+H<br><span class="note">(m_chanhistory)</span></td><td class="tablecell-yes2">+H<br><span class="note">(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> <tr>
<th scope="row">invex</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td><td class="tablecell-yes2">+I<br><span class="note">(m_inviteexception)</span></td><td class="tablecell-yes2">+I<br><span class="note">(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> <tr>
<th scope="row">inviteonly</th> <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> <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><td class="tablecell-yes">+i</td></tr>
<tr> <tr>
<th scope="row">issecure</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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">+Z<br><span class="note">(chanmodes/issecure)</span></td></tr>
<tr> <tr>
<th scope="row">joinflood</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+j<br><span class="note">(m_joinflood)</span></td><td class="tablecell-yes2">+j<br><span class="note">(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> <tr>
<th scope="row">key</th> <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> <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><td class="tablecell-yes">+k</td></tr>
<tr> <tr>
<th scope="row">kicknorejoin</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <tr>
<th scope="row">kicknorejoin_insp</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+J<br><span class="note">(m_kicknorejoin)</span></td><td class="tablecell-yes2">+J<br><span class="note">(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> <tr>
<th scope="row">largebanlist</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <tr>
<th scope="row">limit</th> <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> <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><td class="tablecell-yes">+l</td></tr>
<tr> <tr>
<th scope="row">moderated</th> <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> <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><td class="tablecell-yes">+m</td></tr>
<tr> <tr>
<th scope="row">netadminonly</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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">+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> <tr>
<th scope="row">nickflood</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+F<br><span class="note">(m_nickflood)</span></td><td class="tablecell-yes2">+F<br><span class="note">(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> <tr>
<th scope="row">noamsg</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <tr>
<th scope="row">noctcp</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-yes2">+C<br><span class="note">(m_noctcp)</span></td><td class="tablecell-yes2">+C<br><span class="note">(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-yes2">+C<br><span class="note">(chanmodes/noctcp)</span></td></tr>
<tr> <tr>
<th scope="row">noextmsg</th> <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> <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><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><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td></tr>
<tr> <tr>
<th scope="row">noforwards</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <tr>
<th scope="row">noinvite</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-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-yes2">+V<br><span class="note">(chanmodes/noinvite)</span></td></tr>
<tr> <tr>
<th scope="row">nokick</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+Q<br><span class="note">(m_nokicks)</span></td><td class="tablecell-yes2">+Q<br><span class="note">(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-yes2">+Q<br><span class="note">(chanmodes/nokick)</span></td></tr>
<tr> <tr>
<th scope="row">noknock</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-special">+p*</td><td class="tablecell-yes2">+K<br><span class="note">(m_knock)</span></td><td class="tablecell-yes2">+K<br><span class="note">(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-special">+p*</td><td class="tablecell-special">+p*</td><td class="tablecell-special">+p*</td><td class="tablecell-special">+p*</td><td class="tablecell-yes2">+K<br><span class="note">(chanmodes/noknock)</span></td></tr>
<tr> <tr>
<th scope="row">nonick</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+N<br><span class="note">(m_nonicks)</span></td><td class="tablecell-yes2">+N<br><span class="note">(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-yes2">+N<br><span class="note">(chanmodes/nonickchange)</span></td></tr>
<tr> <tr>
<th scope="row">nonotice</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+T<br><span class="note">(m_nonotice)</span></td><td class="tablecell-yes2">+T<br><span class="note">(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-yes2">+T<br><span class="note">(ext/chm_nonotice)</span></td><td class="tablecell-yes2">+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-yes2">+T<br><span class="note">(chanmodes/nonotice)</span></td></tr>
<tr> <tr>
<th scope="row">official-join</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+Y<br><span class="note">(m_ojoin)</span></td><td class="tablecell-yes2">+Y<br><span class="note">(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> <tr>
<th scope="row">op</th> <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> <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><td class="tablecell-yes">+o</td></tr>
<tr> <tr>
<th scope="row">operonly</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+O</td><td class="tablecell-yes2">+O<br><span class="note">(m_operchans)</span></td><td class="tablecell-yes2">+O<br><span class="note">(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-yes2">+O<br><span class="note">(ext/chm_operonly)</span></td><td class="tablecell-yes2">+O<br><span class="note">(ext/chm_operonly)</span></td><td class="tablecell-yes2">+O<br><span class="note">(ext/chm_operonly.so)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+O<br><span class="note">(chanmodes/operonly)</span></td></tr>
<tr> <tr>
<th scope="row">oplevel_apass</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <tr>
<th scope="row">oplevel_upass</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <tr>
<th scope="row">opmoderated</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+U<br><span class="note">(contrib/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-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> <tr>
<th scope="row">owner</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+q<br><span class="note">(m_customprefix, m_chanprotect)</span></td><td class="tablecell-yes2">+q<br><span class="note">(customprefix)</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-yes2">+y<br><span class="note">(when enabled)</span></td><td class="tablecell-yes2">+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> <tr>
<th scope="row">paranoia</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-special">+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><td class="tablecell-na note">n/a</td></tr>
<tr> <tr>
<th scope="row">permanent</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+P<br><span class="note">(m_permchannels)</span></td><td class="tablecell-yes2">+P<br><span class="note">(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-yes2">+P<br><span class="note">(chanmodes/permanent)</span></td></tr>
<tr> <tr>
<th scope="row">private</th> <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> <td class="tablecell-yes">+p</td><td class="tablecell-special">+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-yes">+p</td><td class="tablecell-special">+p*</td><td class="tablecell-special">+p*</td><td class="tablecell-special">+p*</td><td class="tablecell-special">+p*</td><td class="tablecell-yes">+p</td></tr>
<tr> <tr>
<th scope="row">quiet</th> <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> <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-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> <tr>
<th scope="row">redirect</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+L<br><span class="note">(m_redirect)</span></td><td class="tablecell-yes2">+L<br><span class="note">(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-yes2">+L<br><span class="note">(chanmodes/link)</span></td></tr>
<tr> <tr>
<th scope="row">registered</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td><td class="tablecell-yes2">+r<br><span class="note">(m_services_account)</span></td><td class="tablecell-yes2">+r<br><span class="note">(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> <tr>
<th scope="row">regmoderated</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+M</td><td class="tablecell-yes2">+M<br><span class="note">(m_services_account)</span></td><td class="tablecell-yes2">+M<br><span class="note">(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-yes2">+M<br><span class="note">(chanmodes/regonlyspeak)</span></td></tr>
<tr> <tr>
<th scope="row">regonly</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+R</td><td class="tablecell-yes2">+R<br><span class="note">(m_services_account)</span></td><td class="tablecell-yes2">+R<br><span class="note">(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-yes2">+R<br><span class="note">(chanmodes/regonly)</span></td></tr>
<tr> <tr>
<th scope="row">repeat</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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">+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> <tr>
<th scope="row">repeat_insp</th> <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> <td class="tablecell-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">+E<br><span class="note">(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> <tr>
<th scope="row">secret</th> <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> <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><td class="tablecell-yes">+s</td></tr>
<tr> <tr>
<th scope="row">sslonly</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+S</td><td class="tablecell-yes2">+z<br><span class="note">(m_sslmodes)</span></td><td class="tablecell-yes2">+z<br><span class="note">(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-yes2">+S<br><span class="note">(ext/chm_sslonly)</span></td><td class="tablecell-yes2">+S<br><span class="note">(ext/chm_sslonly)</span></td><td class="tablecell-yes2">+S<br><span class="note">(ext/chm_sslonly.c)</span></td><td class="tablecell-yes">+S</td><td class="tablecell-yes2">+z<br><span class="note">(chanmodes/secureonly)</span></td></tr>
<tr> <tr>
<th scope="row">stripcolor</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+S<br><span class="note">(m_stripcolor)</span></td><td class="tablecell-yes2">+S<br><span class="note">(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-yes2">+S<br><span class="note">(chanmodes/stripcolor)</span></td></tr>
<tr> <tr>
<th scope="row">topiclock</th> <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> <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><td class="tablecell-yes">+t</td></tr>
<tr> <tr>
<th scope="row">voice</th> <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> <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><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> <p><b>Note</b>: Channel modes for InspIRCd and UnrealIRCd are automatically negotiated on connect; this may not be a complete list.</p><p>* Mode +p corresponds to both “noknock” and “private” on TS6 IRCds, as well as “paranoia” on hybrid.</p>
</table> </table>
</body> </body>

View File

@ -16,7 +16,7 @@ ban_noctcp,C:,,,
ban_nojoins,,,,~j: ban_nojoins,,,,~j:
ban_nokicks,Q:,,, ban_nokicks,Q:,,,
ban_nonick,N:,~n:,,~n: ban_nonick,N:,~n:,,~n:
ban_nonotice,T:,,, ban_nonotice,T:,,,~m:notice: (+e only)
ban_not_account,,,$~a:, ban_not_account,,,$~a:,
ban_not_banshare,,,$~j:, ban_not_banshare,,,$~j:,
ban_not_extgecos,,,$~x:, ban_not_extgecos,,,$~x:,
@ -29,7 +29,12 @@ ban_opertype,O:,,,~O:
ban_partmsgs,p:,,, ban_partmsgs,p:,,,
ban_realname,r:,~r:,$r:,~r: ban_realname,r:,~r:,$r:,~r:
ban_server,s:,,$s:, ban_server,s:,,$s:,
ban_stripcolor,S:,,, ban_stripcolor,S:,,,~m:color: (+e only)
ban_unregistered,,,$~a,
ban_unregistered_mark,,~M:,, ban_unregistered_mark,,~M:,,
ban_unregistered_matching,U:,,, ban_unregistered_matching,U:,,,
msgbypass_external,,,,~m:external:
msgbypass_censor,,,,~m:censor:
msgbypass_moderated,,,,~m:moderated:
quiet,m:,~q:,(via cmode +q),~q: quiet,m:,~q:,(via cmode +q),~q:
timedban_unreal,,,,~t:

1 Extban / IRCd inspircd p10/nefarious ts6/charybdis unreal
16 ban_nojoins ~j:
17 ban_nokicks Q:
18 ban_nonick N: ~n: ~n:
19 ban_nonotice T: ~m:notice: (+e only)
20 ban_not_account $~a:
21 ban_not_banshare $~j:
22 ban_not_extgecos $~x:
29 ban_partmsgs p:
30 ban_realname r: ~r: $r: ~r:
31 ban_server s: $s:
32 ban_stripcolor S: ~m:color: (+e only)
33 ban_unregistered $~a
34 ban_unregistered_mark ~M:
35 ban_unregistered_matching U:
36 msgbypass_external ~m:external:
37 msgbypass_censor ~m:censor:
38 msgbypass_moderated ~m:moderated:
39 quiet m: ~q: (via cmode +q) ~q:
40 timedban_unreal ~t:

View File

@ -44,7 +44,7 @@ td:first-child, th[scope="row"] {
} }
.tablecell-planned, .tablecell-yes2 { .tablecell-planned, .tablecell-yes2 {
background-color: #92E8DF background-color: #B1FCDE
} }
.tablecell-partial { .tablecell-partial {
@ -55,17 +55,6 @@ td:first-child, th[scope="row"] {
background-color: #DCB1FC background-color: #DCB1FC
} }
.tablecell-caveats {
background-color: #F0C884
}
.tablecell-caveats2 {
background-color: #ED9A80
}
.tablecell-no-padding {
padding: initial;
}
</style> </style>
</head> </head>
@ -131,7 +120,7 @@ td:first-child, th[scope="row"] {
<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> <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> <tr>
<th scope="row">ban_nonotice</th> <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> <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-yes2">~m:notice:<br><span class="note">(+e only)</span></td></tr>
<tr> <tr>
<th scope="row">ban_not_account</th> <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> <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>
@ -170,7 +159,10 @@ td:first-child, th[scope="row"] {
<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> <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> <tr>
<th scope="row">ban_stripcolor</th> <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> <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-yes2">~m:color:<br><span class="note">(+e only)</span></td></tr>
<tr>
<th scope="row">ban_unregistered</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> <tr>
<th scope="row">ban_unregistered_mark</th> <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> <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>
@ -178,8 +170,20 @@ td:first-child, th[scope="row"] {
<th scope="row">ban_unregistered_matching</th> <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> <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> <tr>
<th scope="row">msgbypass_external</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">~m:external:</td></tr>
<tr>
<th scope="row">msgbypass_censor</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">~m:censor:</td></tr>
<tr>
<th scope="row">msgbypass_moderated</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">~m:moderated:</td></tr>
<tr>
<th scope="row">quiet</th> <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> <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>
<tr>
<th scope="row">timedban_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-yes">~t:</td></tr>
</table> </table>

View File

@ -3,9 +3,9 @@
Generates HTML versions of the mode list .csv definitions. Generates HTML versions of the mode list .csv definitions.
""" """
import csv
import os import os
import os.path import os.path
import csv
os.chdir(os.path.dirname(__file__)) os.chdir(os.path.dirname(__file__))
@ -31,12 +31,12 @@ def _format(articlename, text):
text, note = text.split(' ', 1) text, note = text.split(' ', 1)
except ValueError: except ValueError:
if text.endswith('*'): if text.endswith('*'):
text = '<td class="tablecell-yes2">%s</td>' % text text = '<td class="tablecell-special">%s</td>' % text
else: else:
text = '<td class="tablecell-yes">%s</td>' % text text = '<td class="tablecell-yes">%s</td>' % text
else: else:
text = '%s<br><span class="note">%s</span>' % (text, note) text = '%s<br><span class="note">%s</span>' % (text, note)
text = '<td class="tablecell-special">%s</td>' % text text = '<td class="tablecell-yes2">%s</td>' % text
else: else:
text = '<td class="tablecell-na note">n/a</td>' text = '<td class="tablecell-na note">n/a</td>'
return text return text
@ -95,7 +95,7 @@ td:first-child, th[scope="row"] {
} }
.tablecell-planned, .tablecell-yes2 { .tablecell-planned, .tablecell-yes2 {
background-color: #92E8DF background-color: #B1FCDE
} }
.tablecell-partial { .tablecell-partial {
@ -106,17 +106,6 @@ td:first-child, th[scope="row"] {
background-color: #DCB1FC background-color: #DCB1FC
} }
.tablecell-caveats {
background-color: #F0C884
}
.tablecell-caveats2 {
background-color: #ED9A80
}
.tablecell-no-padding {
padding: initial;
}
</style> </style>
</head> </head>

View File

@ -1,39 +1,39 @@
User Mode / IRCd,RFC 1459,hybrid,inspircd,ngircd,p10/ircu,p10/nefarious,p10/snircd,ts6/charybdis,ts6/chatircd,ts6/elemental,ts6/ratbox,unreal 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, admin,,a,,,,a,,a,a,a,a,
away,,,,a,,,,,,,, away,,,,a,,,,,,,,
bot,,,B,B,,B,,,B,B,,B bot,,,B (botmode),B,,B,,,B,B,,B (usermodes/bot)
callerid,,g,g,,,,,g,g,g,g, callerid,,g,g (callerid),,,,,g,g,g,g,
callerid_sslonly,,,,,,,,,t,,, censor,,,,,,,,,,,,G (usermodes/censor)
cloak,,x,x,x,x,x,x,x,x,x,,x cloak,,x,x (cloaking),x,x,x,x,x,x,x,,x
cloak_fakehost,,,,,,f,,,,,, cloak_fakehost,,,,,,f,,,,,,
cloak_hashedhost,,,,,,C,,,,,, cloak_hashedhost,,,,,,C,,,,,,
cloak_hashedip,,,,,,c,,,,,, cloak_hashedip,,,,,,c,,,,,,
cloak_sethost,,,,,,h,h,,,,, cloak_sethost,,,,,,h,h,,,,,
deaf,,D,d,b,d,d,d,D,D,D,D,d deaf,,D,d (deaf),,d,d,d,D,D,D,D,d
deaf_commonchan,,G,c,C,,q,,,,,, deaf_commonchan,,G,c (commonchans),C,,q,,,,,,
debug,,d,,,,,,,,,, debug,,d,,,,,,,,,,
filter,,,,,,,,,,,,G filter,,,,,,,,,,,,G
floodexempt,,,,f,,,,,,,, floodexempt,,,,F,,,,,,,,
helpop,,,h,,,,,,,,, helpop,,,h (helpop),,,,,,,,,
hidechans,,p,I,I,,n,n,,,I,,p hidechans,,p,I (hidechans),I,,n,n,,,I,,p (usermodes/privacy)
hideidle,,q,,,,I,I,,,,,I hideidle,,q,,,,I,I,,,,,I
hideoper,,H,H,,,H,,,,,,H hideoper,,H,H (hideoper),,,H,,,,,,H
invisible,i,i,i,i,i,i,i,i,i,i,i,i invisible,i,i,i,i,i,i,i,i,i,i,i,i
locops,,l,,,O,O,O,l,l,l,l, locops,,l,,,O,O,O,l,l,l,l,
netadmin,,,,,,,,,N,,, netadmin,,,,,,,,,N,,,
noctcp,,,,,,,,,,C,,T noctcp,,,,,,,,,,C,,T (usermodes/noctcp)
noforward,,,,,,L,,Q,Q,Q,, noforward,,,L (redirect),,,L,,Q,Q,Q,,
noinvite,,,,,,,,,,V,, noinvite,,,,,,,,,,V,,
oper,o,o,o,o,o,o,o,o,o,o,o,o oper,o,o,o,o,o,o,o,o,o,o,o,o
operwall,,,,,,,,z,z,z,z, operwall,,,,,,,,z,z,z,z,
override,,,,,,X,X,p,p,p,, override,,,,,,X,X,p,p,p,,
privdeaf,,,,,,D,,,,,, privdeaf,,,,b,,D,,,,,,D (usermodes/privdeaf)
protected,,,,,,,,,,,,q protected,,,,,,,,,,,,q (usermodes/nokick)
regdeaf,,R,R,,,R,R,R,R,R,,R regdeaf,,R,R (services_account),,,R,R,R,R,R,,R (usermodes/regonlymsg)
registered,,r,r,R,r,r,r,,,,,r registered,,r,r (services_account),R,r,r,r,,,,,r
restricted,,,,r,,,,,,,, restricted,,,,r,,,,,,,,
servprotect,,,k,q,k,k,k,S,S,S,S,S servprotect,,,k (servprotect),q,k,k,k,S,S,S,S,S (usermodes/servicebot)
showwhois,,,W,,,W,,,,,,W showwhois,,,W (showwhois),,,W,,,,,,W (usermodes/showwhois)
sno_badclientconnections,,u,,,,,,,,,u, sno_badclientconnections,,u,,,,,,,,,u,
sno_botfloods,,b,,,,,,,,,b, sno_botfloods,,b,,,,,,,,,b,
sno_clientconnections,,c,,c,,,,,,,c, sno_clientconnections,,c,,c,,,,,,,c,
@ -46,9 +46,10 @@ sno_remoteclientconnections,,F,,,,,,,,,,
sno_serverconnects,,e,,,,,,,,,x, sno_serverconnects,,e,,,,,,,,,x,
sno_skill,,k,,,,,,,,,k, sno_skill,,k,,,,,,,,,k,
sno_stats,,y,,,,,,,,,y, sno_stats,,y,,,,,,,,,y,
snomask,s,s,s,s,,s,,s,s,s,s,s snomask,s,s,s,s,s,s,s,s,s,s,s,s
ssl,,S,,,,z,,,,,,z ssl,,S,,,,z,,,,,,z
stripcolor,,,S,,,,,,,,, sslonlymsg,,,,,,,,,t,,,Z (usermodes/secureonlymsg)
stripcolor,,,S (stripcolor),,,,,,,,,
vhost,,,,,,,,,,,,t vhost,,,,,,,,,,,,t
wallops,w,w,w,w,,w,,w,w,w,w,w wallops,w,w,w,w,w,w,w,w,w,w,w,w
webirc,,W,,,,,,,,,, webirc,,W,,,,,,,,,,

1 User Mode / IRCd RFC 1459 hybrid inspircd ngircd p10/ircu p10/nefarious p10/snircd ts6/charybdis ts6/chatircd ts6/elemental ts6/ratbox unreal
2 admin a a a a a a
3 away a
4 bot B B (botmode) B B B B B B (usermodes/bot)
5 callerid g g g (callerid) g g g g
6 callerid_sslonly censor t G (usermodes/censor)
7 cloak x x x (cloaking) x x x x x x x x
8 cloak_fakehost f
9 cloak_hashedhost C
10 cloak_hashedip c
11 cloak_sethost h h
12 deaf D d d (deaf) b d d d D D D D d
13 deaf_commonchan G c c (commonchans) C q
14 debug d
15 filter G
16 floodexempt f F
17 helpop h h (helpop)
18 hidechans p I I (hidechans) I n n I p p (usermodes/privacy)
19 hideidle q I I I
20 hideoper H H H (hideoper) H H
21 invisible i i i i i i i i i i i i
22 locops l O O O l l l l
23 netadmin N
24 noctcp C T T (usermodes/noctcp)
25 noforward L (redirect) L Q Q Q
26 noinvite V
27 oper o o o o o o o o o o o o
28 operwall z z z z
29 override X X p p p
30 privdeaf b D D (usermodes/privdeaf)
31 protected q q (usermodes/nokick)
32 regdeaf R R R (services_account) R R R R R R R (usermodes/regonlymsg)
33 registered r r r (services_account) R r r r r
34 restricted r
35 servprotect k k (servprotect) q k k k S S S S S S (usermodes/servicebot)
36 showwhois W W (showwhois) W W W (usermodes/showwhois)
37 sno_badclientconnections u u
38 sno_botfloods b b
39 sno_clientconnections c c c
46 sno_serverconnects e x
47 sno_skill k k
48 sno_stats y y
49 snomask s s s s s s s s s s s s
50 ssl S z z
51 stripcolor sslonlymsg S t Z (usermodes/secureonlymsg)
52 stripcolor S (stripcolor)
53 vhost t
54 wallops w w w w w w w w w w w w
55 webirc W

View File

@ -44,7 +44,7 @@ td:first-child, th[scope="row"] {
} }
.tablecell-planned, .tablecell-yes2 { .tablecell-planned, .tablecell-yes2 {
background-color: #92E8DF background-color: #B1FCDE
} }
.tablecell-partial { .tablecell-partial {
@ -55,17 +55,6 @@ td:first-child, th[scope="row"] {
background-color: #DCB1FC background-color: #DCB1FC
} }
.tablecell-caveats {
background-color: #F0C884
}
.tablecell-caveats2 {
background-color: #ED9A80
}
.tablecell-no-padding {
padding: initial;
}
</style> </style>
</head> </head>
@ -94,16 +83,16 @@ td:first-child, th[scope="row"] {
<td class="tablecell-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> <td class="tablecell-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> <tr>
<th scope="row">bot</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+B<br><span class="note">(botmode)</span></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-yes2">+B<br><span class="note">(usermodes/bot)</span></td></tr>
<tr> <tr>
<th scope="row">callerid</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+g</td><td class="tablecell-yes2">+g<br><span class="note">(callerid)</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-yes">+g</td><td class="tablecell-na note">n/a</td></tr>
<tr> <tr>
<th scope="row">callerid_sslonly</th> <th scope="row">censor</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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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">+G<br><span class="note">(usermodes/censor)</span></td></tr>
<tr> <tr>
<th scope="row">cloak</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+x</td><td class="tablecell-yes2">+x<br><span class="note">(cloaking)</span></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> <tr>
<th scope="row">cloak_fakehost</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
@ -118,10 +107,10 @@ td:first-child, th[scope="row"] {
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <tr>
<th scope="row">deaf</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+D</td><td class="tablecell-yes2">+d<br><span class="note">(deaf)</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-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> <tr>
<th scope="row">deaf_commonchan</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+G</td><td class="tablecell-yes2">+c<br><span class="note">(commonchans)</span></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> <tr>
<th scope="row">debug</th> <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> <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>
@ -130,19 +119,19 @@ td:first-child, th[scope="row"] {
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <tr>
<th scope="row">floodexempt</th> <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> <td class="tablecell-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> <tr>
<th scope="row">helpop</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+h<br><span class="note">(helpop)</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> <tr>
<th scope="row">hidechans</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+p</td><td class="tablecell-yes2">+I<br><span class="note">(hidechans)</span></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-yes2">+p<br><span class="note">(usermodes/privacy)</span></td></tr>
<tr> <tr>
<th scope="row">hideidle</th> <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> <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> <tr>
<th scope="row">hideoper</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+H</td><td class="tablecell-yes2">+H<br><span class="note">(hideoper)</span></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> <tr>
<th scope="row">invisible</th> <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> <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>
@ -154,10 +143,10 @@ td:first-child, th[scope="row"] {
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <tr>
<th scope="row">noctcp</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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-yes2">+T<br><span class="note">(usermodes/noctcp)</span></td></tr>
<tr> <tr>
<th scope="row">noforward</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+L<br><span class="note">(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">+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> <tr>
<th scope="row">noinvite</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
@ -172,25 +161,25 @@ td:first-child, th[scope="row"] {
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <tr>
<th scope="row">privdeaf</th> <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> <td class="tablecell-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><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-yes2">+D<br><span class="note">(usermodes/privdeaf)</span></td></tr>
<tr> <tr>
<th scope="row">protected</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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">+q<br><span class="note">(usermodes/nokick)</span></td></tr>
<tr> <tr>
<th scope="row">regdeaf</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+R</td><td class="tablecell-yes2">+R<br><span class="note">(services_account)</span></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-yes2">+R<br><span class="note">(usermodes/regonlymsg)</span></td></tr>
<tr> <tr>
<th scope="row">registered</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td><td class="tablecell-yes2">+r<br><span class="note">(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> <tr>
<th scope="row">restricted</th> <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> <td class="tablecell-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> <tr>
<th scope="row">servprotect</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+k<br><span class="note">(servprotect)</span></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-yes2">+S<br><span class="note">(usermodes/servicebot)</span></td></tr>
<tr> <tr>
<th scope="row">showwhois</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+W<br><span class="note">(showwhois)</span></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-yes2">+W<br><span class="note">(usermodes/showwhois)</span></td></tr>
<tr> <tr>
<th scope="row">sno_badclientconnections</th> <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> <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>
@ -229,19 +218,22 @@ td:first-child, th[scope="row"] {
<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> <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> <tr>
<th scope="row">snomask</th> <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> <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> <tr>
<th scope="row">ssl</th> <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> <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> <tr>
<th scope="row">sslonlymsg</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-yes2">+Z<br><span class="note">(usermodes/secureonlymsg)</span></td></tr>
<tr>
<th scope="row">stripcolor</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+S<br><span class="note">(stripcolor)</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> <tr>
<th scope="row">vhost</th> <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> <td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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> <tr>
<th scope="row">wallops</th> <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> <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><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><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td></tr>
<tr> <tr>
<th scope="row">webirc</th> <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> <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>

View File

@ -52,12 +52,14 @@ Remote versions of the `manage`, `list`, `sync`, and `clear` commands also exist
- `commands.loglevel` - Grants access to the `loglevel` command. - `commands.loglevel` - Grants access to the `loglevel` command.
- `commands.logout.force` - Allows forcing logouts on other users via the `logout` command. - `commands.logout.force` - Allows forcing logouts on other users via the `logout` command.
- `commands.showchan` - Grants access to the `showchan` command. **With the default permissions set, this is granted to all users.** - `commands.showchan` - Grants access to the `showchan` command. **With the default permissions set, this is granted to all users.**
- `commands.shownet` - Grants access to the `shownet` command (basic info including netname, protocol module, and encoding). **With the default permissions set, this is granted to all users.**
- `commands.shownet.extended` - Grants access to extended info in `shownet`, including connected status, target IP:port, and configured PyLink hostname / SID.
- `commands.showuser` - Grants access to the `showuser` 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.** - `commands.status` - Grants access to the `status` command. **With the default permissions set, this is granted to all users.**
## Exec ## Exec
- `exec.exec` - Grants access to the `exec` command. - `exec.exec` - Grants access to the `exec` and `iexec` commands.
- `exec.eval` - Grants access to the `eval` command. - `exec.eval` - Grants access to the `eval`, `ieval`, `peval`, and `pieval` commands.
- `exec.inject` - Grants access to the `inject` command. - `exec.inject` - Grants access to the `inject` command.
- `exec.threadinfo` - Grants access to the `threadinfo` command. - `exec.threadinfo` - Grants access to the `threadinfo` command.

View File

@ -1,17 +1,22 @@
# PyLink Relay Quick Start Guide # PyLink Relay Quick Start
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!). ## What is Relay?
This guide goes over some of the basic commands in Relay, as well as all the must-know notes. PyLink Relay is a plugin that provides transparent relays between channels on different networks. On participating networks, PyLink connects as a services server and mirrors messages as well as user lists from relayed channels, the latter by creating "puppet" service clients for all remote users in common channels. Relay offers an alternative to classic IRC linking, letting networks share channels on demand while retaining their services, policies, and distinct branding. By default, Relay also secures channels from remote oper overrides via a CLAIM feature, which restricts /kick, /mode, and /topic changes from un-opped users unless they are granted permissions via CLAIM.
## How nick suffixing work Relay shares many ideas from its predecessor Janus, but is a complete rewrite in Python. This guide goes over some of the basic commands in Relay, as well as some must-know gotchas.
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. ## Important notes (READ FIRST!)
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. ### How nick suffixing work
## Services compatibility By default, Relay will automatically tag users from other networks with a suffix such as `/net`. This prevents confusing nick collisions if the same nick is used on multiple linked networks, and ensure that nicks from remote networks are all isolated into their own namespaces.
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.**
How is this relevant to an operator? 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' 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) - 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. - 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.
@ -21,6 +26,7 @@ While PyLink is generally able to run independently of individual networks's ser
- Rationale: ChanFix is incompatible with Relay CLAIM because it overrides ops on relay channels whenever they appear "opless". This basic op check is unable to consider the case of remote channel services not being set to join channels, and will instead cause join/message/part spam as CLAIM reverts the ChanFix service's mode changes. - Rationale: ChanFix is incompatible with Relay CLAIM because it overrides ops on relay channels whenever they appear "opless". This basic op check is unable to consider the case of remote channel services not being set to join channels, and will instead cause join/message/part spam as CLAIM reverts the ChanFix service's mode changes.
- *Any*: **Do NOT register a relayed channel on multiple networks** - *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?) - Rationale: It is very easy for this to start kick or mode wars. (Bad akick mask? Secure ops enabled?)
- Clientbot is an exception to this, though you may want to add Clientbot networks to CLAIM so that PyLink doesn't try to reverse modes set by services on the Clientbot network.
- *Any*: **Do NOT jupe virtual Relay servers** (e.g. `net.relay`) - *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). - 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: - Multiple PyLink Relay instances:
@ -28,35 +34,49 @@ While PyLink is generally able to run independently of individual networks's ser
- **Do NOT connect a network to 2+ separate PyLink instances if there is another network already acting as a hub for them**. - **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 should an operator run the wrong command, which will hammer your CPU and relentlessly spam your channels. - Not following these rules means that it's very easy for the Relay instances to go in a loop should an operator run the wrong command, which will hammer your CPU and relentlessly spam your channels.
## Relay commands Note: P10-specific services packages have not been particularly tested - your feedback is welcome.
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: ## Relay commands
The basic steps for setting up a relay is to first CREATE the channel with PyLink on the network that owns it, and run LINK from each network that wants to link to it. In most cases, you want to run CREATE on the network where the channel is registered with services.
Importantly, this means that CREATE and LINK have to be run on different networks for any particular channel, and that you should only run CREATE once for each distinct channel! This setup is intended to allow individual network admins to pick and choose channels they want to participate in.
First, to list all available channels:
- `/msg PyLink linked` - `/msg PyLink linked`
To create a channel: To create a channel on Relay:
- `/msg PyLink create #channelname` - `/msg PyLink create #channelname`
- Note: **you can only create channels on full IRCd links - this will NOT work with Clientbot.**
- A channel created on a particular network is considered to be _owned_ by that network; this affects how CLAIM works for instance (see the next section)
To link to a channel already created on a different network: To link to a channel already created on a different network:
- `/msg PyLink link othernet #channelname` - `/msg PyLink link othernet #channelname`
- You should replace `othernet` with the *short name* for the network that owns the channel.
- Note: network names are case sensitive!
You can also link remote channels to take a different name on your network. (This is the third argument to the LINK command) You can also link remote channels while using a different name for it on your network. (This is the third argument to the LINK command)
- `/msg PyLink link othernet #lobby #othernet-lobby` - `/msg PyLink link othernet #lobby #othernet-lobby`
To remove a relay channel that you've created: To completely remove a relay channel (on the network that created it):
- `/msg PyLink destroy #channelname` - `/msg PyLink destroy #channelname`
To delink a channel linked to another network: To delink a channel *linked to another network*:
- `/msg PyLink delink #localchannelname` - `/msg PyLink delink #localchannelname`
To delink one of *your* channels from another network:
- `/msg PyLink delink #yourchannelname <name-of-other-network>`
Then, to list all available channels: Then, to list all available channels:
- `/msg PyLink linked` - `/msg PyLink linked`
### Claiming channels ### 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. Channel claiming is a feature which prevents oper override (MODE, KICK, TOPIC, KILL, OJOIN, ...) by other networks' operators from affecting your channels. By default, CLAIM is enabled for all new channels, though this can be configured via the [`relay::enable_default_claim` option](https://github.com/jlu5/PyLink/blob/3.0.0/example-conf.yml#L828-L831). Unless the claimed network list of a channel is _empty__, oper override will only be allowed from networks on the CLAIM list (plus the network that owns the channel).
To set a claim (note: for these commands, you must be on the network which created the channel in question!): Note: these commands must be run from the network which owns the channel in question!
To set a claim:
- `/msg PyLink claim #channel yournet,net2,net3` (the last parameter is a case-sensitive comma-separated list of networks) - `/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: To list claim networks on a channel:
@ -66,7 +86,8 @@ To clear the claim list for a channel:
- `/msg PyLink claim #channel -` - `/msg PyLink claim #channel -`
### Access control for links (LINKACL) ### 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).
LINKACL allows you to allow or deny networks from linking to your channel. New channels are created using a blacklist by default, though this can be configured via the [`relay::linkacl_use_whitelist` option](https://github.com/jlu5/PyLink/blob/3.0.0/example-conf.yml#L823-L826).
To change between blacklist and whitelist mode: To change between blacklist and whitelist mode:
- `/msg PyLink linkacl whitelist #channel true/false` - `/msg PyLink linkacl whitelist #channel true/false`
@ -76,12 +97,13 @@ To view the LINKACL networks for a channel:
- `/msg PyLink linkacl #channel list` - `/msg PyLink linkacl #channel list`
To add a network to the whitelist **OR** remove a network from the blacklist: To add a network to the whitelist **OR** remove a network from the blacklist:
- `/msg PyLink linkacl #channel allow badnet` - `/msg PyLink linkacl #channel allow goodnet`
To remove a network from the whitelist **OR** add a network to the blacklist: To remove a network from the whitelist **OR** add a network to the blacklist:
- `/msg PyLink linkacl #channel deny goodnet` - `/msg PyLink linkacl #channel deny badnet`
### Adding channel descriptions ### Adding channel descriptions
Starting with PyLink 2.0, you can annotate your channels with a description to use in LINKED: Starting with PyLink 2.0, you can annotate your channels with a description to use in LINKED:
To view the description for a channel: To view the description for a channel:
@ -95,16 +117,17 @@ To remove the description for a channel:
## Dealing with disputes and emergencies ## 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. The best thing to do in the event of a dispute is to delink the problem networks / channels. In order for individual networks to maintain their autonomy, KILLs and network bans (K/G/ZLINE) will most often *not* behave the way you expect them to.
### Kill handling ### 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. Special kill handling was introduced in PyLink 2.0, while in previous versions they were always bounced:
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: 1) If the sender was a server and not a client, reject the kill. (This prevents services messups from wreaking havoc across the relay)
- If the killer has claim access in a channel, forward the KILL as a kick to that channel. 2) If the target and source networks share a [kill share pool](https://github.com/jlu5/PyLink/blob/3.0.0/example-conf.yml#L782-L792), relay the kill as-is.
- Otherwise, bounce the kill silently. 3) Otherwise, check every channel that the kill target is in:
- If the sender is opped or has claim access in a channel, forward the KILL as a kick in that channel.
- Otherwise, bounce the kill silently (i.e. rejoin the user immediately).
### Network bans (K/G/ZLINE) ### Network bans (K/G/ZLINE)

View File

@ -1,9 +1,6 @@
### Version Warning
This version of the document targets the current stable branch of PyLink, and may be considerably outdated when compared to the **devel** branch where active development takes place. When developing new plugins or protocol modules, we highly recommend targeting the devel branch.
# PyLink hooks reference # PyLink hooks reference
***Last updated for 2.0.0 (2018-07-11).*** ***Last updated for 3.1-dev (2021-06-13).***
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. 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`. Each hook payload is formatted as a Python `list`, with three arguments: `numeric`, `command`, and `args`.
@ -22,11 +19,11 @@ The command `:42XAAAAAB PRIVMSG #dev :test` would result in the following raw ho
- `['42XAAAAAB', 'PRIVMSG', {'target': '#dev', 'text': 'test', 'ts': 1451174041}]` - `['42XAAAAAB', 'PRIVMSG', {'target': '#dev', 'text': 'test', 'ts': 1451174041}]`
On UnrealIRCd, because SETHOST is mapped to CHGHOST, `:GL SETHOST blah` would return the raw hook data of this (with the nick converted into UID automatically by the protocol module): On UnrealIRCd, because SETHOST is mapped to CHGHOST, `:jlu5 SETHOST blah` would return the raw hook data of this (with the nick converted into UID automatically by the protocol module):
- `['001ZJZW01', 'CHGHOST', {'ts': 1451174512, 'target': '001ZJZW01', 'newhost': 'blah'}]` - `['001ZJZW01', 'CHGHOST', {'ts': 1451174512, 'target': '001ZJZW01', 'newhost': 'blah'}]`
Some hooks, like MODE, are more complex and can include the entire state of a channel. This will be further described later. `:GL MODE #chat +o PyLink-devel` is converted into (pretty-printed for readability): Some hooks, like MODE, are more complex and can include the entire state of a channel. This will be further described later. `:jlu5 MODE #chat +o PyLink-devel` is converted into (pretty-printed for readability):
``` ```
['001ZJZW01', ['001ZJZW01',
@ -67,9 +64,9 @@ The following hooks represent regular IRC commands sent between servers.
- **KICK**: `{'channel': '#channel', 'target': 'UID1', 'text': 'some reason'}` - **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. - `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': 'Killed (james (absolutely not))', 'userdata': data}` - **KILL**: `{'target': killed, 'text': 'Killed (james (absolutely not))', 'userdata': User(...)}`
- `text` refers to the kill reason. `target` is the target's UID. - `text` refers to the kill reason. `target` is the target's UID.
- 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. - `userdata` includes a `classes.User` instance, containing the information of the killed user.
- **MODE**: `{'target': '#channel', 'modes': [('+m', None), ('+i', None), ('+t', None), ('+l', '3'), ('-o', 'person')], 'channeldata': Channel(...)}` - **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). - `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).
@ -89,10 +86,11 @@ The following hooks represent regular IRC commands sent between servers.
- **PRIVMSG**: `{'target': 'UID3', 'text': 'hi there!'}` - **PRIVMSG**: `{'target': 'UID3', 'text': 'hi there!'}`
- Ditto with NOTICE: STATUSMSG targets (e.g. `@#lounge`) are also allowed here. - Ditto with NOTICE: STATUSMSG targets (e.g. `@#lounge`) are also allowed here.
- **QUIT**: `{'text': 'Quit: Bye everyone!'}` - **QUIT**: `{'text': 'Quit: Bye everyone!', 'userdata': User(...)}`
- `text` corresponds to the quit reason. - `text` corresponds to the quit reason.
- `userdata` includes a `classes.User` instance, containing the information of the killed user.
- **SQUIT**: `{'target': '800', 'users': ['UID1', 'UID2', 'UID6'], 'name': 'some.server', 'uplink': '24X', 'nicks': {'#channel1: ['tester1', 'tester2'], '#channel3': ['somebot']}, 'serverdata': Server(...)` - **SQUIT**: `{'target': '800', 'users': ['UID1', 'UID2', 'UID6'], 'name': 'some.server', 'uplink': '24X', 'nicks': {'#channel1: ['tester1', 'tester2'], '#channel3': ['somebot']}, 'serverdata': Server(...), 'affected_servers': ['SID1', 'SID2', 'SID3']`
- `target` is the SID of the server being split, while `name` is the server's name. - `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. - `users` is a list of all UIDs affected by the netsplit. `nicks` maps channels to lists of nicks affected.
- `serverdata` provides the `classes.Server` object of the server that split off. - `serverdata` provides the `classes.Server` object of the server that split off.
@ -102,9 +100,10 @@ The following hooks represent regular IRC commands sent between servers.
- `oldtopic` denotes the original topic, and `text` indicates the new one being set. - `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 at the protocol level. - `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'}` - **UID**: `{'uid': 'UID1', 'ts': 1234567891, 'nick': 'supercoder', 'realhost': 'localhost', 'host': 'admin.testnet.local', 'ident': ident, 'ip': '127.0.0.1', 'secure': True}`
- This command is used to introduce users; the sender of the message should be the server bursting or announcing the connection. - 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. - `ts` refers to the user's signon time.
- `secure` is a ternary value (True/False/None) that determines whether the user is connected over a secure connection (SSL/TLS). This value is only available on some IRCds: currently UnrealIRCd, P10, Charybdis TS6, and Hybrid; on other servers this will be `None`.
### Extra commands (where supported) ### Extra commands (where supported)
@ -164,6 +163,13 @@ Some hooks do not map directly to IRC commands, but to events that protocol modu
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. 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 ## Changes
* 2021-06-13 (3.1-dev)
- Added the `secure` field to `UID` hooks.
* 2019-07-01 (2.1-alpha2)
- KILL and QUIT hooks now always include a non-empty `userdata` key. Now, if a QUIT message for a killed user is received before the corresponding KILL (or vice versa), only the first message received will have the corresponding hook payload broadcasted.
* 2018-12-27 (2.1-dev)
- Add the `affected_servers` argument to SQUIT hooks.
* 2018-07-11 (2.0.0) * 2018-07-11 (2.0.0)
- Version bump for 2.0 stable release; no meaningful content changes. - Version bump for 2.0 stable release; no meaningful content changes.
* 2018-01-13 (2.0-alpha2) * 2018-01-13 (2.0-alpha2)

View File

@ -1,9 +1,6 @@
### Version Warning
This version of the document targets the current stable branch of PyLink, and may be considerably outdated when compared to the **devel** branch where active development takes place. When developing new plugins or protocol modules, we highly recommend targeting the devel branch.
# PyLink Protocol Module Specification # PyLink Protocol Module Specification
***Last updated for 2.0.0 (2018-07-11).*** ***Last updated for 3.1-dev (2021-06-15).***
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. 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.
@ -41,8 +38,9 @@ This class offers the most flexibility because the protocol module can choose ho
### `protocols.ircs2s_common.IRCS2SProtocol` ### `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. `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.
### For non-IRC protocols: `classes.PyLinkNetworkCoreWithUtils` ### `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.
`PyLinkNetworkCoreWithUtils` contains various state checking and IRC-related utility functions. Originally this abstraction was intended to support non-IRC protocols (Discord, Telegram, Slack, ...), but I (jlu5) no longer support this as a development focus. The main reason being is that in order to keep track of IRC server state correctly, PyLink makes a lot of assumptions specific to IRC (e.g. explicit join/part, mode formats, etc.). Trying to reconcile this with other platforms is a large undertaking and ideally requires a different, more generic protocol specification. (In PyLink 2.x there was a [Discord module](https://github.com/PyLink/pylink-discord) that is no longer supported - see https://jlu5.com/blog/the-trouble-with-pylink for a more in depth explanation as to why.)
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. 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.
@ -85,6 +83,8 @@ Unless otherwise noted, the camel-case variants of command functions (e.g. "`spa
- **`nick`**`(self, source, newnick)` - Changes the nick of a PyLink client. - **`nick`**`(self, source, newnick)` - Changes the nick of a PyLink client.
- **`oper_notice`**`(self, source, target)` - Sends a notice to all operators on the network.
- **`notice`**`(self, source, target, text)` - Sends a NOTICE from a PyLink client or server. - **`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`. This should raise `NotImplementedError` if not supported on a protocol. - **`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.
@ -136,14 +136,20 @@ As of writing, the following protocol capabilities (case-sensitive) are implemen
### Supported protocol capabilities ### 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-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-manage-bot-channels` - whether PyLink can manage which channels the bot itself is in. This is off for platforms such as Discord.
- `can-spawn-clients` - determines whether any spawned clients are real or virtual (mainly for `services_support`). - `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) - `can-track-servers` - determines whether servers are accurately tracked (for `servermaps` and other statistics)
- `freeform-nicks` - if set, nicknames for PyLink's virtual clients are not subject to validity and nick collision checks. This implies the `slash-in-nicks` capability.
- Note: PyLink already allows incoming nicks to be freeform, provided they are encoded correctly and don't cause parsing conflicts (i.e. containing reserved chars on IRC)
- `has-irc-modes` - whether IRC style modes are supported
- `has-statusmsg` - whether STATUSMSG messages (e.g. `@#channel`) are supported - `has-statusmsg` - whether STATUSMSG messages (e.g. `@#channel`) are supported
- `has-ts` - determines whether channel and user timestamps are trackable (and not just spoofed) - `has-ts` - determines whether channel and user timestamps are tracked (and not spoofed)
- `slash-in-hosts` - determines whether `/` is allowed in hostnames - `slash-in-hosts` - determines whether `/` is allowed in hostnames
- `slash-in-nicks` - determines whether `/` is allowed in nicks - `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) - `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) - `underscore-in-hosts` - determines whether `_` is allowed in client hostnames (yes, support for this actually varies by IRCd)
- `virtual-server` - marks the server as virtual, i.e. controlled by protocol module under a different server. Virtual servers are ignored by `rehash` and `disconnect` in the `networks` plugin.
- This is used by pylink-discord as of v0.2.0.
- `visible-state-only` - determines whether channels should be autocleared when the PyLink client leaves (for clientbot, etc.) - `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! - Note: enabling this in a protocol module lets `coremods/handlers` automatically clean up old channels for you!
@ -157,7 +163,7 @@ For reference, the `IRCS2SProtocol` class defines the following by default:
- `can-track-servers` - `can-track-servers`
- `has-ts` - `has-ts`
Whereas `PyLinkNetworkCore` defines no capabilities (i.e. the empty set) by default. Whereas `PyLinkNetworkCore` defines no capabilities (i.e. an empty set) by default.
## PyLink structures ## PyLink structures
In this section, `self` refers to the network object/protocol module instance itself (i.e. from its own perspective). In this section, `self` refers to the network object/protocol module instance itself (i.e. from its own perspective).
@ -204,7 +210,7 @@ Modes are stored not stored as strings, but lists of mode pairs in order to ease
- `self.parse_modes('#chat', ['+ol invalidnick'])`: - `self.parse_modes('#chat', ['+ol invalidnick'])`:
- `[]` - `[]`
- `self.parse_modes('#chat', ['+o GLolol'])`: - `self.parse_modes('#chat', ['+o jlu5'])`:
- `[('+o', '001ZJZW01')]` - `[('+o', '001ZJZW01')]`
Afterwords, a parsed mode list can be applied to channel name or UID using `self.apply_modes(target, parsed_modelist)`. Afterwords, a parsed mode list can be applied to channel name or UID using `self.apply_modes(target, parsed_modelist)`.
@ -214,16 +220,16 @@ Afterwords, a parsed mode list can be applied to channel name or UID using `self
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: 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 <+jlu5> PyLink-devel, eval irc.users[source].modes
<@PyLink-devel> {('i', None), ('x', None), ('w', None), ('o', None)} <@PyLink-devel> {('i', None), ('x', None), ('w', None), ('o', None)}
<+GLolol> PyLink-devel, eval irc.channels['#chat'].modes <+jlu5> PyLink-devel, eval irc.channels['#chat'].modes
<@PyLink-devel> {('n', None), ('t', None)} <@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 `Channel.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 <@jlu5> PyLink-devel, eval irc.channels['#chat'].prefixmodes
<+PyLink-devel> {'op': set(), 'halfop': set(), 'voice': {'38QAAAAAA'}, 'owner': set(), 'admin': set()} <+PyLink-devel> {'op': set(), 'halfop': set(), 'voice': {'38QAAAAAA'}, 'owner': set(), 'admin': set()}
``` ```
@ -260,6 +266,15 @@ In short, protocol modules have some very important jobs. If any of these aren't
7) Declare the correct set of protocol module capabilities to prevent confusing PyLink's plugins. 7) Declare the correct set of protocol module capabilities to prevent confusing PyLink's plugins.
## Changes to this document ## Changes to this document
* 2021-06-15 (3.1-dev)
- Added `oper_notice()` function to send notices to opers (GLOBOPS / OPERWALL on most IRCds)
- Update notes about non-IRC protocols and PyLinkNetworkCoreWithUtils
* 2019-11-02 (2.1-beta1)
- Added protocol capability: `can-manage-bot-channels`
* 2019-10-10 (2.1-beta1)
- Added protocol capability: `has-irc-modes`
* 2019-06-23 (2.1-alpha2)
- Added new protocol capabilities: `virtual-server` and `freeform-nicks`
* 2018-07-11 (2.0.0) * 2018-07-11 (2.0.0)
- Version bump for 2.0 stable release; no meaningful content changes. - Version bump for 2.0 stable release; no meaningful content changes.
* 2018-06-26 (2.0-beta1) * 2018-06-26 (2.0-beta1)

View File

@ -1,8 +1,7 @@
### Version Warning
This version of the document targets the current stable branch of PyLink, and may be considerably outdated when compared to the **devel** branch where active development takes place. When developing new plugins or protocol modules, we highly recommend targeting the devel branch.
# Writing plugins for PyLink # Writing plugins for PyLink
***Last updated for 2.1-alpha2 (2019-06-27).***
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. 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 plugin [`example.py`](../../plugins/example.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.
@ -24,7 +23,7 @@ Functions intended to be hook handlers therefore take in 4 arguments correspondi
#### Return codes for hook handlers #### Return codes for hook handlers
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). As of PyLink 2.0, 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: The following return values are supported so far:
@ -33,10 +32,17 @@ The following return values are supported so far:
Hook handlers may raise exceptions without blocking the event from reaching further handlers; these are caught by PyLink and logged appropriately. Hook handlers may raise exceptions without blocking the event from reaching further handlers; these are caught by PyLink and logged appropriately.
#### Modifying a hook payload
As of PyLink 2.1, it is acceptable to modify a hook event payload in any plugin handler. This can be used for filtering purposes, e.g. Antispam's part/quit message filtering.
You should take extra caution not to corrupt hook payloads, especially ones that relate to state keeping. Otherwise, other plugins may fail to function correctly.
### Hook priorities ### 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). 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: 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 (the default priority for handlers is **100**):
| Module | Commands | Priority | Description | | Module | Commands | Priority | Description |
|-------------------|-----------------|----------|-------------| |-------------------|-----------------|----------|-------------|

View File

@ -46,12 +46,21 @@ pylink:
# be merged into that of the main PyLink service bot. # be merged into that of the main PyLink service bot.
#spawn_services: true #spawn_services: true
# Determines the default ban style that PyLink should use for setting bans (e.g. in Antispam)
# $ident, $host, $realhost, $ip, and $nick are among the supported substitutions here.
# This defaults to "*!*@$host" if not set.
# You should take extra caution that 1) the resulting mask is a valid nick!user@host
# 2) generated bans actually match the target user
#ban_style: "*!*@$host"
#ban_style: "*!$ident@$host" # A possible alternative
# Defines extra directories to look up plugins and protocol modules in. # Defines extra directories to look up plugins and protocol modules in.
# Environment variables (e.g. $HOME) and home folders (~) are expanded here, in that order. # Environment variables (e.g. $HOME) and home folders (~) are expanded here, in that order.
#plugin_dirs: ["~/my-plugins", "~/pylink-contrib-modules/plugins"] #plugin_dirs: ["~/my-plugins", "~/pylink-contrib-modules/plugins"]
#protocol_dirs: ["~/pylink-contrib-modules/protocols"] #protocol_dirs: ["~/pylink-contrib-modules/protocols"]
# Determines whether we should show unknown command errors for service bots. Defaults to True. # Determines whether service bots should return unknown command errors. Defaults to true if not
# speciifed.
#show_unknown_commands: true #show_unknown_commands: true
# Determines whether hideoper modes should be respected in WHOIS replies. # Determines whether hideoper modes should be respected in WHOIS replies.
@ -72,6 +81,19 @@ pylink:
# of all database-enabled plugins to take effect. # of all database-enabled plugins to take effect.
#save_delay: 300 #save_delay: 300
# Determines whether that service bots should join preconfigured channels even if they are empty.
# This can also be overriden per-network via the "join_empty_channels" variable.
# This defaults to False if not set.
#join_empty_channels: false
# Determines where the plugins write their databases. The path can be relative to the directory
# PyLink is run from. Defaults to the current directory.
#data_dir: ""
# Determines where the PID file is written. The path can be relative to the directory PyLink is
# run from. Defaults to the current directory.
#pid_dir: ""
login: login:
# NOTE: for users migrating from PyLink < 1.1, the old login:user/login:password settings # NOTE: for users migrating from PyLink < 1.1, the old login:user/login:password settings
# have been deprecated. We strongly recommend migrating to the new "accounts:" block below, as # have been deprecated. We strongly recommend migrating to the new "accounts:" block below, as
@ -103,6 +125,15 @@ login:
# are supported here as well. # are supported here as well.
#hosts: ["*!*@localhost", "*!*@trusted.isp"] #hosts: ["*!*@localhost", "*!*@trusted.isp"]
# For ADVANCED users: adjusts settings for PyLink's default passlib CryptContext.
# As of PyLink 2.0.3, the default is to use pbkdf2_sha256 for new hashes, while also allowing verifying
# sha512_crypt for compatibility with PyLink < 2.0.3.
# This is configured as a dict of settings, which will be passed into the CryptContext constructor.
# See https://passlib.readthedocs.io/en/stable/lib/passlib.context.html for a list of valid options.
# Changes to this setting require a rehash to apply.
#cryptcontext_settings:
#schemes: ["pbkdf2_sha256", "sha512_crypt"]
permissions: permissions:
# Permissions blocks in PyLink are define as a mapping of PyLink targets (i.e. hostmasks or # Permissions blocks in PyLink are define as a mapping of PyLink targets (i.e. hostmasks or
# exttargets) to lists of permission nodes. You can find a list of permissions that PyLink and # exttargets) to lists of permission nodes. You can find a list of permissions that PyLink and
@ -138,7 +169,6 @@ servers:
# CHANGE THIS to some abbreviation representing your network; usually # CHANGE THIS to some abbreviation representing your network; usually
# something 3-5 characters should be good. # something 3-5 characters should be good.
inspnet: inspnet:
# Server IP, port, and passwords. The ip: field also supports resolving # Server IP, port, and passwords. The ip: field also supports resolving
# hostnames. # hostnames.
ip: 127.0.0.1 ip: 127.0.0.1
@ -155,63 +185,66 @@ servers:
# Hostname we will use to connect to the remote server # Hostname we will use to connect to the remote server
hostname: "pylink.yournet.local" hostname: "pylink.yournet.local"
# Sets the server ID (SID) that the main PyLink server should use. # Sets the server ID (SID) that the main PyLink server should use. For TS6-like servers
# For TS6-like servers (InspIRCd, Charybdis, UnrealIRCd, etc.), this # (InspIRCd, Charybdis, UnrealIRCd, etc.), this must be three characters:
# must be three characters: the first char must be a digit [0-9], and # the first char must be a digit [0-9], and the remaining two may be either uppercase
# the remaining two may be either uppercase letters [A-Z] or digits. # letters [A-Z] or digits.
sid: "0PY" sid: "0PY"
# Server ID range: this specifies the range of server IDs that PyLink # Server ID range: this specifies the range of server IDs that PyLink# may use for
# may use for subservers such as relay. On TS6, this should be a # subservers such as Relay. On TS6, this should be a combination of digits, letters, and #'s.
# combination of digits, letters, and #'s. Each # denotes a range (0-9A-Z) # Each # denotes a range (0-9A-Z) of characters that can be used by PyLink to generate SIDs.
# of characters that can be used by PyLink to generate appropriate SIDs.
# You will want to make sure no other servers are using this range. # You will want to make sure no other servers are using this range.
# There must be at least one # in this entry. # There must be at least one # in this entry.
sidrange: "8##" sidrange: "8##"
# Sets the protocol module to use for this network - see the README for a # Sets the protocol module to use for this network - see the README for a list of supported
# list of supported IRCds. # IRCds.
protocol: "inspircd" protocol: "inspircd"
# Sets the max nick length for the network. It is important that this is # InspIRCd specific option: sets the target InspIRCd protocol version.
# set correctly, or PyLink might introduce a nick that is too long and # Valid values include:
# cause netsplits! This defaults to 30 if not set. # "insp3" - InspIRCd 3.x (1205) [DEFAULT]
# "insp20" - InspIRCd 2.0.x (1202) [legacy, deprecated]
#target_version: insp3
# 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 maxnicklen: 30
# Toggles SSL for this network - you should seriously consider using TLS in all your links # Toggles TLS/SSL for this network - you should seriously consider using TLS in all server links
# for optimal security. Defaults to False if not specified. # for optimal security. Defaults to False if not specified.
ssl: true ssl: true
# Optional SSL cert/key to pass to the uplink server. # Optional TLS cert/key to pass to the uplink server.
#ssl_certfile: pylink-cert.pem #ssl_certfile: pylink-cert.pem
#ssl_keyfile: pylink-key.pem #ssl_keyfile: pylink-key.pem
# New in 2.0: Determines whether the target server's TLS certificate hostnames should be # New in PyLink 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 # 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 # networks and others linked to via a hostname. It depends on ssl_accept_invalid_certs being
# *disabled* to take effect. # *disabled* to take effect.
#ssl_validate_hostname: true #ssl_validate_hostname: true
# New in 2.0: When enabled, this disables TLS certificate validation on the target network. # New in PyLink 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 # 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, # (where bad certs are accepted). This disables the ssl_validate_hostname option, forcing it to
# effectively forcing it to be false. # be false.
#ssl_accept_invalid_certs: false #ssl_accept_invalid_certs: false
# Optionally, you can set this option to verify the SSL certificate fingerprint of your # Optionally, you can set this option to verify the TLS certificate fingerprint of your
# uplink. This check works regardless of whether ssl_validate_hostname and # uplink. This check works independently of ssl_validate_hostname and ssl_accept_invalid_certs.
# ssl_accept_invalid_certs are enabled.
#ssl_fingerprint: "e0fee1adf795c84eec4735f039503eb18d9c35cc" #ssl_fingerprint: "e0fee1adf795c84eec4735f039503eb18d9c35cc"
# This sets the hash type for the fingerprint (md5, sha1, sha256, etc.) # This sets the hash type for the above TLS certificate fingerprint.
# Valid values include md5 and sha1-sha512, though others may be # Valid values include md5 and sha1-sha512, and others that may be supported depending on
# supported depending on your system: see # your system: see https://docs.python.org/3/library/hashlib.html
# https://docs.python.org/3/library/hashlib.html # This defaults to sha256 if not set.
# This setting defaults to sha256.
#ssl_fingerprint_type: sha256 #ssl_fingerprint_type: sha256
# Sets autoconnect delay - comment this out or set the value below 1 to # Sets autoconnect delay - comment this out or set the value below 1 to disable autoconnect
# disable autoconnect entirely. # entirely.
autoconnect: 10 autoconnect: 10
# Optional autoconnect settings: # Optional autoconnect settings:
@ -235,21 +268,20 @@ servers:
# https://docs.python.org/3/library/codecs.html#standard-encodings # https://docs.python.org/3/library/codecs.html#standard-encodings
# Changing this setting requires a disconnect and reconnect of the corresponding network # Changing this setting requires a disconnect and reconnect of the corresponding network
# to apply. # to apply.
# This setting is EXPERIMENTAL as of PyLink 1.2.x.
#encoding: utf-8 #encoding: utf-8
# Sets the ping frequency in seconds (i.e. how long we should wait between # Sets the ping frequency in seconds (i.e. how long we should wait between sending pings to
# sending pings to our uplink). When more than two consecutive pings are missed, # our uplink). When more than two consecutive pings are missed, PyLink will disconnect with
# PyLink will disconnect with a ping timeout. This defaults to 90 if not set. # a ping timeout. This defaults to 90 if not set.
#pingfreq: 90 #pingfreq: 90
# If relay nick tagging is disabled, this option specifies a list of nick globs to always # If relay nick tagging is disabled, this option specifies a list of nick globs to always
# tag when introducing remote users *onto* this network. # tag when introducing remote users *onto* this network.
#relay_forcetag_nicks: ["someuser", "Guest*"] #relay_forcetag_nicks: ["someuser", "Guest*"]
# Sets the suffix that relay subservers on this network should use. # Sets the suffix that relay subservers on this network should use. If not specified per
# If not specified per network, this falls back to the value at # network, this falls back to the value at relay::server_suffix, or the string "relay" if
# relay::server_suffix, or "relay" if that is also not set. # that is also not set.
#relay_server_suffix: "relay.yournet.net" #relay_server_suffix: "relay.yournet.net"
# Determines whether relay will tag nicks on this network. This overrides the relay::tag_nicks # Determines whether relay will tag nicks on this network. This overrides the relay::tag_nicks
@ -273,13 +305,6 @@ servers:
ip: ::1 ip: ::1
port: 8067 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
# (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 # 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. # these two fields to "*" and enable SSL with a cert and key file.
recvpass: "coffee" recvpass: "coffee"
@ -313,9 +338,13 @@ servers:
port: 45454 port: 45454
# When the IP field is set to a hostname, the "ipv6" option determines whether IPv4 or IPv6 # 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 # addresses should be used when resolving it.
# explicit bindhost instead, which will let the the address type be automatically detected. # As of PyLink 3.1, this defaults to null, falling back to the system's default preferences
#ipv6: false # if not set (e.g. /etc/gai.conf on Linux). Previous versions default to making IPv4 connections only.
# This option is overridden by "bindhost" if it is also provided.
#ipv6: null
# Specifies the IP to make outgoing connections from, for multi-homed hosts.
#bindhost: 1111:2222:3333:4444 #bindhost: 1111:2222:3333:4444
# Note: if you are actually using dynamic DNS for an IRC link, consider enabling # Note: if you are actually using dynamic DNS for an IRC link, consider enabling
@ -425,6 +454,11 @@ servers:
# and earlier. # and earlier.
#ircd: charybdis #ircd: charybdis
# Determines whether PyLink servers should be marked hidden in /links and /map.
# This only takes effect if the 'flatten_links' option in the IRCd is enabled.
# Defaults to false if not set.
#hidden: false
# Sample Clientbot configuration, if you want to connect PyLink as a bot to relay somewhere # Sample Clientbot configuration, if you want to connect PyLink as a bot to relay somewhere
# (or do other bot things). # (or do other bot things).
magicnet: magicnet:
@ -472,7 +506,7 @@ servers:
# Message throttling: when set to a non-zero value, only one message will be sent every X # 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 # seconds. If your bot is constantly running into Excess Flood errors, raising this to
# something like 0.5 or 1.0 should help. Defaults to 0.005 if not set. # something like 0.5 or 1.0 should help. Since PyLink 2.0.2, this defaults to 0 if not set.
throttle_time: 0.3 throttle_time: 0.3
# Determines whether messages from unknown clients (servers, clients not sharing in a -n # Determines whether messages from unknown clients (servers, clients not sharing in a -n
@ -507,6 +541,10 @@ servers:
# - "MODE $nick +B" # - "MODE $nick +B"
# - "NOTICE somebody :hello, i've connected" # - "NOTICE somebody :hello, i've connected"
# Determines whether we should always attempt to rejoin channels we've been removed from.
# These attempts take place in the same interval as pingfreq. Defaults to false if not set.
#always_autorejoin: true
# Determines whether oper statuses should be tracked on this Clientbot network. This # Determines whether oper statuses should be tracked on this Clientbot network. This
# defaults to False for the best security, since oper status may allow more access to the # defaults to False for the best security, since oper status may allow more access to the
# entire PyLink service than what's desired, even when PyLink is only connected as a bot. # entire PyLink service than what's desired, even when PyLink is only connected as a bot.
@ -532,6 +570,10 @@ plugins:
# Ctcp plugin: handles basic CTCP replies (VERSION, etc) towards service bots. # Ctcp plugin: handles basic CTCP replies (VERSION, etc) towards service bots.
- ctcp - ctcp
# Servprotect plugin: disconnects from networks if too many kills or nick collisions to
# PyLink clients are received. Requires the cachetools Python library.
- servprotect
# Relay plugin: Transparent server-side relay between channels (like Janus). See # Relay plugin: Transparent server-side relay between channels (like Janus). See
# the relay: block below for configuration. # the relay: block below for configuration.
#- relay #- relay
@ -562,10 +604,6 @@ plugins:
# You *will need* to configure it via the "antispam:" configuration block below. # You *will need* to configure it via the "antispam:" configuration block below.
#- antispam #- antispam
# Servprotect plugin: disconnects from networks if too many kills or nick collisions to
# PyLink clients are received.
#- servprotect
# Global plugin: Janus-style global plugin; announces messages to all channels the PyLink # Global plugin: Janus-style global plugin; announces messages to all channels the PyLink
# client is in. # client is in.
#- global #- global
@ -613,18 +651,22 @@ logging:
"#services": "#services":
loglevel: INFO loglevel: INFO
files: # The directory where the log files are written. The path can be relative to the directory
# Logs to file targets. These will be placed in the log/ folder in the # PyLink is run from. Defaults to "log/".
# PyLink directory, with a filename based on the current instance name #log_dir: "log"
# and the target name: instancename-targetname.log
# When running with ./pylink, this will create log/pylink-errors.log files:
# When running with ./pylink someconf.yml, this will create log/someconf-errors.log # Logs to file targets. These will be placed in the folder specified by log_dir with a
# filename based on the current instance name and the target name:
# instancename-targetname.log
# When running with "pylink", this will create log/pylink-errors.log
# When running with "pylink someconf.yml", this will create log/someconf-errors.log
"errors": "errors":
loglevel: ERROR loglevel: ERROR
# Ditto above. When running with ./pylink, it will use log/pylink-commands.log # Ditto above. When running with "pylink", it will use log/pylink-commands.log
# When running with ./pylink someconf.yml, this will create log/someconf-commands.log # When running with "pylink someconf.yml", this will create log/someconf-commands.log
"commands": "commands":
loglevel: INFO loglevel: INFO
@ -638,8 +680,8 @@ logging:
# in the format pylink-commands.log, pylink-commands.log.1, pylink-commands.log.2, etc. # in the format pylink-commands.log, pylink-commands.log.1, pylink-commands.log.2, etc.
# If either max_bytes or backup_count is 0, log rotation will be disabled. # If either max_bytes or backup_count is 0, log rotation will be disabled.
# Max amount of bytes per file, before rotation is done. Defaults to 50 MiB (52428800 bytes). # Max amount of bytes per file before rotation is done. Defaults to 20 MiB (20971520 bytes).
#max_bytes: 52428800 #max_bytes: 20971520
# Amount of backups to make. Defaults to 5. # Amount of backups to make. Defaults to 5.
#backup_count: 5 #backup_count: 5
@ -651,44 +693,48 @@ changehost:
# Sets the networks where Changehost should be enabled. Please note: changehost does NOT support # Sets the networks where Changehost should be enabled. Please note: changehost does NOT support
# arbitrarily cloaking clients introduced by PyLink (e.g. relay clients), as doing so would make # arbitrarily cloaking clients introduced by PyLink (e.g. relay clients), as doing so would make
# ban matching impossible. In these cases, it is the remote admin's job to turn on cloaking on # ban matching impossible. In these cases, it is the remote admin's job to turn on cloaking on
# their IRCd! # their IRCd.
# You can also add to this list of enabled networks by setting "servers::<server name>::changehost_enable"
# to true.
enabled_nets: enabled_nets:
- inspnet - inspnet
- ts6net - ts6net
# Sets the networks where Changehost hosts should be enforced: that is, any attempts # Sets the networks where Changehost hosts should be enforced: that is, any attempts
# by the user or other services to overwrite a host will be reverted. # by the user or other services to overwrite a host will be reverted.
# You can also add to this list of enabled networks by setting "servers::<server name>::changehost_enforce"
# to true.
#enforced_nets: #enforced_nets:
# - inspnet # - inspnet
# Sets the masks that Changehost enforcement should ignore: these can be users with certain # Sets the masks that Changehost enforcement should ignore: these can be users with certain
# hosts, exttargets, etc. # hosts, exttargets, etc.
# Since PyLink 2.1, you can also add to this list on a per-network basis by adding options under
# "servers::<server name>::changehost_enforce_exceptions".
enforce_exceptions: enforce_exceptions:
- "*!*@yournet/staff/*" - "*!*@yournet/staff/*"
#- "$account" #- "$account"
# Determines whether Changehost rules should also match the host portion of a mask by IP and # Determines whether Changehost rules should also match the host portion of a mask by IP and
# real hosts. These default to false. # real hosts. These default to false. You can override these on a per-network basis by setting
# "servers::<server name>::changehost_match_ip" or "servers::<server name>::changehost_match_realhosts".
#match_ip: false #match_ip: false
#match_realhosts: false #match_realhosts: false
# This sets the hostmasks that Changehost should look for. Whenever someone # This sets the hostmasks that Changehost should look for. Whenever someone with a matching nick!user@host
# with a matching nick!user@host connects, their host will be set to the # connects, their host will be set to the text defined. The following substitutions are available here:
# text defined. The following substitutions are available here:
# $uid, $ts (time of connection), $nick, $realhost, $ident, and $ip. # $uid, $ts (time of connection), $nick, $realhost, $ident, and $ip.
# Invalid characters in hosts are replaced with a "-". # Invalid characters in hosts are replaced with a "-".
# Also, make sure you quote each entry so the YAML parser treats them as # Also, make sure you quote each entry so the YAML parser treats them as raw strings.
# raw strings. # Since PyLink 2.1, you can also add to this list on a per-network basis by adding options under
# "servers::<server name>::changehost_hosts".
hosts: hosts:
# Here are some examples. Note that to keep your users' details private, you should probably refrain
# Here are some examples. Note that to keep your users' details # from using $ip or $realhost, in these hostmasks, unless cloaking is already disabled.
# private, you should probably refrain from using $ip or $realhost,
# in these hostmasks, unless cloaking is already disabled.
"*!yourname@*.yournet.com": "$nick.opers.yournet.com" "*!yourname@*.yournet.com": "$nick.opers.yournet.com"
"*!*@localhost": "some-server.hostname" "*!*@localhost": "some-server.hostname"
# Freenode-style masks are possible with this (though without the # Freenode-style masks are possible with this (though without the hashing)
# hashing)
"*!*@bnc-server.yournet.com": "yournet/bnc-users/$ident" "*!*@bnc-server.yournet.com": "yournet/bnc-users/$ident"
"*!*@ircip?.mibbit.com": "$ident.$realhost" "*!*@ircip?.mibbit.com": "$ident.$realhost"
"WebchatUser*!*@*": "webchat/users/$ident" "WebchatUser*!*@*": "webchat/users/$ident"
@ -920,7 +966,7 @@ stats:
#masshighlight: #masshighlight:
# This block configures options for antispam's mass highlight blocking. It can also be # 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 # overridden (as an entire block) per-network by copying its options to:
# servers::<server name>::antispam_masshighlight # servers::<server name>::antispam_masshighlight
# Determines whether mass highlight prevention should be enabled. Defaults to false if not # Determines whether mass highlight prevention should be enabled. Defaults to false if not
@ -942,7 +988,7 @@ stats:
#textfilter: #textfilter:
# This block configures options for antispam's text spamfilters. It can also be overridden # 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 # (as an entire block) per-network by copying its options to:
# servers::<server name>::antispam_textfilter # servers::<server name>::antispam_textfilter
# Determines whether text spamfilters should be enabled. Defaults to false if not set. # Determines whether text spamfilters should be enabled. Defaults to false if not set.
@ -951,11 +997,15 @@ stats:
# Sets the punishment that Antispam's text spamfilter should use. # Sets the punishment that Antispam's text spamfilter should use.
# Valid values include "kill", "kick", "ban", "quiet", and combinations of these strung # 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. # 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 # If you want to use Antispam with PM monitoring (see the "watch_pms" option below), you
# not channel-specific (such as "kill" or "block"). # will want to include at least one punishment that is not channel-specific
# ("kill" or "block").
# Punishments not supported in a context or network (e.g. kicking for PM spam or "quiet"
# on a network that doesn't support it) will be silently dropped if others succeed.
# If no punishments succeed, then a warning will be logged.
#punishment: kick+ban+block #punishment: kick+ban+block
# Sets the kick / kill message used when the text spamfilter is triggered. # Sets the kick / kill message used when a message triggers the text spamfilter.
#reason: "Spam is prohibited" #reason: "Spam is prohibited"
# Determines whether PMs to PyLink clients should also be tracked to prevent spam. # Determines whether PMs to PyLink clients should also be tracked to prevent spam.
@ -968,11 +1018,33 @@ stats:
# This defaults to false if not set. # This defaults to false if not set.
#watch_pms: false #watch_pms: false
# Configures an (ASCII case-insensitive) list of bad strings to block in messages (PRIVMSG, NOTICE). # Configures a case-insensitive list of bad strings to block in messages (PRIVMSG, NOTICE).
# Globs are supported; use them to your advantage here. # Globs are supported; use them to your advantage here.
# You can also define server specific lists of bad strings by defining # You can also define server specific lists of bad strings by defining
# servers::<server name>::antispam_textfilters_globs # servers::<server name>::antispam_textfilter_globs
# the contents of which will be *merged* into the global list of bad strings specified below. # the contents of which will be *merged* into the global list of bad strings specified below.
#textfilter_globs: #textfilter_globs:
# - "*very bad don't say this*" # - "*very bad don't say this*"
# - "TWFkZSB5b3UgbG9vayE=" # - "TWFkZSB5b3UgbG9vayE="
#partquit:
# This configures Antispam's part / quit message filter for plugins like Relay. It can also be
# overridden (as an entire block) per-network by copying its options to:
# servers::<server name>::antispam_partquit
# Determines whether part and quit messages matching any text spamfilters will be filtered.
# These default to true if not set.
#watch_parts: false
#watch_quits: false
# Sets the message to use when a part or quit message is filtered.
#part_filter_message: Reason filtered
#quit_filter_message: Reason filtered
# Configures a case-insensitive list of bad strings to block in part and quit messages.
# 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_partquit_globs
# the contents of which will be *merged* into the global list of bad strings specified below.
#partquit_globs:
# - "*some-spammy-site.xyz*"

View File

@ -4,10 +4,11 @@ PyLink IRC Services launcher.
""" """
import os import os
import sys
import signal import signal
import sys
import time import time
from pylinkirc import world, conf, __version__, real_version
from pylinkirc import __version__, conf, real_version, world
try: try:
import psutil import psutil
@ -17,7 +18,6 @@ except ImportError:
args = {} args = {}
def _main(): def _main():
# FIXME: we can't pass logging on to conf until we set up the config...
conf.load_conf(args.config) conf.load_conf(args.config)
from pylinkirc.log import log from pylinkirc.log import log
@ -25,28 +25,33 @@ def _main():
# Write and check for an existing PID file unless specifically told not to. # Write and check for an existing PID file unless specifically told not to.
if not args.no_pid: if not args.no_pid:
pidfile = '%s.pid' % conf.confname pid_dir = conf.conf['pylink'].get('pid_dir', '')
has_pid = False pidfile = os.path.join(pid_dir, '%s.pid' % conf.confname)
pid_exists = False
pid = None pid = None
if os.path.exists(pidfile): if os.path.exists(pidfile):
try:
with open(pidfile) as f:
pid = int(f.read())
except OSError:
log.exception("Could not read PID file %s:", pidfile)
else:
pid_exists = True
has_pid = True
if psutil is not None and os.name == 'posix': if psutil is not None and os.name == 'posix':
# FIXME: Haven't tested this on other platforms, so not turning it on by default. # FIXME: Haven't tested this on other platforms, so not turning it on by default.
with open(pidfile) as f:
try: try:
pid = int(f.read())
proc = psutil.Process(pid) proc = psutil.Process(pid)
except psutil.NoSuchProcess: # Process doesn't exist! except psutil.NoSuchProcess: # Process doesn't exist!
has_pid = False pid_exists = False
log.info("Ignoring stale PID %s from PID file %r: no such process exists.", pid, pidfile) log.info("Ignoring stale PID %s from PID file %r: no such process exists.", pid, pidfile)
else: else:
# This PID got reused for something that isn't us? # This PID got reused for something that isn't us?
if not any('pylink' in arg.lower() for arg in proc.cmdline()): if not any('pylink' in arg.lower() for arg in proc.cmdline()):
log.info("Ignoring stale PID %s from PID file %r: process command line %r is not us", pid, pidfile, proc.cmdline()) log.info("Ignoring stale PID %s from PID file %r: process command line %r is not us", pid, pidfile, proc.cmdline())
has_pid = False pid_exists = False
if has_pid: if pid and pid_exists:
if args.rehash: if args.rehash:
os.kill(pid, signal.SIGUSR1) os.kill(pid, signal.SIGUSR1)
log.info('OK, rehashed PyLink instance %s (config %r)', pid, args.config) log.info('OK, rehashed PyLink instance %s (config %r)', pid, args.config)
@ -79,7 +84,7 @@ def _main():
world._should_remove_pid = True world._should_remove_pid = True
log.error('Cannot stop/rehash PyLink: no process with PID %s exists.', pid) log.error('Cannot stop/rehash PyLink: no process with PID %s exists.', pid)
else: else:
log.error('Cannot stop/rehash PyLink: PID file %r does not exist.', pidfile) log.error('Cannot stop/rehash PyLink: PID file %r does not exist or cannot be read.', pidfile)
sys.exit(1) sys.exit(1)
world._should_remove_pid = True world._should_remove_pid = True
@ -117,6 +122,7 @@ def _main():
elif os.name == 'posix': elif os.name == 'posix':
sys.stdout.write("\x1b]2;PyLink %s\x07" % __version__) sys.stdout.write("\x1b]2;PyLink %s\x07" % __version__)
if not args.no_pid:
# Write the PID file only after forking. # Write the PID file only after forking.
with open(pidfile, 'w') as f: with open(pidfile, 'w') as f:
f.write(str(os.getpid())) f.write(str(os.getpid()))
@ -172,7 +178,7 @@ def main():
parser.add_argument("-r", "--restart", help="restarts the PyLink instance with the given config file", 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("-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("-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("-d", "--daemonize", help="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("-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-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='') parser.add_argument('--trace-ignore-dirs', help='comma-separated list of extra directories to ignore when tracing', action='store', default='')

26
log.py
View File

@ -10,14 +10,13 @@ import logging
import logging.handlers import logging.handlers
import os import os
from . import world, conf from . import conf, world
__all__ = ['log']
# Stores a list of active file loggers. # Stores a list of active file loggers.
fileloggers = [] fileloggers = []
logdir = os.path.join(os.getcwd(), 'log')
os.makedirs(logdir, exist_ok=True)
# TODO: perhaps make this format configurable? # TODO: perhaps make this format configurable?
_format = '%(asctime)s [%(levelname)s] %(message)s' _format = '%(asctime)s [%(levelname)s] %(message)s'
logformatter = logging.Formatter(_format) logformatter = logging.Formatter(_format)
@ -35,7 +34,7 @@ world.console_handler.setFormatter(logformatter)
world.console_handler.setLevel(_get_console_log_level()) world.console_handler.setLevel(_get_console_log_level())
# Get the main logger object; plugins can import this variable for convenience. # Get the main logger object; plugins can import this variable for convenience.
log = logging.getLogger() log = logging.getLogger('pylinkirc')
log.addHandler(world.console_handler) log.addHandler(world.console_handler)
# This is confusing, but we have to set the root logger to accept all events. Only this way # This is confusing, but we have to set the root logger to accept all events. Only this way
@ -47,20 +46,28 @@ def _make_file_logger(filename, level=None):
""" """
Initializes a file logging target with the given filename and level. Initializes a file logging target with the given filename and level.
""" """
logconf = conf.conf.get('logging', {})
logdir = logconf.get('log_dir')
if logdir is None:
logdir = os.path.join(os.getcwd(), 'log')
os.makedirs(logdir, exist_ok=True)
# Use log names specific to the current instance, to prevent multiple # Use log names specific to the current instance, to prevent multiple
# PyLink instances from overwriting each others' log files. # PyLink instances from overwriting each others' log files.
target = os.path.join(logdir, '%s-%s.log' % (conf.confname, filename)) target = os.path.join(logdir, '%s-%s.log' % (conf.confname, filename))
logrotconf = conf.conf.get('logging', {}).get('filerotation', {}) logrotconf = logconf.get('filerotation', {})
# Max amount of bytes per file, before rotation is done. Defaults to 50 MiB. # Max amount of bytes per file, before rotation is done. Defaults to 20 MiB.
maxbytes = logrotconf.get('max_bytes', 52428800) maxbytes = logrotconf.get('max_bytes', 20971520)
# Amount of backups to make (e.g. pylink-debug.log, pylink-debug.log.1, pylink-debug.log.2, ...) # Amount of backups to make (e.g. pylink-debug.log, pylink-debug.log.1, pylink-debug.log.2, ...)
# Defaults to 5. # Defaults to 5.
backups = logrotconf.get('backup_count', 5) backups = logrotconf.get('backup_count', 5)
filelogger = logging.handlers.RotatingFileHandler(target, maxBytes=maxbytes, backupCount=backups) filelogger = logging.handlers.RotatingFileHandler(target, maxBytes=maxbytes, backupCount=backups, encoding='utf-8')
filelogger.setFormatter(logformatter) filelogger.setFormatter(logformatter)
# If no log level is specified, use the same one as the console logger. # If no log level is specified, use the same one as the console logger.
@ -153,4 +160,3 @@ class PyLinkChannelLogger(logging.Handler):
return return
else: else:
self.called = False self.called = False

View File

@ -1,8 +1,6 @@
# antispam.py: Basic services-side spamfilters for IRC # antispam.py: Basic services-side spamfilters for IRC
import ircmatch from pylinkirc import conf, utils
from pylinkirc import utils, world, conf
from pylinkirc.log import log from pylinkirc.log import log
mydesc = ("Provides anti-spam functionality.") mydesc = ("Provides anti-spam functionality.")
@ -11,6 +9,78 @@ sbot = utils.register_service("antispam", default_nick="AntiSpam", desc=mydesc)
def die(irc=None): def die(irc=None):
utils.unregister_service("antispam") utils.unregister_service("antispam")
_UNICODE_CHARMAP = {
'A': 'AΑА𝐀𝐴𝑨𝒜𝓐𝔄𝔸𝕬𝖠𝗔𝘈𝘼𝙰𝚨𝛢𝜜𝝖𝞐',
'B': 'ΒВв𐌁𝐁𝐵𝑩𝓑𝔅𝔹𝕭𝖡𝗕𝘉𝘽𝙱𝚩𝛣𝜝𝝗𝞑',
'C': 'CϹС𐌂𝐂𝐶𝑪𝒞𝓒𝕮𝖢𝗖𝘊𝘾𝙲',
'D': 'D𝐃𝐷𝑫𝒟𝓓𝔇𝔻𝕯𝖣𝗗𝘋𝘿𝙳',
'E': 'EΕЕ𝐄𝐸𝑬𝓔𝔈𝔼𝕰𝖤𝗘𝘌𝙀𝙴𝚬𝛦𝜠𝝚𝞔',
'F': 'FϜ𝐅𝐹𝑭𝓕𝔉𝔽𝕱𝖥𝗙𝘍𝙁𝙵𝟊',
'G': 'Ԍԍ𝐆𝐺𝑮𝒢𝓖𝔊𝔾𝕲𝖦𝗚𝘎𝙂𝙶',
'H': 'ΗНн𝐇𝐻𝑯𝓗𝕳𝖧𝗛𝘏𝙃𝙷𝚮𝛨𝜢𝝜𝞖',
'J': 'JЈ𝐉𝐽𝑱𝒥𝓙𝔍𝕁𝕵𝖩𝗝𝘑𝙅𝙹',
'K': 'KΚК𝐊𝐾𝑲𝒦𝓚𝔎𝕂𝕶𝖪𝗞𝘒𝙆𝙺𝚱𝛫𝜥𝝟𝞙',
'L': '𝐋𝐿𝑳𝓛𝔏𝕃𝕷𝖫𝗟𝘓𝙇𝙻',
'M': 'MΜϺМ𐌑𝐌𝑀𝑴𝓜𝔐𝕄𝕸𝖬𝗠𝘔𝙈𝙼𝚳𝛭𝜧𝝡𝞛',
'N': 'Ν𝐍𝑁𝑵𝒩𝓝𝔑𝕹𝖭𝗡𝘕𝙉𝙽𝚴𝛮𝜨𝝢𝞜',
'P': 'PΡРᴘᴩ𝐏𝑃𝑷𝒫𝓟𝔓𝕻𝖯𝗣𝘗𝙋𝙿𝚸𝛲𝜬𝝦𝞠',
'Q': 'Q𝐐𝑄𝑸𝒬𝓠𝔔𝕼𝖰𝗤𝘘𝙌𝚀',
'R': 'RƦʀ𝐑𝑅𝑹𝓡𝕽𝖱𝗥𝘙𝙍𝚁',
'S': 'SЅՏ𝐒𝑆𝑺𝒮𝓢𝔖𝕊𝕾𝖲𝗦𝘚𝙎𝚂',
'T': 'TΤτТт𐌕𝐓𝑇𝑻𝒯𝓣𝔗𝕋𝕿𝖳𝗧𝘛𝙏𝚃𝚻𝛕𝛵𝜏𝜯𝝉𝝩𝞃𝞣𝞽',
'U': 'UՍ𝐔𝑈𝑼𝒰𝓤𝔘𝕌𝖀𝖴𝗨𝘜𝙐𝚄',
'V': 'VѴ٧۷𝐕𝑉𝑽𝒱𝓥𝔙𝕍𝖁𝖵𝗩𝘝𝙑𝚅',
'W': 'WԜ𝐖𝑊𝑾𝒲𝓦𝔚𝕎𝖂𝖶𝗪𝘞𝙒𝚆',
'X': 'XΧХ𐌗𐌢𝐗𝑋𝑿𝒳𝓧𝔛𝕏𝖃𝖷𝗫𝘟𝙓𝚇𝚾𝛸𝜲𝝬𝞦',
'Y': 'YΥϒУҮ𝐘𝑌𝒀𝒴𝓨𝔜𝕐𝖄𝖸𝗬𝘠𝙔𝚈𝚼𝛶𝜰𝝪𝞤',
'Z': 'ZΖ𝐙𝑍𝒁𝒵𝓩𝖅𝖹𝗭𝘡𝙕𝚉𝚭𝛧𝜡𝝛𝞕',
'a': 'aɑαа𝐚𝑎𝒂𝒶𝓪𝔞𝕒𝖆𝖺𝗮𝘢𝙖𝚊𝛂𝛼𝜶𝝰𝞪',
'b': 'bƄЬ𝐛𝑏𝒃𝒷𝓫𝔟𝕓𝖇𝖻𝗯𝘣𝙗𝚋',
'c': 'cϲс𝐜𝑐𝒄𝒸𝓬𝔠𝕔𝖈𝖼𝗰𝘤𝙘𝚌',
'd': 'dԁ𝐝𝑑𝒅𝒹𝓭𝔡𝕕𝖉𝖽𝗱𝘥𝙙𝚍',
'e': 'eеҽ𝐞𝑒𝒆𝓮𝔢𝕖𝖊𝖾𝗲𝘦𝙚𝚎',
'f': 'fſϝք𝐟𝑓𝒇𝒻𝓯𝔣𝕗𝖋𝖿𝗳𝘧𝙛𝚏𝟋',
'g': 'gƍɡց𝐠𝑔𝒈𝓰𝔤𝕘𝖌𝗀𝗴𝘨𝙜𝚐',
'h': 'hһհ𝐡𝒉𝒽𝓱𝔥𝕙𝖍𝗁𝗵𝘩𝙝𝚑',
'i': 'iıɩɪιіӏ𝐢𝑖𝒊𝒾𝓲𝔦𝕚𝖎𝗂𝗶𝘪𝙞𝚒𝚤𝛊𝜄𝜾𝝸𝞲',
'j': 'jϳј𝐣𝑗𝒋𝒿𝓳𝔧𝕛𝖏𝗃𝗷𝘫𝙟𝚓',
'k': 'k𝐤𝑘𝒌𝓀𝓴𝔨𝕜𝖐𝗄𝗸𝘬𝙠𝚔',
'l': '',
'm': 'ⅿm',
'n': 'nոռ𝐧𝑛𝒏𝓃𝓷𝔫𝕟𝖓𝗇𝗻𝘯𝙣𝚗',
'o': 'οо',
'p': 'pρϱр𝐩𝑝𝒑𝓅𝓹𝔭𝕡𝖕𝗉𝗽𝘱𝙥𝚙𝛒𝛠𝜌𝜚𝝆𝝔𝞀𝞎𝞺𝟈',
'q': 'qԛգզ𝐪𝑞𝒒𝓆𝓺𝔮𝕢𝖖𝗊𝗾𝘲𝙦𝚚',
'r': 'rг𝐫𝑟𝒓𝓇𝓻𝔯𝕣𝖗𝗋𝗿𝘳𝙧𝚛',
's': 'sƽѕ𝐬𝑠𝒔𝓈𝓼𝔰𝕤𝖘𝗌𝘀𝘴𝙨𝚜',
't': 't𝐭𝑡𝒕𝓉𝓽𝔱𝕥𝖙𝗍𝘁𝘵𝙩𝚝',
'u': 'uʋυս𝐮𝑢𝒖𝓊𝓾𝔲𝕦𝖚𝗎𝘂𝘶𝙪𝚞𝛖𝜐𝝊𝞄𝞾',
'v': 'vνѵט𝐯𝑣𝒗𝓋𝓿𝔳𝕧𝖛𝗏𝘃𝘷𝙫𝚟𝛎𝜈𝝂𝝼𝞶',
'w': 'wɯѡԝա𝐰𝑤𝒘𝓌𝔀𝔴𝕨𝖜𝗐𝘄𝘸𝙬𝚠',
'x': 'x×х𝐱𝑥𝒙𝓍𝔁𝔵𝕩𝖝𝗑𝘅𝘹𝙭𝚡',
'y': 'yɣʏγуүỿ𝐲𝑦𝒚𝓎𝔂𝔶𝕪𝖞𝗒𝘆𝘺𝙮𝚢𝛄𝛾𝜸𝝲𝞬',
'z': 'z𝐳𝑧𝒛𝓏𝔃𝔷𝕫𝖟𝗓𝘇𝘻𝙯𝚣',
'/': '',
'\\': '',
' ': '\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\xa0\u202f\u205f',
'.': '',
'-': '˗╴﹣-−⎼',
'!': '﹗!ǃⵑ︕',
':': ':˸։፡᛬⁚∶⠆︓﹕',
'#': '#﹟'
}
def _prep_maketrans(data):
from_s = ''
to_s = ''
for target, chars in data.items():
from_s += chars
to_s += target * len(chars)
return str.maketrans(from_s, to_s)
UNICODE_CHARMAP = _prep_maketrans(_UNICODE_CHARMAP)
PUNISH_OPTIONS = ['kill', 'ban', 'quiet', 'kick', 'block'] PUNISH_OPTIONS = ['kill', 'ban', 'quiet', 'kick', 'block']
EXEMPT_OPTIONS = ['voice', 'halfop', 'op'] EXEMPT_OPTIONS = ['voice', 'halfop', 'op']
DEFAULT_EXEMPT_OPTION = 'halfop' DEFAULT_EXEMPT_OPTION = 'halfop'
@ -215,8 +285,9 @@ utils.add_hook(handle_masshighlight, 'NOTICE', priority=1000)
TEXTFILTER_DEFAULTS = { TEXTFILTER_DEFAULTS = {
'reason': "Spam is prohibited", 'reason': "Spam is prohibited",
'punishment': 'kick+ban+block', 'punishment': 'kick+ban+block',
'watch_pms': 'false', 'watch_pms': False,
'enabled': False 'enabled': False,
'munge_unicode': True,
} }
def handle_textfilter(irc, source, command, args): def handle_textfilter(irc, source, command, args):
"""Antispam text filter handler.""" """Antispam text filter handler."""
@ -277,10 +348,12 @@ def handle_textfilter(irc, source, command, args):
if irc.get_service_option('antispam', 'strip_formatting', True): if irc.get_service_option('antispam', 'strip_formatting', True):
text = utils.strip_irc_formatting(text) text = utils.strip_irc_formatting(text)
if txf_settings.get('munge_unicode', TEXTFILTER_DEFAULTS['munge_unicode']):
text = str.translate(text, UNICODE_CHARMAP)
punished = False punished = False
for filterglob in txf_globs: for filterglob in txf_globs:
if ircmatch.match(1, filterglob, text): if utils.match_text(filterglob, text):
log.info("(%s) antispam: punishing %s => %s for text filter %r", log.info("(%s) antispam: punishing %s => %s for text filter %r",
irc.name, irc.name,
irc.get_friendly_name(source), irc.get_friendly_name(source),
@ -293,3 +366,44 @@ def handle_textfilter(irc, source, command, args):
utils.add_hook(handle_textfilter, 'PRIVMSG', priority=999) utils.add_hook(handle_textfilter, 'PRIVMSG', priority=999)
utils.add_hook(handle_textfilter, 'NOTICE', priority=999) utils.add_hook(handle_textfilter, 'NOTICE', priority=999)
PARTQUIT_DEFAULTS = {
'watch_quits': True,
'watch_parts': True,
'part_filter_message': "Reason filtered",
'quit_filter_message': "Reason filtered",
}
def handle_partquit(irc, source, command, args):
"""Antispam part/quit message filter."""
text = args.get('text')
pq_settings = irc.get_service_option('antispam', 'partquit',
PARTQUIT_DEFAULTS)
if not text:
return # No text to match against
elif command == 'QUIT' and not pq_settings.get('watch_quits', True):
return # Not enabled
elif command == 'PART' and not pq_settings.get('watch_parts', True):
return
# Merge together global and local partquit filter lists.
pq_globs = set(conf.conf.get('antispam', {}).get('partquit_globs', [])) | \
set(irc.serverdata.get('antispam_partquit_globs', []))
if not pq_globs:
return
for filterglob in pq_globs:
if utils.match_text(filterglob, text):
# For parts, also log the affected channels
if command == 'PART':
filtered_message = pq_settings.get('part_filter_message', PARTQUIT_DEFAULTS['part_filter_message'])
log.info('(%s) antispam: filtered part message from %s on %s due to part/quit filter glob %s',
irc.name, irc.get_hostmask(source), ','.join(args['channels']), filterglob)
else:
filtered_message = pq_settings.get('quit_filter_message', PARTQUIT_DEFAULTS['quit_filter_message'])
log.info('(%s) antispam: filtered quit message from %s due to part/quit filter glob %s',
irc.name, args['userdata'].nick, filterglob)
args['text'] = filtered_message
break
utils.add_hook(handle_partquit, 'PART', priority=999)
utils.add_hook(handle_partquit, 'QUIT', priority=999)

View File

@ -3,10 +3,11 @@ automode.py - Provide simple channel ACL management by giving prefix modes to us
hostmasks or exttargets. hostmasks or exttargets.
""" """
import collections import collections
import string
from pylinkirc import utils, conf, world, structures from pylinkirc import conf, structures, utils, world
from pylinkirc.log import log
from pylinkirc.coremods import permissions from pylinkirc.coremods import permissions
from pylinkirc.log import log
mydesc = ("The \x02Automode\x02 plugin provides simple channel ACL management by giving prefix modes " mydesc = ("The \x02Automode\x02 plugin provides simple channel ACL management by giving prefix modes "
"to users matching hostmasks or exttargets.") "to users matching hostmasks or exttargets.")
@ -94,10 +95,16 @@ def _check_automode_access(irc, uid, channel, command):
def match(irc, channel, uids=None): def match(irc, channel, uids=None):
""" """
Automode matcher engine. Set modes on matching users. If uids is not given, check all users in the channel and give
them modes as needed.
""" """
if isinstance(channel, int) or str(channel).startswith(tuple(string.digits)):
channel = '#' + str(channel) # Mangle channels on networks where they're stored as an ID
dbentry = db.get(irc.name+channel) dbentry = db.get(irc.name+channel)
if dbentry is None: if not irc.has_cap('has-irc-modes'):
log.debug('(%s) automode: skipping match() because IRC modes are not supported on this protocol', irc.name)
return
elif dbentry is None:
return return
modebot_uid = modebot.uids.get(irc.name) modebot_uid = modebot.uids.get(irc.name)
@ -162,10 +169,13 @@ utils.add_hook(handle_services_login, 'PYLINK_RELAY_SERVICES_LOGIN')
def _get_channel_pair(irc, source, chanpair, perm=None): def _get_channel_pair(irc, source, chanpair, perm=None):
""" """
Fetches the network and channel given a channel pair, Fetches the network and channel given a channel pair, also optionally checking the caller's permissions.
also optionally checking the caller's permissions.
""" """
log.debug('(%s) Looking up chanpair %s', irc.name, chanpair) log.debug('(%s) Looking up chanpair %s', irc.name, chanpair)
if '#' not in chanpair and chanpair.startswith(tuple(string.digits)):
chanpair = '#' + chanpair # Mangle channels on networks where they're stored by ID
try: try:
network, channel = chanpair.split('#', 1) network, channel = chanpair.split('#', 1)
except ValueError: except ValueError:
@ -173,9 +183,6 @@ def _get_channel_pair(irc, source, chanpair, perm=None):
channel = '#' + channel channel = '#' + channel
channel = irc.to_lower(channel) channel = irc.to_lower(channel)
if not irc.is_channel(channel):
raise ValueError("Invalid channel name %s." % channel)
if network: if network:
ircobj = world.networkobjects.get(network) ircobj = world.networkobjects.get(network)
else: else:
@ -211,6 +218,9 @@ def setacc(irc, source, args):
\x02SETACC #staffchan $channel:#mainchan:op o \x02SETACC #staffchan $channel:#mainchan:op o
""" """
if not irc.has_cap('has-irc-modes'):
error(irc, "IRC style modes are not supported on this protocol.")
return
try: try:
chanpair, mask, modes = args chanpair, mask, modes = args
@ -231,7 +241,7 @@ def setacc(irc, source, args):
reply(irc, "Done. \x02%s\x02 now has modes \x02+%s\x02 in \x02%s\x02." % (mask, modes, 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 persistently. # Join the Automode bot to the channel persistently.
modebot.add_persistent_channel(irc, 'automode', channel) modebot.add_persistent_channel(ircobj, 'automode', channel)
modebot.add_cmd(setacc, aliases=('setaccess', 'set'), featured=True) modebot.add_cmd(setacc, aliases=('setaccess', 'set'), featured=True)
@ -285,7 +295,7 @@ def delacc(irc, source, args):
if not dbentry: if not dbentry:
log.debug("Automode: purging empty channel pair %s/%s", ircobj.name, channel) log.debug("Automode: purging empty channel pair %s/%s", ircobj.name, channel)
del db[ircobj.name+channel] del db[ircobj.name+channel]
modebot.remove_persistent_channel(irc, 'automode', channel) modebot.remove_persistent_channel(ircobj, 'automode', channel)
modebot.add_cmd(delacc, aliases=('delaccess', 'del'), featured=True) modebot.add_cmd(delacc, aliases=('delaccess', 'del'), featured=True)
@ -365,7 +375,7 @@ def clearacc(irc, source, args):
del db[ircobj.name+channel] del db[ircobj.name+channel]
log.info('(%s) %s cleared modes on %s', ircobj.name, irc.get_hostmask(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) reply(irc, "Done. Removed all Automode access entries for \x02%s\x02." % channel)
modebot.remove_persistent_channel(irc, 'automode', channel) modebot.remove_persistent_channel(ircobj, 'automode', channel)
else: else:
error(irc, "No Automode access entries exist for \x02%s\x02." % channel) error(irc, "No Automode access entries exist for \x02%s\x02." % channel)

View File

@ -3,9 +3,9 @@ bots.py: Spawn virtual users/bots on a PyLink server and make them interact
with things. with things.
""" """
from pylinkirc import utils from pylinkirc import utils
from pylinkirc.log import log
from pylinkirc.coremods import permissions from pylinkirc.coremods import permissions
@utils.add_cmd @utils.add_cmd
def spawnclient(irc, source, args): def spawnclient(irc, source, args):
"""<nick> <ident> <host> """<nick> <ident> <host>
@ -39,7 +39,10 @@ def quit(irc, source, args):
irc.error("Not enough arguments. Needs 1-2: nick, reason (optional).") irc.error("Not enough arguments. Needs 1-2: nick, reason (optional).")
return return
u = irc.nick_to_uid(nick) u = irc.nick_to_uid(nick, filterfunc=irc.is_internal_client)
if u is None:
irc.error("Unknown user %r" % nick)
return
if irc.pseudoclient.uid == u: if irc.pseudoclient.uid == u:
irc.error("Cannot quit the main PyLink client!") irc.error("Cannot quit the main PyLink client!")
@ -69,13 +72,13 @@ def joinclient(irc, source, args):
try: try:
# Check if the first argument is an existing PyLink client. If it is not, # 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. # then assume that the first argument was actually the channels being joined.
u = irc.nick_to_uid(args[0]) u = irc.nick_to_uid(args[0], filterfunc=irc.is_internal_client)
if not irc.is_internal_client(u): # First argument isn't one of our clients if u is None: # First argument isn't one of our clients
raise IndexError raise IndexError
clist = args[1] clist = args[1]
except IndexError: # No nick was given; shift arguments one to the left. except IndexError: # No valid nick was given; shift arguments one to the left.
u = irc.pseudoclient.uid u = irc.pseudoclient.uid
try: try:
clist = args[0] clist = args[0]
@ -114,7 +117,7 @@ def joinclient(irc, source, args):
except KeyError: except KeyError:
modes = [] modes = []
# Call a join hook manually so other plugins like relay can understand it. # Signal the join to other plugins
irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_JOIN', {'channel': real_channel, 'users': [u], irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_JOIN', {'channel': real_channel, 'users': [u],
'modes': modes, 'parse_as': 'JOIN'}]) 'modes': modes, 'parse_as': 'JOIN'}])
irc.reply("Done.") irc.reply("Done.")
@ -138,7 +141,7 @@ def nick(irc, source, args):
except IndexError: except IndexError:
irc.error("Not enough arguments. Needs 1-2: nick (optional), newnick.") irc.error("Not enough arguments. Needs 1-2: nick (optional), newnick.")
return return
u = irc.nick_to_uid(nick) u = irc.nick_to_uid(nick, filterfunc=irc.is_internal_client)
if newnick in ('0', u): # Allow /nick 0 to work if newnick in ('0', u): # Allow /nick 0 to work
newnick = u newnick = u
@ -153,7 +156,7 @@ def nick(irc, source, args):
irc.nick(u, newnick) irc.nick(u, newnick)
irc.reply("Done.") irc.reply("Done.")
# Ditto above: manually send a NICK change hook payload to other plugins. # Signal the nick change to other plugins
irc.call_hooks([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 @utils.add_cmd
@ -171,8 +174,8 @@ def part(irc, source, args):
# First, check if the first argument is an existing PyLink client. If it is not, # 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. # then assume that the first argument was actually the channels being parted.
u = irc.nick_to_uid(nick) u = irc.nick_to_uid(nick, filterfunc=irc.is_internal_client)
if not irc.is_internal_client(u): # First argument isn't one of our clients if u is None: # First argument isn't one of our clients
raise IndexError raise IndexError
except IndexError: # No nick was given; shift arguments one to the left. except IndexError: # No nick was given; shift arguments one to the left.
@ -217,12 +220,11 @@ def msg(irc, source, args):
# First, check if the first argument is an existing PyLink client. If it is not, # 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. # then assume that the first argument was actually the message TARGET.
sourceuid = irc.nick_to_uid(msgsource) sourceuid = irc.nick_to_uid(msgsource, filterfunc=irc.is_internal_client)
if not irc.is_internal_client(sourceuid): # First argument isn't one of our clients
if sourceuid is None or not text: # First argument isn't one of our clients
raise IndexError raise IndexError
if not text:
raise IndexError
except IndexError: except IndexError:
try: try:
sourceuid = irc.pseudoclient.uid sourceuid = irc.pseudoclient.uid
@ -236,12 +238,26 @@ def msg(irc, source, args):
irc.error('No text given.') irc.error('No text given.')
return return
if not irc.is_channel(target): try:
# Convert nick of the message target to a UID, if the target isn't a channel int_u = int(target)
real_target = irc.nick_to_uid(target) except:
if real_target is None: # Unknown target user, if target isn't a valid channel name int_u = None
if int_u and int_u in irc.users:
real_target = int_u # Some protocols use numeric UIDs
elif target in irc.users:
real_target = target
elif not irc.is_channel(target):
# Convert nick of the message target to a UID, if the target isn't a channel or UID
potential_targets = irc.nick_to_uid(target, multi=True)
if not potential_targets: # Unknown target user, if target isn't a valid channel name
irc.error('Unknown user %r.' % target) irc.error('Unknown user %r.' % target)
return return
elif len(potential_targets) > 1:
irc.error('Multiple users with the nick %r found: please select the right UID: %s' % (target, str(potential_targets)))
return
else:
real_target = potential_targets[0]
else: else:
real_target = target real_target = target

View File

@ -1,20 +1,16 @@
""" """
Changehost plugin - automatically changes the hostname of matching users. Changehost plugin - automatically changes the hostname of matching users.
""" """
from pylinkirc import utils, world, conf
from pylinkirc.log import log
from pylinkirc.coremods import permissions
import string import string
# ircmatch library from https://github.com/mammon-ircd/ircmatch from pylinkirc import conf, utils, world
# (pip install ircmatch) from pylinkirc.coremods import permissions
import ircmatch from pylinkirc.log import log
# Characters allowed in a hostname. # Characters allowed in a hostname.
allowed_chars = string.ascii_letters + '-./:' + string.digits allowed_chars = string.ascii_letters + '-./:' + string.digits
def _changehost(irc, target, args): def _changehost(irc, target):
changehost_conf = conf.conf.get("changehost") changehost_conf = conf.conf.get("changehost")
if target not in irc.users: if target not in irc.users:
@ -23,24 +19,25 @@ def _changehost(irc, target, args):
log.debug('(%s) Skipping changehost on internal client %s', irc.name, target) log.debug('(%s) Skipping changehost on internal client %s', irc.name, target)
return return
if not changehost_conf: if irc.name not in changehost_conf.get('enabled_nets') and not irc.serverdata.get('changehost_enable'):
log.warning("(%s) Missing 'changehost:' configuration block; "
"Changehost will not function correctly!", irc.name)
return
elif irc.name not in changehost_conf.get('enabled_nets'):
# We're not enabled on the network, break. # We're not enabled on the network, break.
return return
match_ip = changehost_conf.get('match_ip', False) match_ip = irc.get_service_option('changehost', 'match_ip', default=False)
match_realhosts = changehost_conf.get('match_realhosts', False) match_realhosts = irc.get_service_option('changehost', 'match_realhosts', default=False)
changehost_hosts = changehost_conf.get('hosts') changehost_hosts = irc.get_service_options('changehost', 'hosts', dict)
if not changehost_hosts: if not changehost_hosts:
log.warning("(%s) No hosts were defined in changehost::hosts; "
"Changehost will not function correctly!", irc.name)
return return
args = args.copy() args = irc.users[target].get_fields()
# $host is explicitly forbidden by default because it can cause recursive
# loops when IP or real host masks are used to match a target. vHost
# updates do not affect these fields, so any further host application will
# cause the vHost to grow rapidly in size.
# That said, it is possible to get away with this expansion if you're
# careful enough, and that's why this hidden option exists.
if not changehost_conf.get('force_host_expansion'): if not changehost_conf.get('force_host_expansion'):
del args['host'] del args['host']
@ -57,16 +54,6 @@ def _changehost(irc, target, args):
# Substitute using the fields provided the hook data. This means # Substitute using the fields provided the hook data. This means
# that the following variables are available for substitution: # that the following variables are available for substitution:
# $uid, $ts, $nick, $realhost, $ident, and $ip. # $uid, $ts, $nick, $realhost, $ident, and $ip.
# $host is explicitly forbidden by default because it can cause
# recursive loops when IP or real host masks are used to match a
# target. vHost updates do not affect these fields, so any further
# execution of 'applyhosts' will cause $host to expand again to
# the user's new host, causing the vHost to grow rapidly in size.
# That said, it is possible to get away with this expansion if
# you're careful with what you're doing, and that is why this
# hidden option exists. -GLolol
try: try:
new_host = template.substitute(args) new_host = template.substitute(args)
except KeyError as e: except KeyError as e:
@ -78,6 +65,8 @@ def _changehost(irc, target, args):
if char not in allowed_chars: if char not in allowed_chars:
new_host = new_host.replace(char, '-') new_host = new_host.replace(char, '-')
# Only send a host change if something has changed
if new_host != irc.users[target].host:
irc.update_client(target, 'HOST', new_host) irc.update_client(target, 'HOST', new_host)
# Only operate on the first match. # Only operate on the first match.
@ -89,26 +78,23 @@ def handle_uid(irc, sender, command, args):
""" """
target = args['uid'] target = args['uid']
_changehost(irc, target, args) _changehost(irc, target)
utils.add_hook(handle_uid, 'UID') utils.add_hook(handle_uid, 'UID')
def handle_chghost(irc, sender, command, args): def handle_chghost(irc, sender, command, args):
""" """
Handles incoming CHGHOST requests for optional host-change enforcement. Handles incoming CHGHOST requests for optional host-change enforcement.
""" """
changehost_conf = conf.conf.get("changehost") changehost_conf = conf.conf.get("changehost", {})
if not changehost_conf:
return
target = args['target'] target = args['target']
if (not irc.is_internal_client(sender)) and (not irc.is_internal_server(sender)): if (not irc.is_internal_client(sender)) and (not irc.is_internal_server(sender)):
if irc.name in changehost_conf.get('enforced_nets', []): if irc.name in changehost_conf.get('enforced_nets', []) or irc.serverdata.get('changehost_enforce'):
log.debug('(%s) Enforce for network is on, re-checking host for target %s/%s', log.debug('(%s) Enforce for network is on, re-checking host for target %s/%s',
irc.name, target, irc.get_friendly_name(target)) irc.name, target, irc.get_friendly_name(target))
for ex in changehost_conf.get("enforce_exceptions", []): for ex in irc.get_service_options("changehost", "enforce_exceptions", list):
if irc.match_host(ex, target): if irc.match_host(ex, target):
log.debug('(%s) Skipping host change for target %s; they are exempted by mask %s', log.debug('(%s) Skipping host change for target %s; they are exempted by mask %s',
irc.name, target, ex) irc.name, target, ex)
@ -116,10 +102,16 @@ def handle_chghost(irc, sender, command, args):
userobj = irc.users.get(target) userobj = irc.users.get(target)
if userobj: if userobj:
_changehost(irc, target, userobj.get_fields()) _changehost(irc, target)
utils.add_hook(handle_chghost, 'CHGHOST') utils.add_hook(handle_chghost, 'CHGHOST')
def handle_svslogin(irc, sender, command, args):
"""
Handles services account changes for changehost.
"""
_changehost(irc, sender)
utils.add_hook(handle_svslogin, 'CLIENT_SERVICES_LOGIN')
@utils.add_cmd @utils.add_cmd
def applyhosts(irc, sender, args): def applyhosts(irc, sender, args):
"""[<network>] """[<network>]
@ -136,7 +128,7 @@ def applyhosts(irc, sender, args):
irc.error("Unknown network '%s'." % network) irc.error("Unknown network '%s'." % network)
return return
for user, userdata in network.users.copy().items(): for user in network.users.copy():
_changehost(network, user, userdata.__dict__) _changehost(network, user)
irc.reply("Done.") irc.reply("Done.")

View File

@ -1,13 +1,12 @@
# commands.py: base PyLink commands # commands.py: base PyLink commands
import sys
import time import time
from pylinkirc import utils, __version__, world, real_version from pylinkirc import __version__, conf, real_version, utils, world
from pylinkirc.log import log
from pylinkirc.coremods import permissions from pylinkirc.coremods import permissions
from pylinkirc.coremods.login import pwd_context from pylinkirc.coremods.login import pwd_context
default_permissions = {"*!*@*": ['commands.status', 'commands.showuser', 'commands.showchan']} default_permissions = {"*!*@*": ['commands.status', 'commands.showuser', 'commands.showchan', 'commands.shownet']}
def main(irc=None): def main(irc=None):
"""Commands plugin main function, called on plugin load.""" """Commands plugin main function, called on plugin load."""
@ -32,30 +31,31 @@ def status(irc, source, args):
irc.reply('Operator access: \x02%s\x02' % bool(irc.is_oper(source))) irc.reply('Operator access: \x02%s\x02' % bool(irc.is_oper(source)))
_none = '\x1D(none)\x1D' _none = '\x1D(none)\x1D'
@utils.add_cmd _notavail = '\x1DN/A\x1D'
def showuser(irc, source, args): def _do_showuser(irc, source, u):
"""<user> """Helper function for showuser."""
# Some protocol modules store UIDs as ints; make sure we check for that.
Shows information about <user>."""
permissions.check_permissions(irc, source, ['commands.showuser'])
try: try:
target = args[0] int_u = int(u)
except IndexError: except ValueError:
irc.error("Not enough arguments. Needs 1: nick.") pass
return else:
u = irc.nick_to_uid(target) or target if int_u in irc.users:
u = int_u
# Only show private info if the person is calling 'showuser' on themselves, # Only show private info if the person is calling 'showuser' on themselves,
# or is an oper. # or is an oper.
verbose = irc.is_oper(source) or u == source verbose = irc.is_oper(source) or u == source
if u not in irc.users: if u not in irc.users:
irc.error('Unknown user %r.' % target) irc.error('Unknown user %r.' % u)
return return
f = lambda s: irc.reply(s, private=True) f = lambda s: irc.reply(' ' + s, private=True)
userobj = irc.users[u] userobj = irc.users[u]
f('Showing information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident, irc.reply('Showing information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident,
userobj.host, userobj.realname)) userobj.host, userobj.realname), private=True)
sid = irc.get_server(u) sid = irc.get_server(u)
serverobj = irc.servers[sid] serverobj = irc.servers[sid]
@ -63,20 +63,135 @@ def showuser(irc, source, args):
# Show connected server & nick TS if available # Show connected server & nick TS if available
serverinfo = '%s[%s]' % (serverobj.name, sid) \ serverinfo = '%s[%s]' % (serverobj.name, sid) \
if irc.has_cap('can-track-servers') else 'N/A' if irc.has_cap('can-track-servers') else None
tsinfo = '%s [UTC] (%s)' % (time.asctime(time.gmtime(int(ts))), ts) \ tsinfo = '%s [UTC] (%s)' % (time.asctime(time.gmtime(int(ts))), ts) \
if irc.has_cap('has-ts') else 'N/A' if irc.has_cap('has-ts') else None
f('\x02Home server\x02: %s; \x02Nick TS:\x02 %s' % (serverinfo, tsinfo)) if tsinfo or serverinfo:
f('\x02Home server\x02: %s; \x02Nick TS:\x02 %s' % (serverinfo or _notavail, tsinfo or _notavail))
if verbose: # Oper/self only data: user modes, channels in, account info, etc. 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' % \ f('\x02Protocol UID\x02: %s; \x02Real host\x02: %s; \x02IP\x02: %s' % \
(u, userobj.realhost, userobj.ip)) (u, userobj.realhost or _notavail, userobj.ip))
channels = sorted(userobj.channels) channels = sorted(userobj.channels)
f('\x02Channels\x02: %s' % (' '.join(channels) or _none)) f('\x02Channels\x02: %s' % (' '.join(map(str, channels)) or _none))
f('\x02PyLink identification\x02: %s; \x02Services account\x02: %s; \x02Away status\x02: %s' % \ f('\x02PyLink identification\x02: %s; \x02Services account\x02: %s; \x02Away status\x02: %s' % \
((userobj.account or _none), (userobj.services_account or _none), userobj.away or _none)) ((userobj.account or _none), (userobj.services_account or _none), userobj.away or _none))
f('\x02User modes\x02: %s' % irc.join_modes(userobj.modes, sort=True))
# Show relay user data if available
relay = world.plugins.get('relay')
if relay:
try:
userpair = relay.get_orig_user(irc, u) or (irc.name, u)
remoteusers = relay.relayusers[userpair].items()
except KeyError:
pass
else:
nicks = []
if remoteusers:
# Display all of the user's relay subclients, if there are any
nicks.append('%s:\x02%s\x02' % (userpair[0],
world.networkobjects[userpair[0]].users[userpair[1]].nick))
for r in remoteusers:
remotenet, remoteuser = r
remoteirc = world.networkobjects[remotenet]
nicks.append('%s:\x02%s\x02' % (remotenet, remoteirc.users[remoteuser].nick))
f("\x02Relay nicks\x02: %s" % ', '.join(nicks))
if verbose:
# Show the relay channels the user is in, if applicable
relaychannels = []
for ch in irc.users[u].channels:
relayentry = relay.get_relay(irc, ch)
if relayentry:
relaychannels.append(''.join(relayentry))
if relaychannels and verbose:
f("\x02Relay channels\x02: %s" % ' '.join(relaychannels))
@utils.add_cmd
def showuser(irc, source, args):
"""<user>
Shows information about <user>."""
permissions.check_permissions(irc, source, ['commands.showuser'])
target = ' '.join(args)
if not target:
irc.error("Not enough arguments. Needs 1: nick.")
return
users = irc.nick_to_uid(target, multi=True) or [target]
for user in users:
_do_showuser(irc, source, user)
@utils.add_cmd
def shownet(irc, source, args):
"""[<network name>]
Shows information about <network name>, or the current network if no argument is given."""
permissions.check_permissions(irc, source, ['commands.shownet'])
try:
extended = permissions.check_permissions(irc, source, ['commands.shownet.extended'])
except utils.NotAuthorizedError:
extended = False
try:
target = args[0]
except IndexError:
target = irc.name
try:
netobj = world.networkobjects[target]
serverdata = netobj.serverdata
except KeyError:
netobj = None
# If we have extended access, also look for disconnected networks
if extended and target in conf.conf['servers']:
serverdata = conf.conf['servers'][target]
else:
irc.error('Unknown network %r' % target)
return
# Get extended protocol details: IRCd type, virtual server info
protocol_name = serverdata.get('protocol')
ircd_type = None
# A bit of hardcoding here :(
if protocol_name == 'ts6':
ircd_type = serverdata.get('ircd', 'charybdis[default]')
elif protocol_name == 'inspircd':
ircd_type = serverdata.get('target_version', 'insp20[default]')
elif protocol_name == 'p10':
ircd_type = serverdata.get('ircd') or serverdata.get('p10_ircd') or 'nefarious[default]'
if protocol_name and ircd_type:
protocol_name = '%s/%s' % (protocol_name, ircd_type)
elif netobj and not protocol_name: # Show virtual server detail if applicable
try:
parent_name = netobj.virtual_parent.name
except AttributeError:
parent_name = None
protocol_name = 'none; virtual server defined by \x02%s\x02' % parent_name
irc.reply('Information on network \x02%s\x02: \x02%s\x02' %
(target, netobj.get_full_network_name() if netobj else '\x1dCurrently not connected\x1d'))
irc.reply('\x02PyLink protocol module\x02: %s; \x02Encoding\x02: %s' %
(protocol_name, netobj.encoding if netobj else serverdata.get('encoding', 'utf-8[default]')))
# Extended info: target host, defined hostname / SID
if extended:
connected = netobj and netobj.connected.is_set()
irc.reply('\x02Connected?\x02 %s' % ('\x0303true' if connected else '\x0304false'))
if serverdata.get('ip'):
irc.reply('\x02Server target\x02: \x1f%s:%s' % (serverdata['ip'], serverdata.get('port')))
if serverdata.get('hostname'):
irc.reply('\x02PyLink hostname\x02: %s; \x02SID:\x02 %s; \x02SID range:\x02 %s' %
(serverdata.get('hostname') or _none,
serverdata.get('sid') or _none,
serverdata.get('sidrange') or _none))
@utils.add_cmd @utils.add_cmd
def showchan(irc, source, args): def showchan(irc, source, args):
@ -129,16 +244,24 @@ def showchan(irc, source, args):
nick = irc.prefixmodes.get(irc.cmodes.get(pmode, ''), '') + nick nick = irc.prefixmodes.get(irc.cmodes.get(pmode, ''), '') + nick
nicklist.append(nick) nicklist.append(nick)
while nicklist[:20]: # 20 nicks per line to prevent message cutoff. f('\x02User list\x02: %s' % ' '.join(nicklist))
f('\x02User list\x02: %s' % ' '.join(nicklist[:20]))
nicklist = nicklist[20:] # Show relay info, if applicable
relay = world.plugins.get('relay')
if relay:
relayentry = relay.get_relay(irc, channel)
if relayentry:
relays = ['\x02%s\x02' % ''.join(relayentry)]
relays += [''.join(link) for link in relay.db[relayentry]['links']]
f('\x02Relayed channels:\x02 %s' % (' '.join(relays)))
@utils.add_cmd @utils.add_cmd
def version(irc, source, args): def version(irc, source, args):
"""takes no arguments. """takes no arguments.
Returns the version of the currently running PyLink instance.""" Returns the version of the currently running PyLink instance."""
irc.reply("PyLink version \x02%s\x02 (in VCS: %s), released under the Mozilla Public License version 2.0." % (__version__, real_version)) py_version = utils.NORMALIZEWHITESPACE_RE.sub(' ', sys.version)
irc.reply("PyLink version \x02%s\x02 (in VCS: %s), running on Python %s." % (__version__, real_version, py_version))
irc.reply("The source of this program is available at \x02%s\x02." % world.source) irc.reply("The source of this program is available at \x02%s\x02." % world.source)
@utils.add_cmd @utils.add_cmd

View File

@ -1,10 +1,11 @@
# ctcp.py: Handles basic CTCP requests. # ctcp.py: Handles basic CTCP requests.
import random
import datetime import datetime
import random
from pylinkirc import utils from pylinkirc import utils
from pylinkirc.log import log from pylinkirc.log import log
def handle_ctcp(irc, source, command, args): def handle_ctcp(irc, source, command, args):
""" """
CTCP event handler. CTCP event handler.

View File

@ -1,9 +1,9 @@
# example.py: An example PyLink plugin. # example.py: An example PyLink plugin.
import random
from pylinkirc import utils from pylinkirc import utils
from pylinkirc.log import log from pylinkirc.log import log
import random
# Example PRIVMSG hook that returns "hi there!" when PyLink's nick is mentioned # Example PRIVMSG hook that returns "hi there!" when PyLink's nick is mentioned
# in a channel. # in a channel.

View File

@ -2,23 +2,23 @@
exec.py: Provides commands for executing raw code and debugging PyLink. exec.py: Provides commands for executing raw code and debugging PyLink.
""" """
import pprint import pprint
import threading
from pylinkirc import *
from pylinkirc.log import log
from pylinkirc.coremods import permissions
# These imports are not strictly necessary, but make the following modules # These imports are not strictly necessary, but make the following modules
# easier to access through eval and exec. # easier to access through eval and exec.
import re import threading
import time
import pylinkirc from pylinkirc import utils, world, conf
import importlib from pylinkirc.coremods import permissions
from pylinkirc.log import log
exec_locals_dict = {} exec_locals_dict = {}
PPRINT_MAX_LINES = 20 PPRINT_MAX_LINES = 20
PPRINT_WIDTH = 200 PPRINT_WIDTH = 200
if not conf.conf['pylink'].get("debug_enabled", False):
raise RuntimeError("pylink::debug_enabled must be enabled to load this plugin. "
"This should ONLY be used in test environments for debugging and development, "
"as anyone with access to this plugin's commands can run arbitrary code as the PyLink user!")
def _exec(irc, source, args, locals_dict=None): def _exec(irc, source, args, locals_dict=None):
"""<code> """<code>

View File

@ -1,7 +1,8 @@
# fantasy.py: Adds FANTASY command support, to allow calling commands in channels # fantasy.py: Adds FANTASY command support, to allow calling commands in channels
from pylinkirc import utils, world, conf from pylinkirc import conf, utils, world
from pylinkirc.log import log from pylinkirc.log import log
def handle_fantasy(irc, source, command, args): def handle_fantasy(irc, source, command, args):
"""Fantasy command handler.""" """Fantasy command handler."""
@ -41,10 +42,9 @@ def handle_fantasy(irc, source, command, args):
conf.conf['pylink'].get('prefixes', {}).get(botname))] conf.conf['pylink'].get('prefixes', {}).get(botname))]
# If responding to nick is enabled, add variations of the current nick # If responding to nick is enabled, add variations of the current nick
# to the prefix list: "<nick>," and "<nick>:" # to the prefix list: "<nick>,", "<nick>:", and "@<nick>" (for Discord and other protocols)
nick = irc.to_lower(irc.users[servuid].nick) nick = irc.to_lower(irc.users[servuid].nick)
nick_prefixes = [nick+',', nick+':', '@'+nick]
nick_prefixes = [nick+',', nick+':']
if respondtonick: if respondtonick:
prefixes += nick_prefixes prefixes += nick_prefixes

View File

@ -4,7 +4,6 @@ games.py: Creates a bot providing a few simple games.
import random import random
from pylinkirc import utils from pylinkirc import utils
from pylinkirc.log import log
mydesc = "The \x02Games\x02 plugin provides simple games for IRC." mydesc = "The \x02Games\x02 plugin provides simple games for IRC."

View File

@ -3,8 +3,8 @@
import string import string
from pylinkirc import conf, utils, world from pylinkirc import conf, utils, world
from pylinkirc.log import log
from pylinkirc.coremods import permissions from pylinkirc.coremods import permissions
from pylinkirc.log import log
DEFAULT_FORMAT = "[$sender@$fullnetwork] $text" DEFAULT_FORMAT = "[$sender@$fullnetwork] $text"
@ -28,7 +28,8 @@ def g(irc, source, args):
netcount = 0 netcount = 0
chancount = 0 chancount = 0
for netname, ircd in world.networkobjects.items(): for netname, ircd in world.networkobjects.items():
if ircd.connected.is_set(): # Only attempt to send to connected networks # Skip networks that aren't ready and dummy networks which don't have .pseudoclient set
if ircd.connected.is_set() and ircd.pseudoclient:
netcount += 1 netcount += 1
for channel in ircd.pseudoclient.channels: for channel in ircd.pseudoclient.channels:
@ -36,7 +37,7 @@ def g(irc, source, args):
skip = False skip = False
for exempt in local_exempt_channels: for exempt in local_exempt_channels:
if irc.match_text(exempt, channel): if ircd.match_text(exempt, str(channel)):
log.debug('global: Skipping channel %s%s for exempt %r', netname, channel, exempt) log.debug('global: Skipping channel %s%s for exempt %r', netname, channel, exempt)
skip = True skip = True
break break

View File

@ -1,11 +1,12 @@
"""Networks plugin - allows you to manipulate connections to various configured networks.""" """Networks plugin - allows you to manipulate connections to various configured networks."""
import importlib import importlib
import types
import threading import threading
import types
from pylinkirc import utils, world, conf, classes import pylinkirc
from pylinkirc.log import log from pylinkirc import utils, world
from pylinkirc.coremods import control, permissions from pylinkirc.coremods import control, permissions
from pylinkirc.log import log
REMOTE_IN_USE = threading.Event() REMOTE_IN_USE = threading.Event()
@ -26,10 +27,14 @@ def disconnect(irc, source, args):
except KeyError: # Unknown network. except KeyError: # Unknown network.
irc.error('No such network "%s" (case sensitive).' % netname) irc.error('No such network "%s" (case sensitive).' % netname)
return 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))
if network.has_cap('virtual-server'):
irc.error('"%s" is a virtual server and cannot be directly disconnected.' % netname)
return
log.info('Disconnecting network %r per %s', netname, irc.get_hostmask(source))
control.remove_network(network) control.remove_network(network)
irc.reply("Done. If you want to reconnect this network, use the 'rehash' command.")
@utils.add_cmd @utils.add_cmd
def autoconnect(irc, source, args): def autoconnect(irc, source, args):
@ -177,7 +182,17 @@ def reloadproto(irc, source, args):
irc.error('Not enough arguments (needs 1: protocol module name)') irc.error('Not enough arguments (needs 1: protocol module name)')
return return
# Reload the dependency libraries first
importlib.reload(pylinkirc.classes)
log.debug('networks.reloadproto: reloading %s', pylinkirc.classes)
for common_name in pylinkirc.protocols.common_modules:
module = utils._get_protocol_module(common_name)
log.debug('networks.reloadproto: reloading %s', module)
importlib.reload(module)
proto = utils._get_protocol_module(name) proto = utils._get_protocol_module(name)
log.debug('networks.reloadproto: reloading %s', proto)
importlib.reload(proto) 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) irc.reply("Done. You will have to manually disconnect and reconnect any network using the %r module for changes to apply." % name)

View File

@ -4,8 +4,8 @@ opercmds.py: Provides a subset of network management commands.
import argparse import argparse
from pylinkirc import utils, world from pylinkirc import utils, world
from pylinkirc.log import log
from pylinkirc.coremods import permissions from pylinkirc.coremods import permissions
from pylinkirc.log import log
# Having a hard limit here is sensible because otherwise it can flood the client or server off. # Having a hard limit here is sensible because otherwise it can flood the client or server off.
CHECKBAN_MAX_RESULTS = 200 CHECKBAN_MAX_RESULTS = 200
@ -333,6 +333,29 @@ def jupe(irc, source, args):
irc.reply("Done.") irc.reply("Done.")
def _try_find_target(irc, nick):
"""
Tries to find the target UID for the given nick, raising LookupError if it doesn't exist or is ambiguous.
"""
try:
int_u = int(nick)
except:
int_u = None
if int_u and int_u in irc.users:
return int_u # Some protocols use numeric UIDs
elif nick in irc.users:
return nick
potential_targets = irc.nick_to_uid(nick, multi=True)
if not potential_targets:
# Whatever we were told to kick doesn't exist!
raise LookupError("No such target %r." % nick)
elif len(potential_targets) > 1:
raise LookupError("Multiple users with the nick %r found: please select the right UID: %s" % (nick, str(potential_targets)))
else:
return potential_targets[0]
@utils.add_cmd @utils.add_cmd
def kick(irc, source, args): def kick(irc, source, args):
"""<channel> <user> [<reason>] """<channel> <user> [<reason>]
@ -347,16 +370,11 @@ def kick(irc, source, args):
irc.error("Not enough arguments. Needs 2-3: channel, target, reason (optional).") irc.error("Not enough arguments. Needs 2-3: channel, target, reason (optional).")
return return
targetu = irc.nick_to_uid(target)
if channel not in irc.channels: # KICK only works on channels that exist. if channel not in irc.channels: # KICK only works on channels that exist.
irc.error("Unknown channel %r." % channel) irc.error("Unknown channel %r." % channel)
return return
if not targetu: targetu = _try_find_target(irc, target)
# Whatever we were told to kick doesn't exist!
irc.error("No such target nick %r." % target)
return
sender = irc.pseudoclient.uid sender = irc.pseudoclient.uid
irc.kick(sender, channel, targetu, reason) irc.kick(sender, channel, targetu, reason)
@ -379,25 +397,16 @@ def kill(irc, source, args):
# Convert the source and target nicks to UIDs. # Convert the source and target nicks to UIDs.
sender = irc.pseudoclient.uid sender = irc.pseudoclient.uid
targetu = irc.nick_to_uid(target)
userdata = irc.users.get(targetu)
if targetu not in irc.users: targetu = _try_find_target(irc, target)
# Whatever we were told to kick doesn't exist!
irc.error("No such nick %r." % target) if irc.pseudoclient.uid == targetu:
return
elif irc.pseudoclient.uid == targetu:
irc.error("Cannot kill the main PyLink client!") irc.error("Cannot kill the main PyLink client!")
return return
# Deliver a more complete kill reason if our target is a non-PyLink client. userdata = irc.users.get(targetu)
# We skip this for PyLink clients so that relayed kills don't get
# "Killed (abc (...))" tacked on both here and by the receiving IRCd. reason = "Requested by %s: %s" % (irc.get_friendly_name(source), reason)
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)
irc.kill(sender, targetu, reason) irc.kill(sender, targetu, reason)
@ -473,23 +482,24 @@ def chghost(irc, source, args):
"""<user> <new host> """<user> <new host>
Changes the visible host of the target user.""" Changes the visible host of the target user."""
chgfield(irc, source, args, 'host') _chgfield(irc, source, args, 'host')
@utils.add_cmd @utils.add_cmd
def chgident(irc, source, args): def chgident(irc, source, args):
"""<user> <new ident> """<user> <new ident>
Changes the ident of the target user.""" Changes the ident of the target user."""
chgfield(irc, source, args, 'ident') _chgfield(irc, source, args, 'ident')
@utils.add_cmd @utils.add_cmd
def chgname(irc, source, args): def chgname(irc, source, args):
"""<user> <new name> """<user> <new name>
Changes the GECOS (realname) of the target user.""" Changes the GECOS (realname) of the target user."""
chgfield(irc, source, args, 'name', 'GECOS') _chgfield(irc, source, args, 'name', 'GECOS')
def chgfield(irc, source, args, human_field, internal_field=None): def _chgfield(irc, source, args, human_field, internal_field=None):
"""Helper function for chghost/chgident/chgname."""
permissions.check_permissions(irc, source, ['opercmds.chg' + human_field]) permissions.check_permissions(irc, source, ['opercmds.chg' + human_field])
try: try:
target = args[0] target = args[0]
@ -499,10 +509,7 @@ def chgfield(irc, source, args, human_field, internal_field=None):
return return
# Find the user # Find the user
targetu = irc.nick_to_uid(target) targetu = _try_find_target(irc, target)
if targetu not in irc.users:
irc.error("No such nick %r." % target)
return
internal_field = internal_field or human_field.upper() internal_field = internal_field or human_field.upper()
irc.update_client(targetu, internal_field, new) irc.update_client(targetu, internal_field, new)

View File

@ -1,9 +1,10 @@
""" """
raw.py: Provides a 'raw' command for sending raw text to IRC. 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 from pylinkirc import utils
from pylinkirc.coremods import permissions
from pylinkirc.log import log
from pylinkirc import conf
@utils.add_cmd @utils.add_cmd
def raw(irc, source, args): def raw(irc, source, args):
@ -11,15 +12,12 @@ def raw(irc, source, args):
Sends raw text to the IRC server. Sends raw text to the IRC server.
This command is not officially supported on non-Clientbot networks, where it Use with caution - This command is only officially supported on Clientbot networks."""
requires a separate permission.""" if not conf.conf['pylink'].get("raw_enabled", False):
raise RuntimeError("Raw commands are not supported on this protocol")
if irc.protoname == 'clientbot':
# exec.raw is included for backwards compatibility with PyLink 1.x # exec.raw is included for backwards compatibility with PyLink 1.x
perms = ['raw.raw', 'exec.raw'] permissions.check_permissions(irc, source, ['raw.raw', 'exec.raw'])
else:
perms = ['raw.raw.unsupported_network']
permissions.check_permissions(irc, source, perms)
args = ' '.join(args) args = ' '.join(args)
if not args.strip(): if not args.strip():

View File

@ -1,26 +1,41 @@
# relay.py: PyLink Relay plugin # relay.py: PyLink Relay plugin
import time import base64
import threading
import string
from collections import defaultdict
import inspect import inspect
import string
import threading
import time
from collections import defaultdict
from pylinkirc import utils, world, conf, structures from pylinkirc import conf, structures, utils, world
from pylinkirc.log import log
from pylinkirc.coremods import permissions from pylinkirc.coremods import permissions
from pylinkirc.log import log
# Sets the timeout to wait for as individual servers / the PyLink daemon to start up.
TCONDITION_TIMEOUT = 2
CHANNEL_DELINKED_MSG = "Channel delinked." CHANNEL_DELINKED_MSG = "Channel delinked."
RELAY_UNLOADED_MSG = "Relay plugin unloaded." RELAY_UNLOADED_MSG = "Relay plugin unloaded."
try:
import cachetools
except ImportError as e:
raise ImportError("PyLink Relay requires cachetools as of PyLink 3.0: https://pypi.org/project/cachetools/") from e
try:
import unidecode
except ImportError:
log.info('relay: unidecode not found; disabling unicode nicks support')
USE_UNIDECODE = False
else:
USE_UNIDECODE = conf.conf.get('relay', {}).get('use_unidecode', True)
### GLOBAL (statekeeping) VARIABLES ### GLOBAL (statekeeping) VARIABLES
relayusers = defaultdict(dict) relayusers = defaultdict(dict)
relayservers = defaultdict(dict) relayservers = defaultdict(dict)
spawnlocks = defaultdict(threading.RLock) spawnlocks = defaultdict(threading.Lock)
spawnlocks_servers = defaultdict(threading.RLock) spawnlocks_servers = defaultdict(threading.Lock)
channels_init_in_progress = defaultdict(threading.Event)
# Claim bounce cache to prevent kick/mode/topic loops
__claim_bounce_timeout = conf.conf.get('relay', {}).get('claim_bounce_timeout', 5)
claim_bounce_cache = cachetools.TTLCache(float('inf'), __claim_bounce_timeout)
claim_bounce_cache_lock = threading.Lock()
dbname = conf.get_database_name('pylinkrelay') dbname = conf.get_database_name('pylinkrelay')
datastore = structures.PickleDataStore('pylinkrelay', dbname) datastore = structures.PickleDataStore('pylinkrelay', dbname)
@ -48,10 +63,6 @@ def initialize_all(irc):
if network == irc.name: if network == irc.name:
initialize_channel(irc, channel) initialize_channel(irc, channel)
# Wait for all IRC objects to be created first. This prevents
# relay servers from being spawned too early (before server authentication),
# which would break connections.
if world.started.wait(TCONDITION_TIMEOUT):
t = threading.Thread(target=_initialize_all, daemon=True, t = threading.Thread(target=_initialize_all, daemon=True,
name='relay initialize_all thread from network %r' % irc.name) name='relay initialize_all thread from network %r' % irc.name)
t.start() t.start()
@ -118,8 +129,26 @@ def die(irc=None):
except KeyError: except KeyError:
log.debug('relay.die: failed to clear persistent channels:', exc_info=True) log.debug('relay.die: failed to clear persistent channels:', exc_info=True)
allowed_chars = string.digits + string.ascii_letters + '/^|\\-_[]{}`' IRC_ASCII_ALLOWED_CHARS = string.digits + string.ascii_letters + '^|\\-_[]{}`'
fallback_separator = '|' FALLBACK_SEPARATOR = '|'
FALLBACK_CHARACTER = '-'
def _replace_special(text):
"""
Replaces brackets and spaces by similar IRC-representable characters.
"""
for pair in {('(', '['), (')', ']'), (' ', FALLBACK_CHARACTER), ('<', '['), ('>', ']')}:
text = text.replace(pair[0], pair[1])
return text
def _sanitize(text, extrachars=''):
"""Replaces characters not in IRC_ASCII_ALLOWED_CHARS with FALLBACK_CHARACTER."""
whitelist = IRC_ASCII_ALLOWED_CHARS + extrachars
for char in text:
if char not in whitelist:
text = text.replace(char, FALLBACK_CHARACTER)
return text
def normalize_nick(irc, netname, nick, times_tagged=0, uid=''): def normalize_nick(irc, netname, nick, times_tagged=0, uid=''):
""" """
Creates a normalized nickname for the given nick suitable for introduction to a remote network Creates a normalized nickname for the given nick suitable for introduction to a remote network
@ -128,6 +157,25 @@ def normalize_nick(irc, netname, nick, times_tagged=0, uid=''):
UID is optional for checking regular nick changes, to make sure that the sender doesn't get UID is optional for checking regular nick changes, to make sure that the sender doesn't get
marked as nick-colliding with itself. marked as nick-colliding with itself.
""" """
if irc.has_cap('freeform-nicks'): # ☺
return nick
is_unicode_capable = irc.casemapping in ('utf8', 'utf-8', 'rfc7700')
if USE_UNIDECODE and not is_unicode_capable:
decoded_nick = unidecode.unidecode(nick).strip()
netname = unidecode.unidecode(netname).strip()
if decoded_nick:
nick = decoded_nick
else:
# XXX: The decoded version of the nick is empty, YUCK!
# Base64 the nick for now, since (interestingly) we don't enforce UIDs to always be
# ASCII strings.
nick = base64.b64encode(nick.encode(irc.encoding, 'replace'), altchars=b'[]')
nick = nick.decode()
# Normalize spaces to hyphens, () => []
nick = _replace_special(nick)
netname = _replace_special(netname)
# Get the nick/net separator # Get the nick/net separator
separator = irc.serverdata.get('separator') or \ separator = irc.serverdata.get('separator') or \
@ -161,22 +209,34 @@ def normalize_nick(irc, netname, nick, times_tagged=0, uid=''):
irc.serverdata.get('relay_force_slashes') irc.serverdata.get('relay_force_slashes')
if '/' not in separator or not protocol_allows_slashes: if '/' not in separator or not protocol_allows_slashes:
separator = separator.replace('/', fallback_separator) separator = separator.replace('/', FALLBACK_SEPARATOR)
nick = nick.replace('/', fallback_separator) nick = nick.replace('/', FALLBACK_SEPARATOR)
if nick.startswith(tuple(string.digits+'-')): # Loop over every character in the nick, making sure that it only contains valid
# characters.
if not is_unicode_capable:
nick = _sanitize(nick, extrachars='/')
else:
# UnrealIRCd 4's forbidden nick chars, from
# https://github.com/unrealircd/unrealircd/blob/02d69e7d8/src/modules/charsys.c#L152-L163
for char in """!+%@&~#$:'\"?*,.""":
nick = nick.replace(char, FALLBACK_CHARACTER)
if nick.startswith(tuple(string.digits)):
# On TS6 IRCds, nicks that start with 0-9 are only allowed if # On TS6 IRCds, nicks that start with 0-9 are only allowed if
# they match the UID of the originating server. Otherwise, you'll # they match the UID of the originating server. Otherwise, you'll
# get nasty protocol violation SQUITs! # get nasty protocol violation SQUITs!
# Nicks starting with - are likewise not valid.
nick = '_' + nick nick = '_' + nick
elif nick.startswith('-'):
# Nicks starting with - are likewise not valid.
nick = '_' + nick[1:]
# Maximum allowed length that relay nicks may have, minus the /network tag if used. # Maximum allowed length that relay nicks may have, minus the /network tag if used.
allowedlength = maxnicklen allowedlength = maxnicklen
# Track how many times the given nick has been tagged. If this is 0, no tag is used. # Track how many times the given nick has been tagged. If this is 0, no tag is used.
# If this is 1, a /network tag is added. Otherwise, keep adding one character to the # If this is 1, a /network tag is added. Otherwise, keep adding one character to the
# separator: GLolol -> GLolol/net1 -> GLolol//net1 -> ... # separator: jlu5 -> jlu5/net1 -> jlu5//net1 -> ...
if times_tagged >= 1: if times_tagged >= 1:
suffix = "%s%s%s" % (separator[0]*times_tagged, separator[1:], netname) suffix = "%s%s%s" % (separator[0]*times_tagged, separator[1:], netname)
allowedlength -= len(suffix) allowedlength -= len(suffix)
@ -187,12 +247,6 @@ def normalize_nick(irc, netname, nick, times_tagged=0, uid=''):
if times_tagged >= 1: if times_tagged >= 1:
nick += suffix nick += suffix
# Loop over every character in the nick, making sure that it only contains valid
# characters.
for char in nick:
if char not in allowed_chars:
nick = nick.replace(char, fallback_separator)
while irc.nick_to_uid(nick) not in (None, uid): while irc.nick_to_uid(nick) not in (None, uid):
# The nick we want exists: Increase the separator length by 1 if the user was already # The nick we want exists: Increase the separator length by 1 if the user was already
# tagged, but couldn't be created due to a nick conflict. This can happen when someone # tagged, but couldn't be created due to a nick conflict. This can happen when someone
@ -255,8 +309,8 @@ def spawn_relay_server(irc, remoteirc):
if irc.connected.is_set(): if irc.connected.is_set():
try: try:
suffix = irc.serverdata.get('relay_server_suffix', conf.conf.get('relay', {}).get('server_suffix', 'relay')) suffix = irc.serverdata.get('relay_server_suffix', conf.conf.get('relay', {}).get('server_suffix', 'relay'))
# Strip any leading or trailing .'s # Strip any leading .'s
suffix = suffix.strip('.') suffix = suffix.lstrip('.')
# On some IRCds (e.g. InspIRCd), we have to delay endburst to prevent triggering # On some IRCds (e.g. InspIRCd), we have to delay endburst to prevent triggering
# join flood protections that are counted locally. # join flood protections that are counted locally.
@ -301,13 +355,12 @@ def get_relay_server_sid(irc, remoteirc, spawn_if_missing=True):
log.debug('(%s) Grabbing spawnlocks_servers[%s] from thread %r in function %r', irc.name, irc.name, log.debug('(%s) Grabbing spawnlocks_servers[%s] from thread %r in function %r', irc.name, irc.name,
threading.current_thread().name, inspect.currentframe().f_code.co_name) threading.current_thread().name, inspect.currentframe().f_code.co_name)
if spawnlocks_servers[irc.name].acquire(timeout=TCONDITION_TIMEOUT): with spawnlocks_servers[irc.name]:
try: try:
sid = relayservers[irc.name][remoteirc.name] sid = relayservers[irc.name][remoteirc.name]
except KeyError: except KeyError:
if not spawn_if_missing: if not spawn_if_missing:
log.debug('(%s) get_relay_server_sid: %s.relay doesn\'t have a known SID, ignoring.', irc.name, remoteirc.name) log.debug('(%s) get_relay_server_sid: %s.relay doesn\'t have a known SID, ignoring.', irc.name, remoteirc.name)
spawnlocks_servers[irc.name].release()
return return
log.debug('(%s) get_relay_server_sid: %s.relay doesn\'t have a known SID, spawning.', irc.name, remoteirc.name) log.debug('(%s) get_relay_server_sid: %s.relay doesn\'t have a known SID, spawning.', irc.name, remoteirc.name)
@ -319,7 +372,6 @@ def get_relay_server_sid(irc, remoteirc, spawn_if_missing=True):
return return
log.debug('(%s) get_relay_server_sid: got %s for %s.relay (round 2)', irc.name, sid, remoteirc.name) log.debug('(%s) get_relay_server_sid: got %s for %s.relay (round 2)', irc.name, sid, remoteirc.name)
spawnlocks_servers[irc.name].release()
return sid return sid
def _has_common_pool(sourcenet, targetnet, namespace): def _has_common_pool(sourcenet, targetnet, namespace):
@ -348,8 +400,19 @@ def spawn_relay_user(irc, remoteirc, user, times_tagged=0, reuse_sid=None):
return return
nick = normalize_nick(remoteirc, irc.name, userobj.nick, times_tagged=times_tagged) nick = normalize_nick(remoteirc, irc.name, userobj.nick, times_tagged=times_tagged)
# Sanitize UTF8 for networks that don't support it
ident = _sanitize(userobj.ident, extrachars='~')
# Truncate idents at 10 characters, because TS6 won't like them otherwise! # Truncate idents at 10 characters, because TS6 won't like them otherwise!
ident = userobj.ident[:10] ident = ident[:10]
# HACK: hybrid will reject idents that start with a symbol
if remoteirc.protoname == 'hybrid':
goodchars = tuple(string.ascii_letters + string.digits + '~')
if not ident.startswith(goodchars):
ident = 'r' + ident
# Normalize hostnames # Normalize hostnames
host = normalize_host(remoteirc, userobj.host) host = normalize_host(remoteirc, userobj.host)
realname = userobj.realname realname = userobj.realname
@ -360,7 +423,7 @@ def spawn_relay_user(irc, remoteirc, user, times_tagged=0, reuse_sid=None):
# Try to get the oper type, adding an "(on <networkname>)" suffix similar to what # Try to get the oper type, adding an "(on <networkname>)" suffix similar to what
# Janus does. # Janus does.
if hasattr(userobj, 'opertype'): if hasattr(userobj, 'opertype'):
log.debug('(%s) relay.get_remote_user: setting OPERTYPE of client for %r to %s', log.debug('(%s) spawn_relay_user: setting OPERTYPE of client for %r to %s',
irc.name, user, userobj.opertype) irc.name, user, userobj.opertype)
opertype = userobj.opertype opertype = userobj.opertype
else: else:
@ -435,9 +498,18 @@ def get_remote_user(irc, remoteirc, user, spawn_if_missing=True, times_tagged=0,
if sbot: if sbot:
return sbot.uids.get(remoteirc.name) return sbot.uids.get(remoteirc.name)
# Ignore invisible users - used to skip joining users who are offline or invisible on
# external transports
if user in irc.users:
hide = getattr(irc.users[user], '_invisible', False)
if hide:
log.debug('(%s) get_remote_user: ignoring user %s since they are marked invisible', irc.name,
user)
return
log.debug('(%s) Grabbing spawnlocks[%s] from thread %r in function %r', irc.name, irc.name, log.debug('(%s) Grabbing spawnlocks[%s] from thread %r in function %r', irc.name, irc.name,
threading.current_thread().name, inspect.currentframe().f_code.co_name) threading.current_thread().name, inspect.currentframe().f_code.co_name)
if spawnlocks[irc.name].acquire(timeout=TCONDITION_TIMEOUT): with spawnlocks[irc.name]:
# Be sort-of thread safe: lock the user spawns for the current net first. # Be sort-of thread safe: lock the user spawns for the current net first.
u = None u = None
try: try:
@ -449,24 +521,11 @@ def get_remote_user(irc, remoteirc, user, spawn_if_missing=True, times_tagged=0,
if spawn_if_missing: if spawn_if_missing:
u = spawn_relay_user(irc, remoteirc, user, times_tagged=times_tagged, reuse_sid=reuse_sid) u = spawn_relay_user(irc, remoteirc, user, times_tagged=times_tagged, reuse_sid=reuse_sid)
# This is a sanity check to make sure netsplits and other state resets
# don't break the relayer. If it turns out there was a client in our relayusers
# cache for the requested UID, but it doesn't match the request,
# assume it was a leftover from the last split and replace it with a new one.
# XXX: this technically means that PyLink is desyncing somewhere, and that we should
# fix this in core properly...
if u and ((u not in remoteirc.users) or remoteirc.users[u].remote != (irc.name, user)):
log.warning('(%s) Possible desync? Got invalid relay UID %s for %s on %s',
irc.name, u, irc.get_friendly_name(user), remoteirc.name)
u = spawn_relay_user(irc, remoteirc, user, times_tagged=times_tagged)
spawnlocks[irc.name].release()
return u return u
else: else:
log.debug('(%s) skipping spawn_relay_user(%s, %s, %s, ...); the local server (%s) is not ready yet', log.debug('(%s) skipping spawn_relay_user(%s, %s, %s, ...); the local server (%s) is not ready yet',
irc.name, irc.name, remoteirc.name, user, irc.name) irc.name, irc.name, remoteirc.name, user, irc.name)
log.debug('(%s) spawn_relay_user: current thread is %s', log.debug('(%s) get_remote_user: current thread is %s',
irc.name, threading.current_thread().name) irc.name, threading.current_thread().name)
def get_orig_user(irc, user, targetirc=None): def get_orig_user(irc, user, targetirc=None):
@ -507,7 +566,7 @@ def get_relay(irc, channel):
"""Finds the matching relay entry name for the given network, channel """Finds the matching relay entry name for the given network, channel
pair, if one exists.""" pair, if one exists."""
chanpair = (irc.name, irc.to_lower(channel)) chanpair = (irc.name, irc.to_lower(str(channel)))
if chanpair in db: # This chanpair is a shared channel; others link to it if chanpair in db: # This chanpair is a shared channel; others link to it
return chanpair return chanpair
@ -614,9 +673,32 @@ def remove_channel(irc, channel):
del relayusers[remoteuser][irc.name] del relayusers[remoteuser][irc.name]
irc.quit(user, 'Left all shared channels.') irc.quit(user, 'Left all shared channels.')
def _claim_should_bounce(irc, channel):
"""
Returns whether we should bounce the next action that fails CLAIM.
This is used to prevent kick/mode/topic wars with services.
"""
with claim_bounce_cache_lock:
if irc.name not in claim_bounce_cache: # Nothing in the cache to worry about
return True
limit = irc.get_service_option('relay', 'claim_bounce_limit', default=15)
if limit < 0: # Disabled
return True
elif limit < 5: # Anything below this is just asking for desyncs...
log.warning('(%s) relay: the minimum supported value for relay::claim_bounce_limit is 5.', irc.name)
limit = 5
success = claim_bounce_cache[irc.name] <= limit
ttl = claim_bounce_cache.ttl
if not success:
log.warning("(%s) relay: %s received more than %s claim bounces in %s seconds - your channel may be desynced!",
irc.name, channel, limit, ttl)
return success
def check_claim(irc, channel, sender, chanobj=None): def check_claim(irc, channel, sender, chanobj=None):
""" """
Checks whether the sender of a kick/mode change passes CLAIM checks for Checks whether the sender of a kick/mode/topic change passes CLAIM checks for
a given channel. This returns True if any of the following criteria are met: a given channel. This returns True if any of the following criteria are met:
1) No relay exists for the channel in question. 1) No relay exists for the channel in question.
@ -637,13 +719,23 @@ def check_claim(irc, channel, sender, chanobj=None):
log.debug('(%s) relay.check_claim: sender modes (%s/%s) are %s (mlist=%s)', irc.name, log.debug('(%s) relay.check_claim: sender modes (%s/%s) are %s (mlist=%s)', irc.name,
sender, channel, sender_modes, mlist) sender, channel, sender_modes, mlist)
# XXX: stop hardcoding modes to check for and support mlist in isHalfopPlus and friends # XXX: stop hardcoding modes to check for and support mlist in isHalfopPlus and friends
return (not relay) or irc.name == relay[0] or not db[relay]['claim'] or \ success = (not relay) or irc.name == relay[0] or not db[relay]['claim'] or \
irc.name in db[relay]['claim'] or \ irc.name in db[relay]['claim'] or \
(any([mode in sender_modes for mode in ('y', 'q', 'a', 'o', 'h')]) (any([mode in sender_modes for mode in {'y', 'q', 'a', 'o', 'h'}])
and not irc.is_privileged_service(sender)) \ and not irc.is_privileged_service(sender)) \
or irc.is_internal_client(sender) or \ or irc.is_internal_client(sender) or \
irc.is_internal_server(sender) irc.is_internal_server(sender)
# Increment claim_bounce_cache, checked in _claim_should_bounce()
if not success:
with claim_bounce_cache_lock:
if irc.name not in claim_bounce_cache:
claim_bounce_cache[irc.name] = 1
else:
claim_bounce_cache[irc.name] += 1
return success
def get_supported_umodes(irc, remoteirc, modes): def get_supported_umodes(irc, remoteirc, modes):
"""Given a list of user modes, filters out all of those not supported by the """Given a list of user modes, filters out all of those not supported by the
remote network.""" remote network."""
@ -695,16 +787,7 @@ def get_supported_umodes(irc, remoteirc, modes):
def is_relay_client(irc, user): def is_relay_client(irc, user):
"""Returns whether the given user is a relay client.""" """Returns whether the given user is a relay client."""
try: return user in irc.users and hasattr(irc.users[user], 'remote')
if irc.users[user].remote:
# Is the .remote attribute set? If so, don't relay already
# relayed clients; that'll trigger an endless loop!
return True
except AttributeError: # Nope, it isn't.
pass
except KeyError: # The user doesn't exist?!?
return True
return False
isRelayClient = is_relay_client isRelayClient = is_relay_client
def iterate_all(origirc, func, extra_args=(), kwargs=None): def iterate_all(origirc, func, extra_args=(), kwargs=None):
@ -745,7 +828,7 @@ def relay_joins(irc, channel, users, ts, targetirc=None, **kwargs):
""" """
log.debug('(%s) relay.relay_joins: called on %r with users %r, targetirc=%s', irc.name, channel, log.debug('(%s) relay.relay_joins: called on %r with users %r, targetirc=%s', irc.name, channel,
['%s/%s' % (user, irc.get_friendly_name(user)) for user in users], targetirc) users, targetirc)
if ts < 750000: if ts < 750000:
current_ts = int(time.time()) current_ts = int(time.time())
@ -757,6 +840,9 @@ def relay_joins(irc, channel, users, ts, targetirc=None, **kwargs):
def _relay_joins_loop(irc, remoteirc, channel, users, ts, burst=True): def _relay_joins_loop(irc, remoteirc, channel, users, ts, burst=True):
queued_users = [] queued_users = []
if not remoteirc.connected.is_set():
return # Remote network is not ready yet.
remotechan = get_remote_channel(irc, remoteirc, channel) remotechan = get_remote_channel(irc, remoteirc, channel)
if remotechan is None: if remotechan is None:
# If there is no link on the current network for the channel in question, # If there is no link on the current network for the channel in question,
@ -1076,8 +1162,8 @@ def get_supported_cmodes(irc, remoteirc, channel, modes):
# First, we expand extbans from the local IRCd into a named mode and argument pair. Then, we # First, we expand extbans from the local IRCd into a named mode and argument pair. Then, we
# can figure out how to relay it. # can figure out how to relay it.
for extban_name, extban_prefix in irc.extbans_acting.items(): for extban_name, extban_prefix in irc.extbans_acting.items():
# Acting extbans are only supported with +b (e.g. +b m:n!u@h) # Acting extbans are generally only supported with +b and +e
if name == 'ban' and arg.startswith(extban_prefix): if name in {'ban', 'banexception'} and arg.startswith(extban_prefix):
orig_supported_char, old_arg = supported_char, arg orig_supported_char, old_arg = supported_char, arg
if extban_name in remoteirc.cmodes: if extban_name in remoteirc.cmodes:
@ -1271,7 +1357,7 @@ def handle_join(irc, numeric, command, args):
modes = [] modes = []
for user in users: for user in users:
# XXX: Find the diff of the new and old mode lists of the channel. Not pretty, but I'd # XXX: Find the diff of the new and old mode lists of the channel. Not pretty, but I'd
# rather not change the 'users' format of SJOIN just for this. -GL # rather not change the 'users' format of SJOIN just for this. -jlu5
try: try:
oldmodes = set(chandata.get_prefix_modes(user)) oldmodes = set(chandata.get_prefix_modes(user))
except KeyError: except KeyError:
@ -1287,14 +1373,18 @@ def handle_join(irc, numeric, command, args):
irc.name, user, channel, modediff, oldmodes, newmodes) irc.name, user, channel, modediff, oldmodes, newmodes)
for modename in modediff: for modename in modediff:
modechar = irc.cmodes.get(modename) modechar = irc.cmodes.get(modename)
# Special case for U-lined servers: allow them to join with ops, # Special case for U-lined servers: allow them to join with ops, but don't forward this mode change on.
# but don't forward this mode change on.
if modechar and not irc.is_privileged_service(numeric): if modechar and not irc.is_privileged_service(numeric):
modes.append(('-%s' % modechar, user)) modes.append(('-%s' % modechar, user))
if modes: if modes:
if _claim_should_bounce(irc, channel):
log.debug('(%s) relay.handle_join: reverting modes on BURST: %s', irc.name, irc.join_modes(modes)) log.debug('(%s) relay.handle_join: reverting modes on BURST: %s', irc.name, irc.join_modes(modes))
irc.mode(irc.sid, channel, modes) irc.mode(irc.sid, channel, modes)
else:
# HACK: pretend we managed to deop the caller, so that they can't bypass claim entirely
log.debug('(%s) relay.handle_join: fake reverting modes on BURST: %s', irc.name, irc.join_modes(modes))
irc.apply_modes(channel, modes)
relay_joins(irc, channel, users, ts, burst=False) relay_joins(irc, channel, users, ts, burst=False)
utils.add_hook(handle_join, 'JOIN') utils.add_hook(handle_join, 'JOIN')
@ -1306,7 +1396,7 @@ def handle_quit(irc, numeric, command, args):
log.debug('(%s) Grabbing spawnlocks[%s] from thread %r in function %r', irc.name, irc.name, log.debug('(%s) Grabbing spawnlocks[%s] from thread %r in function %r', irc.name, irc.name,
threading.current_thread().name, inspect.currentframe().f_code.co_name) threading.current_thread().name, inspect.currentframe().f_code.co_name)
if spawnlocks[irc.name].acquire(timeout=TCONDITION_TIMEOUT): with spawnlocks[irc.name]:
def _handle_quit_func(irc, remoteirc, user): def _handle_quit_func(irc, remoteirc, user):
try: # Try to quit the client. If this fails because they're missing, bail. try: # Try to quit the client. If this fails because they're missing, bail.
@ -1316,7 +1406,6 @@ def handle_quit(irc, numeric, command, args):
iterate_all_present(irc, numeric, _handle_quit_func) iterate_all_present(irc, numeric, _handle_quit_func)
del relayusers[(irc.name, numeric)] del relayusers[(irc.name, numeric)]
spawnlocks[irc.name].release()
utils.add_hook(handle_quit, 'QUIT') utils.add_hook(handle_quit, 'QUIT')
@ -1387,7 +1476,7 @@ def handle_part(irc, numeric, command, args):
for user in irc.channels[channel].users.copy(): for user in irc.channels[channel].users.copy():
if (not irc.is_internal_client(user)) and (not is_relay_client(irc, user)): if (not irc.is_internal_client(user)) and (not is_relay_client(irc, user)):
irc.call_hooks([irc.sid, 'CLIENTBOT_SERVICE_KICKED', {'channel': channel, 'target': user, irc.call_hooks([irc.sid, 'CLIENTBOT_SERVICE_KICKED', {'channel': channel, 'target': user,
'text': 'Clientbot was force parted (Reason: %s)' % text or 'None', 'text': 'Clientbot was force parted (%s)' % text or 'None',
'parse_as': 'KICK'}]) 'parse_as': 'KICK'}])
irc.join(irc.pseudoclient.uid, channel) irc.join(irc.pseudoclient.uid, channel)
@ -1432,6 +1521,9 @@ def handle_messages(irc, numeric, command, args):
log.debug('(%s) relay.handle_messages: dropping PM from server %s to %s', log.debug('(%s) relay.handle_messages: dropping PM from server %s to %s',
irc.name, numeric, target) irc.name, numeric, target)
return return
elif not irc.has_cap('can-spawn-clients') and not world.plugins.get('relay_clientbot'):
# For consistency, only read messages from clientbot networks if relay_clientbot is loaded
return
relay = get_relay(irc, target) relay = get_relay(irc, target)
remoteusers = relayusers[(irc.name, numeric)] remoteusers = relayusers[(irc.name, numeric)]
@ -1439,6 +1531,7 @@ def handle_messages(irc, numeric, command, args):
avail_prefixes = {v: k for k, v in irc.prefixmodes.items()} avail_prefixes = {v: k for k, v in irc.prefixmodes.items()}
prefixes = [] prefixes = []
# Split up @#channel prefixes and the like into their prefixes and target components # Split up @#channel prefixes and the like into their prefixes and target components
if isinstance(target, str):
while target and target[0] in avail_prefixes: while target and target[0] in avail_prefixes:
prefixes.append(avail_prefixes[target[0]]) prefixes.append(avail_prefixes[target[0]])
target = target[1:] target = target[1:]
@ -1599,7 +1692,7 @@ def handle_kick(irc, source, command, args):
if (not irc.has_cap('can-spawn-clients')) and irc.pseudoclient and target == irc.pseudoclient.uid: if (not irc.has_cap('can-spawn-clients')) and irc.pseudoclient and target == irc.pseudoclient.uid:
for user in irc.channels[channel].users: for user in irc.channels[channel].users:
if (not irc.is_internal_client(user)) and (not is_relay_client(irc, user)): if (not irc.is_internal_client(user)) and (not is_relay_client(irc, user)):
reason = "Clientbot kicked by %s (Reason: %s)" % (irc.get_friendly_name(source), text) reason = "Clientbot kicked by %s (%s)" % (irc.get_friendly_name(source), text)
irc.call_hooks([irc.sid, 'CLIENTBOT_SERVICE_KICKED', {'channel': channel, 'target': user, irc.call_hooks([irc.sid, 'CLIENTBOT_SERVICE_KICKED', {'channel': channel, 'target': user,
'text': reason, 'parse_as': 'KICK'}]) 'text': reason, 'parse_as': 'KICK'}])
@ -1669,17 +1762,16 @@ def handle_kick(irc, source, command, args):
del relayusers[(irc.name, target)][remoteirc.name] del relayusers[(irc.name, target)][remoteirc.name]
remoteirc.quit(real_target, 'Left all shared channels.') remoteirc.quit(real_target, 'Left all shared channels.')
# Kick was a relay client but sender does not pass CLAIM restrictions. Bounce a rejoin unless we've reached our limit.
if is_relay_client(irc, target) and not check_claim(irc, channel, kicker): if is_relay_client(irc, target) and not check_claim(irc, channel, kicker):
if _claim_should_bounce(irc, channel):
homenet, real_target = get_orig_user(irc, target) homenet, real_target = get_orig_user(irc, target)
homeirc = world.networkobjects.get(homenet) homeirc = world.networkobjects.get(homenet)
homenick = homeirc.users[real_target].nick if homeirc else '<ghost user>' homenick = homeirc.users[real_target].nick if homeirc else '<ghost user>'
homechan = get_remote_channel(irc, homeirc, channel) homechan = get_remote_channel(irc, homeirc, channel)
log.debug('(%s) relay.handle_kick: kicker %s is not opped... We should rejoin the target user %s', irc.name, kicker, real_target) log.debug('(%s) relay.handle_kick: kicker %s is not opped... We should rejoin the target user %s', irc.name, kicker, real_target)
# Home network is not in the channel's claim AND the kicker is not # FIXME: make the check slightly more advanced: i.e. halfops can't kick ops, admins can't kick owners, etc.
# opped. We won't propograte the kick then.
# TODO: make the check slightly more advanced: i.e. halfops can't
# kick ops, admins can't kick owners, etc.
modes = get_prefix_modes(homeirc, irc, homechan, real_target) modes = get_prefix_modes(homeirc, irc, homechan, real_target)
# Join the kicked client back with its respective modes. # Join the kicked client back with its respective modes.
@ -1751,8 +1843,11 @@ def handle_mode(irc, numeric, command, args):
get_relay_server_sid(remoteirc, irc) or remoteirc.sid get_relay_server_sid(remoteirc, irc) or remoteirc.sid
if not remoteirc.has_cap('can-spawn-clients'): if not remoteirc.has_cap('can-spawn-clients'):
if numeric in irc.servers and not irc.servers[numeric].has_eob:
log.debug('(%s) Not relaying modes from server %s/%s to %s as it has not finished bursting',
irc.name, numeric, irc.get_friendly_name(numeric), remoteirc.name)
else:
friendly_modes = [] friendly_modes = []
for modepair in modes: for modepair in modes:
modechar = modepair[0][-1] modechar = modepair[0][-1]
if modechar in irc.prefixmodes: if modechar in irc.prefixmodes:
@ -1781,10 +1876,19 @@ def handle_mode(irc, numeric, command, args):
# Set hideoper on remote opers, to prevent inflating # Set hideoper on remote opers, to prevent inflating
# /lusers and various /stats # /lusers and various /stats
hideoper_mode = remoteirc.umodes.get('hideoper') hideoper_mode = remoteirc.umodes.get('hideoper')
try:
use_hideoper = conf.conf['relay']['hideoper']
except KeyError:
use_hideoper = True
# If Relay oper hiding is enabled, don't allow unsetting +H
if use_hideoper and ('-%s' % hideoper_mode, None) in modes:
modes.remove(('-%s' % hideoper_mode, None))
modes = get_supported_umodes(irc, remoteirc, modes) modes = get_supported_umodes(irc, remoteirc, modes)
if hideoper_mode: if hideoper_mode:
if ('+o', None) in modes: if ('+o', None) in modes and use_hideoper:
modes.append(('+%s' % hideoper_mode, None)) modes.append(('+%s' % hideoper_mode, None))
elif ('-o', None) in modes: elif ('-o', None) in modes:
modes.append(('-%s' % hideoper_mode, None)) modes.append(('-%s' % hideoper_mode, None))
@ -1814,15 +1918,15 @@ def handle_mode(irc, numeric, command, args):
if irc.is_privileged_service(numeric): if irc.is_privileged_service(numeric):
# Special hack for "U-lined" servers - ignore changes to SIMPLE modes and # Special hack for "U-lined" servers - ignore changes to SIMPLE modes and
# attempts to op u-lined clients (trying to change status for others # attempts to op its own clients (trying to change status for others
# SHOULD be reverted). # SHOULD be reverted).
# This is for compatibility with Anope's DEFCON for the most part, as well as # This is for compatibility with Anope's DEFCON for the most part, as well as
# silly people who try to register a channel multiple times via relay. # silly people who try to register a channel multiple times via relay.
reversed_modes = [modepair for modepair in reversed_modes if reversed_modes = [modepair for modepair in reversed_modes if
# Mode is a prefix mode but target isn't ulined, revert # Include prefix modes if target isn't also U-lined
((modepair[0][-1] in irc.prefixmodes and not ((modepair[0][-1] in irc.prefixmodes and not
irc.is_privileged_service(modepair[1])) irc.is_privileged_service(modepair[1]))
# Tried to set a list mode, revert # Include all list modes (bans, etc.)
or modepair[0][-1] in irc.cmodes['*A']) or modepair[0][-1] in irc.cmodes['*A'])
] ]
modes.clear() # Clear the mode list so nothing is relayed below modes.clear() # Clear the mode list so nothing is relayed below
@ -1855,9 +1959,14 @@ def handle_mode(irc, numeric, command, args):
irc.name, str(modepair), target) irc.name, str(modepair), target)
if reversed_modes: if reversed_modes:
log.debug('(%s) relay.handle_mode: Reversing mode changes of %r with %r.', if _claim_should_bounce(irc, target):
irc.name, args['modes'], reversed_modes) log.debug('(%s) relay.handle_mode: Reversing mode changes %r on %s with %r.',
irc.name, args['modes'], target, reversed_modes)
irc.mode(irc.sid, target, reversed_modes) irc.mode(irc.sid, target, reversed_modes)
else:
log.debug('(%s) relay.handle_mode: Fake reversing mode changes %r on %s with %r.',
irc.name, args['modes'], target, reversed_modes)
irc.apply_modes(target, reversed_modes)
if modes: if modes:
iterate_all(irc, _handle_mode_loop, extra_args=(numeric, command, target, modes)) iterate_all(irc, _handle_mode_loop, extra_args=(numeric, command, target, modes))
@ -1891,7 +2000,7 @@ def handle_topic(irc, numeric, command, args):
remoteirc.topic_burst(rsid, remotechan, topic) remoteirc.topic_burst(rsid, remotechan, topic)
iterate_all(irc, _handle_topic_loop, extra_args=(numeric, command, args)) iterate_all(irc, _handle_topic_loop, extra_args=(numeric, command, args))
elif oldtopic: # Topic change blocked by claim. elif oldtopic and _claim_should_bounce(irc, channel): # Topic change blocked by claim.
irc.topic_burst(irc.sid, channel, oldtopic) irc.topic_burst(irc.sid, channel, oldtopic)
utils.add_hook(handle_topic, 'TOPIC') utils.add_hook(handle_topic, 'TOPIC')
@ -1946,6 +2055,8 @@ def handle_kill(irc, numeric, command, args):
def _relay_kill_to_kick(origirc, remoteirc, rtarget): def _relay_kill_to_kick(origirc, remoteirc, rtarget):
# Forward as a kick to each present relay client # Forward as a kick to each present relay client
remotechan = get_remote_channel(origirc, remoteirc, homechan) remotechan = get_remote_channel(origirc, remoteirc, homechan)
if not remotechan:
return
rsender = get_relay_server_sid(remoteirc, irc, spawn_if_missing=False) or \ rsender = get_relay_server_sid(remoteirc, irc, spawn_if_missing=False) or \
remoteirc.sid remoteirc.sid
log.debug('(%s) relay.handle_kill: forwarding kill to %s/%s@%s as ' log.debug('(%s) relay.handle_kill: forwarding kill to %s/%s@%s as '
@ -1959,7 +2070,7 @@ def handle_kill(irc, numeric, command, args):
# Then, forward to the home network. # Then, forward to the home network.
hsender = get_relay_server_sid(origirc, irc, spawn_if_missing=False) or \ hsender = get_relay_server_sid(origirc, irc, spawn_if_missing=False) or \
homeirc.sid origirc.sid
log.debug('(%s) relay.handle_kill: forwarding kill to %s/%s@%s as ' log.debug('(%s) relay.handle_kill: forwarding kill to %s/%s@%s as '
'kick on %s', irc.name, realuser[1], target_nick, 'kick on %s', irc.name, realuser[1], target_nick,
realuser[0], homechan) realuser[0], homechan)
@ -1974,13 +2085,9 @@ def handle_kill(irc, numeric, command, args):
irc.sjoin(irc.sid, localchan, [(modes, client)]) irc.sjoin(irc.sid, localchan, [(modes, client)])
# Target user was local. # Target user was local.
else: elif userdata:
# IMPORTANT: some IRCds (charybdis) don't send explicit QUIT messages reason = 'Killed (%s (%s))' % (irc.get_friendly_name(numeric), args['text'])
# for locally killed clients, while others (inspircd) do! handle_quit(irc, target, 'KILL', {'text': reason})
# If we receive a user object in 'userdata' instead of None, it means
# that the KILL hasn't been handled by a preceding QUIT message.
if userdata:
handle_quit(irc, target, 'KILL', {'text': args['text']})
utils.add_hook(handle_kill, 'KILL') utils.add_hook(handle_kill, 'KILL')
@ -1988,6 +2095,22 @@ def handle_away(irc, numeric, command, args):
iterate_all_present(irc, numeric, iterate_all_present(irc, numeric,
lambda irc, remoteirc, user: lambda irc, remoteirc, user:
remoteirc.away(user, args['text'])) remoteirc.away(user, args['text']))
# Check invisible flag, used by external transports to hide offline users
if not irc.is_internal_client(numeric):
invisible = args.get('now_invisible')
log.debug('(%s) relay.handle_away: invisible flag: %s', irc.name, invisible)
if invisible:
# User is now invisible - quit them
log.debug('(%s) relay.handle_away: quitting user %s due to invisible flag', irc.name, numeric)
handle_quit(irc, numeric, 'AWAY_NOW_INVISIBLE', {'text': "User has gone offline"})
elif invisible is False:
# User is no longer invisible - join them to all channels
log.debug('(%s) relay.handle_away: rejoining user %s due to invisible flag', irc.name, numeric)
for channel in irc.users[numeric].channels:
c = irc.channels[channel]
relay_joins(irc, channel, [numeric], c.ts, burst=True)
utils.add_hook(handle_away, 'AWAY') utils.add_hook(handle_away, 'AWAY')
def handle_invite(irc, source, command, args): def handle_invite(irc, source, command, args):
@ -2034,19 +2157,17 @@ def handle_disconnect(irc, numeric, command, args):
# them from our relay clients index. # them from our relay clients index.
log.debug('(%s) Grabbing spawnlocks[%s] from thread %r in function %r', irc.name, irc.name, log.debug('(%s) Grabbing spawnlocks[%s] from thread %r in function %r', irc.name, irc.name,
threading.current_thread().name, inspect.currentframe().f_code.co_name) threading.current_thread().name, inspect.currentframe().f_code.co_name)
if spawnlocks[irc.name].acquire(timeout=TCONDITION_TIMEOUT): with spawnlocks[irc.name]:
for k, v in relayusers.copy().items(): for k, v in relayusers.copy().items():
if irc.name in v: if irc.name in v:
del relayusers[k][irc.name] del relayusers[k][irc.name]
if k[0] == irc.name: if k[0] == irc.name:
del relayusers[k] del relayusers[k]
spawnlocks[irc.name].release()
# SQUIT all relay pseudoservers spawned for us, and remove them # SQUIT all relay pseudoservers spawned for us, and remove them
# from our relay subservers index. # from our relay subservers index.
log.debug('(%s) Grabbing spawnlocks_servers[%s] from thread %r in function %r', irc.name, irc.name, log.debug('(%s) Grabbing spawnlocks_servers[%s] from thread %r in function %r', irc.name, irc.name,
threading.current_thread().name, inspect.currentframe().f_code.co_name) threading.current_thread().name, inspect.currentframe().f_code.co_name)
if spawnlocks_servers[irc.name].acquire(timeout=TCONDITION_TIMEOUT): with spawnlocks_servers[irc.name]:
def _handle_disconnect_loop(irc, remoteirc): def _handle_disconnect_loop(irc, remoteirc):
name = remoteirc.name name = remoteirc.name
@ -2068,8 +2189,6 @@ def handle_disconnect(irc, numeric, command, args):
except KeyError: # Already removed; ignore. except KeyError: # Already removed; ignore.
pass pass
spawnlocks_servers[irc.name].release()
# Announce the disconnects to every leaf channel where the disconnected network is the owner # Announce the disconnects to every leaf channel where the disconnected network is the owner
announcement = conf.conf.get('relay', {}).get('disconnect_announcement') announcement = conf.conf.get('relay', {}).get('disconnect_announcement')
log.debug('(%s) relay: last connection successful: %s', irc.name, args.get('was_successful')) log.debug('(%s) relay: last connection successful: %s', irc.name, args.get('was_successful'))
@ -2104,20 +2223,37 @@ def handle_disconnect(irc, numeric, command, args):
utils.add_hook(handle_disconnect, "PYLINK_DISCONNECT") utils.add_hook(handle_disconnect, "PYLINK_DISCONNECT")
def nick_collide(irc, target): def forcetag_nick(irc, target):
""" """
Handles nick collisions on relay clients and attempts to fix nicks. Force tags the target UID's nick, if it is a relay client.
"""
remotenet, remoteuser = get_orig_user(irc, target)
remoteirc = world.networkobjects[remotenet]
This method is used to handle nick collisions between relay clients and outside ones.
Returns the new nick if the operation succeeded; otherwise returns False.
"""
remote = get_orig_user(irc, target)
if remote is None:
return False
remotenet, remoteuser = remote
try:
remoteirc = world.networkobjects[remotenet]
nick = remoteirc.users[remoteuser].nick nick = remoteirc.users[remoteuser].nick
except KeyError:
return False
# Force a tagged nick by setting times_tagged to 1. # Force a tagged nick by setting times_tagged to 1.
newnick = normalize_nick(irc, remotenet, nick, times_tagged=1) newnick = normalize_nick(irc, remotenet, nick, times_tagged=1)
log.debug('(%s) relay.nick_collide: Fixing nick of relay client %r (%s) to %s', log.debug('(%s) relay.forcetag_nick: Fixing nick of relay client %r (%s) to %s',
irc.name, target, nick, newnick) irc.name, target, nick, newnick)
if nick == newnick:
log.debug('(%s) relay.forcetag_nick: New nick %s for %r matches old nick %s',
irc.name, newnick, target, nick)
return False
irc.nick(target, newnick) irc.nick(target, newnick)
return newnick
def handle_save(irc, numeric, command, args): def handle_save(irc, numeric, command, args):
target = args['target'] target = args['target']
@ -2126,7 +2262,7 @@ def handle_save(irc, numeric, command, args):
# Nick collision! # Nick collision!
# It's one of our relay clients; try to fix our nick to the next # It's one of our relay clients; try to fix our nick to the next
# available normalized nick. # available normalized nick.
nick_collide(irc, target) forcetag_nick(irc, target)
else: else:
# Somebody else on the network (not a PyLink client) had a nick collision; # Somebody else on the network (not a PyLink client) had a nick collision;
# relay this as a nick change appropriately. # relay this as a nick change appropriately.
@ -2141,7 +2277,7 @@ def handle_svsnick(irc, numeric, command, args):
target = args['target'] target = args['target']
if is_relay_client(irc, target): if is_relay_client(irc, target):
nick_collide(irc, target) forcetag_nick(irc, target)
utils.add_hook(handle_svsnick, "SVSNICK") utils.add_hook(handle_svsnick, "SVSNICK")
@ -2212,7 +2348,7 @@ def create(irc, source, args):
creator = irc.get_hostmask(source) creator = irc.get_hostmask(source)
# Create the relay database entry with the (network name, channel name) # Create the relay database entry with the (network name, channel name)
# pair - this is just a dict with various keys. # pair - this is just a dict with various keys.
db[(irc.name, channel)] = {'links': set(), db[(irc.name, str(channel))] = {'links': set(),
'blocked_nets': set(), 'blocked_nets': set(),
'creator': creator, 'creator': creator,
'ts': time.time(), 'ts': time.time(),
@ -2238,11 +2374,11 @@ def destroy(irc, source, args):
Removes the given channel from the PyLink Relay, delinking all networks linked to it. If the home network is given and you are logged in as admin, this can also remove relay channels from other networks.""" Removes the given channel from the PyLink Relay, delinking all networks linked to it. If the home network is given and you are logged in as admin, this can also remove relay channels from other networks."""
try: # Two args were given: first one is network name, second is channel. try: # Two args were given: first one is network name, second is channel.
channel = irc.to_lower(args[1]) channel = args[1]
network = args[0] network = args[0]
except IndexError: except IndexError:
try: # One argument was given; assume it's just the channel. try: # One argument was given; assume it's just the channel.
channel = irc.to_lower(args[0]) channel = args[0]
network = irc.name network = irc.name
except IndexError: except IndexError:
irc.error("Not enough arguments. Needs 1-2: channel, network (optional).") irc.error("Not enough arguments. Needs 1-2: channel, network (optional).")
@ -2259,7 +2395,11 @@ def destroy(irc, source, args):
else: else:
permissions.check_permissions(irc, source, ['relay.destroy.remote']) permissions.check_permissions(irc, source, ['relay.destroy.remote'])
# Allow deleting old channels if the local network's casemapping ever changes
if (network, channel) in db:
entry = (network, channel) entry = (network, channel)
else:
entry = (network, irc.to_lower(channel))
if entry in db: if entry in db:
stop_relay(entry) stop_relay(entry)
del db[entry] del db[entry]
@ -2320,14 +2460,17 @@ def link(irc, source, args):
args = link_parser.parse_args(args) args = link_parser.parse_args(args)
# Normalize channel case # Normalize channel case. For the target channel it's possible for the local and remote casemappings
channel = irc.to_lower(args.channel) # to differ - if we find the unnormalized channel name in the list, we should just use that.
localchan = irc.to_lower(args.localchannel or args.channel) # This mainly affects channels with e.g. | in them.
channel_orig = str(args.channel)
channel_norm = irc.to_lower(channel_orig)
localchan = irc.to_lower(str(args.localchannel or args.channel))
remotenet = args.remotenet remotenet = args.remotenet
for c in (channel, localchan): if not irc.is_channel(localchan):
if not irc.is_channel(c): irc.error('Invalid channel %r.' % localchan)
irc.error('Invalid channel %r.' % c)
return return
if remotenet == irc.name: if remotenet == irc.name:
@ -2366,12 +2509,15 @@ def link(irc, source, args):
irc.error('Channel %r is already part of a relay.' % localchan) irc.error('Channel %r is already part of a relay.' % localchan)
return return
try: if (remotenet, channel_orig) in db:
entry = db[(remotenet, channel)] channel = channel_orig
except KeyError: elif (remotenet, channel_norm) in db:
channel = channel_norm
else:
irc.error('No such relay %r exists.' % args.channel) irc.error('No such relay %r exists.' % args.channel)
return return
else: entry = db[(remotenet, channel)]
whitelist_mode = entry.get('use_whitelist', False) whitelist_mode = entry.get('use_whitelist', False)
if ((not whitelist_mode) and irc.name in entry['blocked_nets']) or \ if ((not whitelist_mode) and irc.name in entry['blocked_nets']) or \
(whitelist_mode and irc.name not in entry.get('allowed_nets', set())): (whitelist_mode and irc.name not in entry.get('allowed_nets', set())):
@ -2649,73 +2795,6 @@ def linkacl(irc, source, args):
else: else:
irc.error('Unknown subcommand %r: valid ones are ALLOW, DENY, and LIST.' % cmd) irc.error('Unknown subcommand %r: valid ones are ALLOW, DENY, and LIST.' % cmd)
@utils.add_cmd
def showuser(irc, source, args):
"""<user>
Shows relay data about the given user. This supplements the 'showuser' command in the 'commands' plugin, which provides more general information."""
try:
target = args[0]
except IndexError:
# No errors here; showuser from the commands plugin already does this
# for us.
return
u = irc.nick_to_uid(target)
if u:
irc.reply("Showing relay information on user \x02%s\x02:" % irc.users[u].nick, private=True)
try:
userpair = get_orig_user(irc, u) or (irc.name, u)
remoteusers = relayusers[userpair].items()
except KeyError:
pass
else:
nicks = []
if remoteusers:
nicks.append('%s:\x02%s\x02' % (userpair[0],
world.networkobjects[userpair[0]].users[userpair[1]].nick))
for r in remoteusers:
remotenet, remoteuser = r
remoteirc = world.networkobjects[remotenet]
nicks.append('%s:\x02%s\x02' % (remotenet, remoteirc.users[remoteuser].nick))
irc.reply("\x02Relay nicks\x02: %s" % ', '.join(nicks), private=True)
relaychannels = []
for ch in irc.users[u].channels:
relay = get_relay(irc, ch)
if relay:
relaychannels.append(''.join(relay))
if relaychannels and (irc.is_oper(source) or u == source):
irc.reply("\x02Relay channels\x02: %s" % ' '.join(relaychannels), private=True)
@utils.add_cmd
def showchan(irc, source, args):
"""<user>
Shows relay data about the given channel. This supplements the 'showchan' command in the 'commands' plugin, which provides more general information."""
try:
channel = irc.to_lower(args[0])
except IndexError:
return
if channel not in irc.channels:
return
f = lambda s: irc.reply(s, private=True)
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.is_oper(source)
secret = ('s', None) in c.modes
if secret and not verbose:
# Hide secret channels from normal users.
return
else:
relayentry = get_relay(irc, channel)
if relayentry:
relays = ['\x02%s\x02' % ''.join(relayentry)]
relays += [''.join(link) for link in db[relayentry]['links']]
f('\x02Relayed channels:\x02 %s' % (' '.join(relays)))
@utils.add_cmd @utils.add_cmd
def save(irc, source, args): def save(irc, source, args):
"""takes no arguments. """takes no arguments.
@ -2782,7 +2861,7 @@ def modedelta(irc, source, args):
Mode names are defined using PyLink named modes, and not IRC mode characters: you can find a Mode names are defined using PyLink named modes, and not IRC mode characters: you can find a
list of channel named modes and the characters they map to on different IRCds at: list of channel named modes and the characters they map to on different IRCds at:
https://raw.githack.com/GLolol/PyLink/devel/docs/modelists/channel-modes.html https://raw.githack.com/jlu5/PyLink/devel/docs/modelists/channel-modes.html
Examples of setting modes: Examples of setting modes:
@ -2847,7 +2926,8 @@ def modedelta(irc, source, args):
if modes: if modes:
old_modes = db[relay].get('modedelta', []) old_modes = db[relay].get('modedelta', [])
db[relay]['modedelta'] = target_modes = modes db[relay]['modedelta'] = modes
target_modes = modes.copy()
log.debug('channel: %s', str(channel)) log.debug('channel: %s', str(channel))
irc.reply('Set the mode delta for \x02%s\x02 to: %s' % (channel, modes)) irc.reply('Set the mode delta for \x02%s\x02 to: %s' % (channel, modes))
else: # No modes given, so show the list. else: # No modes given, so show the list.
@ -2867,8 +2947,7 @@ def modedelta(irc, source, args):
continue continue
remote_modes = [] remote_modes = []
# For each leaf channel, unset the old mode delta and set the new one # For each leaf channel, unset the old mode delta and set the new one if applicable.
# if applicable.
log.debug('(%s) modedelta target modes for %s/%s: %s', irc.name, remotenet, remotechan, target_modes) log.debug('(%s) modedelta target modes for %s/%s: %s', irc.name, remotenet, remotechan, target_modes)
for modepair in target_modes: for modepair in target_modes:
modeprefix = modepair[0][0] modeprefix = modepair[0][0]
@ -2929,3 +3008,24 @@ def chandesc(irc, source, args):
irc.reply('Done. Updated the description for \x02%s\x02.' % channel) irc.reply('Done. Updated the description for \x02%s\x02.' % channel)
else: else:
irc.reply('Description for \x02%s\x02: %s' % (channel, db[relay].get('description') or '\x1D(none)\x1D')) irc.reply('Description for \x02%s\x02: %s' % (channel, db[relay].get('description') or '\x1D(none)\x1D'))
@utils.add_cmd
def forcetag(irc, source, args):
"""<nick>
Attempts to forcetag the given nick, if it is a relay client.
"""
try:
nick = args[0]
except IndexError:
irc.error("Not enough arguments. Needs 1: target nick.")
return
permissions.check_permissions(irc, source, ['relay.forcetag'])
uid = irc.nick_to_uid(nick) or nick
result = forcetag_nick(irc, uid)
if result:
irc.reply('Done. Forcetagged %s to %s' % (nick, result))
else:
irc.error('User %s is already tagged or not a relay client.' % nick)

View File

@ -1,8 +1,9 @@
# relay_clientbot.py: Clientbot extensions for Relay # relay_clientbot.py: Clientbot extensions for Relay
import shlex
import string import string
import time import time
from pylinkirc import utils, conf, world from pylinkirc import conf, utils, world
from pylinkirc.log import log from pylinkirc.log import log
# Clientbot default styles: # Clientbot default styles:
@ -26,6 +27,8 @@ def color_text(s):
""" """
Returns a colorized version of the given text based on a simple hash algorithm. Returns a colorized version of the given text based on a simple hash algorithm.
""" """
if not s:
return s
colors = ('03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '15') colors = ('03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '15')
hash_output = hash(s.encode()) hash_output = hash(s.encode())
num = hash_output % len(colors) num = hash_output % len(colors)
@ -52,15 +55,19 @@ def cb_relay_core(irc, source, command, args):
# Be less floody on startup: don't relay non-PRIVMSGs for the first X seconds after connect. # 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', 20) startup_delay = relay_conf.get('clientbot_startup_delay', 20)
target = args.get('target')
if isinstance(target, str):
# Remove STATUSMSG prefixes (e.g. @#channel) before checking whether target is a channel
target = target.lstrip(''.join(irc.prefixmodes.values()))
# Special case for CTCPs. # Special case for CTCPs.
if real_command == 'MESSAGE': if real_command == 'MESSAGE':
# CTCP action, format accordingly # CTCP action, format accordingly
if (not args.get('is_notice')) and args['text'].startswith('\x01ACTION ') and args['text'].endswith('\x01'): if (not args.get('is_notice')) and args['text'].startswith('\x01ACTION ') and args['text'].endswith('\x01'):
args['text'] = args['text'][8:-1] args['text'] = args['text'][8:-1]
real_command = 'ACTION' real_command = 'ACTION'
elif not irc.is_channel(args['target'].lstrip(''.join(irc.prefixmodes.values()))): elif not irc.is_channel(target):
# Target is a user; handle this accordingly. # Target is a user; handle this accordingly.
if relay_conf.get('allow_clientbot_pms'): if relay_conf.get('allow_clientbot_pms'):
real_command = 'PNOTICE' if args.get('is_notice') else 'PM' real_command = 'PNOTICE' if args.get('is_notice') else 'PM'
@ -80,7 +87,7 @@ def cb_relay_core(irc, source, command, args):
# Try to fetch the format for the given command from the relay:clientbot_styles:$command # 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 # key, falling back to one defined in default_styles above, and then nothing if not found
# there. # there.
text_template = irc.get_service_option('relay', 'clientbot_styles', {}).get( text_template = irc.get_service_options('relay', 'clientbot_styles', dict).get(
real_command, default_styles.get(real_command, '')) real_command, default_styles.get(real_command, ''))
text_template = string.Template(text_template) text_template = string.Template(text_template)
@ -114,7 +121,7 @@ def cb_relay_core(irc, source, command, args):
# Figure out where the message is destined to. # Figure out where the message is destined to.
stripped_target = target = args.get('channel') or args.get('target') stripped_target = target = args.get('channel') or args.get('target')
if target is not None: if isinstance(target, str):
# HACK: cheap fix to prevent @#channel messages from interpreted as non-channel specific # HACK: cheap fix to prevent @#channel messages from interpreted as non-channel specific
stripped_target = target.lstrip(''.join(irc.prefixmodes.values())) stripped_target = target.lstrip(''.join(irc.prefixmodes.values()))
@ -139,7 +146,7 @@ def cb_relay_core(irc, source, command, args):
try: try:
identhost = irc.get_hostmask(source).split('!')[-1] identhost = irc.get_hostmask(source).split('!')[-1]
except KeyError: # User got removed due to quit except KeyError: # User got removed due to quit
identhost = '%s@%s' % (args['olduser'].ident, args['olduser'].host) identhost = '%s@%s' % (args['userdata'].ident, args['userdata'].host)
# This is specifically spaced so that ident@host is only shown for users that have # This is specifically spaced so that ident@host is only shown for users that have
# one, and not servers. # one, and not servers.
identhost = ' (%s)' % identhost identhost = ' (%s)' % identhost
@ -219,11 +226,12 @@ utils.add_hook(cb_relay_core, 'RELAY_RAW_MODE')
@utils.add_cmd @utils.add_cmd
def rpm(irc, source, args): def rpm(irc, source, args):
"""<target> <text> """<target nick/UID> <text>
Sends PMs to users over the relay, if Clientbot PMs are enabled. Sends PMs to users over Relay, if Clientbot PMs are enabled.
If the target nick has spaces in it, you may quote the nick as "nick".
""" """
args = shlex.split(' '.join(args)) # HACK: use shlex.split so that quotes are preserved
try: try:
target = args[0] target = args[0]
text = ' '.join(args[1:]) text = ' '.join(args[1:])
@ -246,15 +254,21 @@ def rpm(irc, source, args):
'administratively disabled.') 'administratively disabled.')
return return
uid = irc.nick_to_uid(target) if target in irc.users:
if not uid: uids = [target]
else:
uids = irc.nick_to_uid(target, multi=True, filterfunc=lambda u: relay.is_relay_client(irc, u))
if not uids:
irc.error('Unknown user %s.' % target) irc.error('Unknown user %s.' % target)
return return
elif not relay.is_relay_client(irc, uid): elif len(uids) > 1:
irc.error('%s is not a relay user.' % target) targets = ['\x02%s\x02: %s @ %s' % (uid, irc.get_hostmask(uid), irc.users[uid].remote[0]) for uid in uids]
irc.error('Please select the target you want to PM: %s' % (', '.join(targets)))
return return
else: else:
assert not irc.is_internal_client(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. # Send the message through relay by faking a hook for its handler.
relay.handle_messages(irc, source, 'RELAY_CLIENTBOT_PRIVMSG', {'target': uid, 'text': text}) relay.handle_messages(irc, source, 'RELAY_CLIENTBOT_PRIVMSG', {'target': uids[0], 'text': text})
irc.reply('Message sent.') irc.reply('Message sent.')

View File

@ -1,11 +1,11 @@
# servermaps.py: Maps out connected IRC servers. # servermaps.py: Maps out connected IRC servers.
from pylinkirc import utils, world
from pylinkirc.log import log
from pylinkirc.coremods import permissions
import collections import collections
from pylinkirc import utils, world
from pylinkirc.coremods import permissions
from pylinkirc.log import log
DEFAULT_PERMISSIONS = {"$ircop": ['servermaps.localmap']} DEFAULT_PERMISSIONS = {"$ircop": ['servermaps.localmap']}
def main(irc=None): def main(irc=None):

View File

@ -1,16 +1,27 @@
# servprotect.py: Protects against KILL and nick collision floods # servprotect.py: Protects against KILL and nick collision floods
from expiringdict import ExpiringDict
from pylinkirc import utils, conf import threading
from pylinkirc import conf, utils
from pylinkirc.log import log from pylinkirc.log import log
try:
from cachetools import TTLCache
except ImportError:
log.warning('servprotect: expiringdict support is deprecated as of PyLink 3.0; consider installing cachetools instead')
from expiringdict import ExpiringDict as TTLCache
# check for definitions # check for definitions
servprotect_conf = conf.conf.get('servprotect', {}) servprotect_conf = conf.conf.get('servprotect', {})
length = servprotect_conf.get('length', 10) length = servprotect_conf.get('length', 10)
age = servprotect_conf.get('age', 10) age = servprotect_conf.get('age', 10)
savecache = ExpiringDict(max_len=length, max_age_seconds=age) def _new_cache_dict():
killcache = ExpiringDict(max_len=length, max_age_seconds=age) return TTLCache(length, age)
savecache = _new_cache_dict()
killcache = _new_cache_dict()
lock = threading.Lock()
def handle_kill(irc, numeric, command, args): def handle_kill(irc, numeric, command, args):
""" """
@ -19,6 +30,7 @@ def handle_kill(irc, numeric, command, args):
""" """
if (args['userdata'] and irc.is_internal_server(args['userdata'].server)) or irc.is_internal_client(args['target']): if (args['userdata'] and irc.is_internal_server(args['userdata'].server)) or irc.is_internal_client(args['target']):
with lock:
if killcache.setdefault(irc.name, 1) >= length: if killcache.setdefault(irc.name, 1) >= length:
log.error('(%s) servprotect: Too many kills received, aborting!', irc.name) log.error('(%s) servprotect: Too many kills received, aborting!', irc.name)
irc.disconnect() irc.disconnect()
@ -34,6 +46,7 @@ def handle_save(irc, numeric, command, args):
automatically disconnects from the network. automatically disconnects from the network.
""" """
if irc.is_internal_client(args['target']): if irc.is_internal_client(args['target']):
with lock:
if savecache.setdefault(irc.name, 0) >= length: if savecache.setdefault(irc.name, 0) >= length:
log.error('(%s) servprotect: Too many nick collisions, aborting!', irc.name) log.error('(%s) servprotect: Too many nick collisions, aborting!', irc.name)
irc.disconnect() irc.disconnect()

View File

@ -1,12 +1,13 @@
""" """
stats.py: Simple statistics for PyLink IRC Services. stats.py: Simple statistics for PyLink IRC Services.
""" """
import time
import datetime import datetime
import time
from pylinkirc import utils, world, conf from pylinkirc import conf, utils, world
from pylinkirc.log import log
from pylinkirc.coremods import permissions from pylinkirc.coremods import permissions
from pylinkirc.log import log
def timediff(before, now): def timediff(before, now):
""" """
@ -112,13 +113,11 @@ def handle_stats(irc, source, command, args):
# 243/RPL_STATSOLINE: "O <hostmask> * <nick> [:<info>]" # 243/RPL_STATSOLINE: "O <hostmask> * <nick> [:<info>]"
# New style accounts only! # New style accounts only!
for accountname, accountdata in conf.conf['login'].get('accounts', {}).items(): for accountname, accountdata in conf.conf['login'].get('accounts', {}).items():
_num(243, "O %s * %s :network_filter:%s require_oper:%s" % networks = accountdata.get('networks', [])
(' '.join(accountdata.get('hosts', [])) or '*@*', if irc.name in networks or not networks:
accountname, hosts = ' '.join(accountdata.get('hosts', ['*@*']))
','.join(accountdata.get('networks', [])) or '*', needoper = 'needoper' if accountdata.get('require_oper') else ''
bool(accountdata.get('require_oper')) _num(243, "O %s * %s :%s" % (hosts, accountname, needoper))
)
)
elif stats_type == 'u': elif stats_type == 'u':
# 242/RPL_STATSUPTIME: ":Server Up <days> days <hours>:<minutes>:<seconds>" # 242/RPL_STATSUPTIME: ":Server Up <days> days <hours>:<minutes>:<seconds>"

View File

@ -1 +1,2 @@
# Stub so that pylinkirc.protocols is a module # Abstract modules containing shared protocol code; modules higher in the hierarchy go first
common_modules = ['ircs2s_common', 'ts6_common']

View File

@ -6,14 +6,17 @@ clientbot.py: Clientbot (regular IRC bot) protocol module for PyLink.
# that a regular server would have (e.g. spawning virtual users for things like Relay). Somehow it # that a regular server would have (e.g. spawning virtual users for things like Relay). Somehow it
# works on most networks though! # works on most networks though!
import time
import threading
import base64 import base64
import string
import threading
import time
from pylinkirc import utils, conf from pylinkirc import utils, world
from pylinkirc.classes import *
from pylinkirc.log import log from pylinkirc.log import log
from pylinkirc.protocols.ircs2s_common import * from pylinkirc.protocols.ircs2s_common import *
from pylinkirc.classes import *
__all__ = ['ClientbotBaseProtocol', 'ClientbotWrapperProtocol']
FALLBACK_REALNAME = 'PyLink Relay Mirror Client' FALLBACK_REALNAME = 'PyLink Relay Mirror Client'
@ -21,18 +24,198 @@ FALLBACK_REALNAME = 'PyLink Relay Mirror Client'
IRCV3_CAPABILITIES = {'multi-prefix', 'sasl', 'away-notify', 'userhost-in-names', 'chghost', 'account-notify', IRCV3_CAPABILITIES = {'multi-prefix', 'sasl', 'away-notify', 'userhost-in-names', 'chghost', 'account-notify',
'account-tag', 'extended-join'} 'account-tag', 'extended-join'}
class ClientbotWrapperProtocol(IRCCommonProtocol): class ClientbotBaseProtocol(PyLinkNetworkCoreWithUtils):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.protocol_caps = {'visible-state-only', 'slash-in-nicks', 'slash-in-hosts', 'underscore-in-hosts', self.protocol_caps |= {'visible-state-only', 'slash-in-nicks', 'slash-in-hosts', 'underscore-in-hosts',
'ssl-should-verify'} 'freeform-nicks'}
self.has_eob = False
# Remove conf key checks for those not needed for Clientbot. # Remove conf key checks for those not needed for Clientbot.
self.conf_keys -= {'recvpass', 'sendpass', 'sid', 'sidrange', 'hostname'} self.conf_keys -= {'recvpass', 'sendpass', 'sid', 'sidrange', 'hostname'}
def _get_UID(self, nick, ident=None, host=None, spawn_new=False):
"""
Fetches the UID for the given nick, creating one if it does not already exist and spawn_new is True.
To prevent message spoofing, this will only return an external (non-PyLink) client or the PyLink bot itself.
"""
#log.debug('(%s) _get_UID: searching for nick %s', self.name, nick, stack_info=True)
idsource = self.nick_to_uid(nick, filterfunc=lambda uid: uid == self.pseudoclient.uid or not self.is_internal_client(uid))
if idsource is None and spawn_new:
# If this sender doesn't already exist, spawn a new client.
idsource = self.spawn_client(nick, ident or 'unknown', host or 'unknown',
server=self.uplink, realname=FALLBACK_REALNAME).uid
return idsource or nick # Return input if missing per upstream spec
def away(self, source, text):
"""STUB: sets away messages for internal clients."""
log.debug('(%s) away: target is %s, internal client? %s', self.name, source, self.is_internal_client(source))
if self.users[source].away != text:
if not self.is_internal_client(source):
log.debug('(%s) away: sending AWAY hook from %s with text %r', self.name, source, text)
self.call_hooks([source, 'AWAY', {'text': text}])
self.users[source].away = text
def join(self, client, channel):
"""STUB: sends a virtual join (CLIENTBOT_JOIN) from the client to channel."""
self._channels[channel].users.add(client)
self.users[client].channels.add(channel)
if self.pseudoclient and client != self.pseudoclient:
log.debug('(%s) join: faking JOIN of client %s/%s to %s', self.name, client,
self.get_friendly_name(client), channel)
self.call_hooks([client, 'CLIENTBOT_JOIN', {'channel': channel}])
def kick(self, source, channel, target, reason=''):
"""STUB: rejoins users on kick attempts, for server transports where kicking users from channels is not implemented."""
if not self.is_internal_client(target):
log.info("(%s) Rejoining user %s to %s since kicks are not supported here.", self.name, target, channel)
self.join(target, channel)
self.call_hooks([None, 'JOIN', {'channel': channel, 'users': [target], 'modes': []}])
elif channel in self.channels:
self.channels[channel].remove_user(target)
self.users[target].channels.discard(channel)
self.call_hooks([source, 'CLIENTBOT_KICK', {'channel': channel, 'target': target, 'text': reason}])
else:
log.warning('(%s) Possible desync? Tried to kick() on non-existent channel %s', self.name, channel)
def message(self, source, target, text, notice=False):
"""STUB: Sends messages to the target."""
if self.pseudoclient and self.pseudoclient.uid != source:
self.call_hooks([source, 'CLIENTBOT_MESSAGE', {'target': target, 'is_notice': notice, 'text': text}])
def nick(self, source, newnick):
"""STUB: sends a virtual nick change (CLIENTBOT_NICK)."""
assert source, "No source given?"
self.call_hooks([source, 'CLIENTBOT_NICK', {'newnick': newnick}])
self.users[source].nick = newnick
def notice(self, source, target, text):
"""Sends notices to the target."""
# Wrap around message(), which does all the text formatting for us.
self.message(source, target, text, notice=True)
def sjoin(self, server, channel, users, ts=None, modes=set()):
"""STUB: bursts joins from a server."""
# This stub only updates the state internally with the users given. modes and TS are currently ignored.
puids = {u[-1] for u in users}
for user in puids:
self.users[user].channels.add(channel)
self._channels[channel].users |= puids
nicks = {self.get_friendly_name(u) for u in puids}
self.call_hooks([server, 'CLIENTBOT_SJOIN', {'channel': channel, 'nicks': nicks}])
# Note: clientbot clients are initialized with umode +i by default
def spawn_client(self, nick, ident='unknown', host='unknown.host', realhost=None, modes={('i', None)},
server=None, ip='0.0.0.0', realname='', ts=None, opertype=None,
manipulatable=False):
"""
STUB: Pretends to spawn a new client with a subset of the given options.
"""
server = server or self.sid
uid = self.uidgen.next_uid(prefix=nick)
ts = ts or int(time.time())
log.debug('(%s) spawn_client stub called, saving nick %s as PUID %s', self.name, nick, uid)
u = self.users[uid] = User(self, nick, ts, uid, server, ident=ident, host=host, realname=realname,
manipulatable=manipulatable, realhost=realhost, ip=ip)
self.servers[server].users.add(uid)
self.apply_modes(uid, modes)
return u
def spawn_server(self, name, sid=None, uplink=None, desc=None, internal=True):
"""
STUB: Pretends to spawn a new server with a subset of the given options.
"""
if internal:
# Use a custom pseudo-SID format for internal servers to prevent any server name clashes
sid = self.sidgen.next_sid(prefix=name)
else:
# For others servers, just use the server name as the SID.
sid = name
self.servers[sid] = Server(self, uplink, name, internal=internal)
return sid
def squit(self, source, target, text):
"""STUB: SQUITs a server."""
# What this actually does is just handle the SQUIT internally: i.e.
# Removing pseudoclients and pseudoservers.
squit_data = self._squit(source, 'CLIENTBOT_VIRTUAL_SQUIT', [target, text])
if squit_data and squit_data.get('nicks'):
self.call_hooks([source, 'CLIENTBOT_SQUIT', squit_data])
def part(self, source, channel, reason=''):
"""STUB: Parts a user from a channel."""
if self.pseudoclient and source == self.pseudoclient.uid:
raise NotImplementedError("Explicitly leaving channels is not supported here.")
self._channels[channel].remove_user(source)
self.users[source].channels.discard(channel)
self.call_hooks([source, 'CLIENTBOT_PART', {'channel': channel, 'text': reason}])
def quit(self, source, reason):
"""STUB: Quits a client."""
userdata = self._remove_client(source)
self.call_hooks([source, 'CLIENTBOT_QUIT', {'text': reason, 'userdata': userdata}])
def _stub(self, *args):
"""Stub outgoing command function (does nothing)."""
return
# Note: invite() and mode() are implemented in ClientbotWrapperProtocol below
invite = mode = topic = topic_burst = _stub # XXX: incomplete
def _stub_raise(self, *args):
"""Stub outgoing command function (raises an error)."""
raise NotImplementedError("Not supported on Clientbot")
kill = knock = numeric = _stub_raise
def update_client(self, target, field, text):
"""Updates the known ident, host, or realname of a client."""
# Note: unlike other protocol modules, this function is also called as a helper to
# update data for external clients.
# Following this, we only want to send hook payloads if the target is an external client.
if target not in self.users:
log.warning("(%s) Unknown target %s for update_client()", self.name, target)
return
u = self.users[target]
if field == 'IDENT' and u.ident != text:
u.ident = text
if not self.is_internal_client(target):
self.call_hooks([self.sid, 'CHGIDENT',
{'target': target, 'newident': text}])
elif field == 'HOST' and u.host != text:
u.host = text
if not self.is_internal_client(target):
self.call_hooks([self.sid, 'CHGHOST',
{'target': target, 'newhost': text}])
elif field in ('REALNAME', 'GECOS') and u.realname != text:
u.realname = text
if not self.is_internal_client(target):
self.call_hooks([self.sid, 'CHGNAME',
{'target': target, 'newgecos': text}])
else:
return # Nothing changed
class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.protocol_caps |= {'ssl-should-verify'}
self.has_eob = False
# This is just a fallback. Actual casemapping is fetched by handle_005() # This is just a fallback. Actual casemapping is fetched by handle_005()
self.casemapping = 'ascii' self.casemapping = 'ascii'
@ -109,52 +292,6 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
ident=ident, realname=realname, host=self.hostname()) ident=ident, realname=realname, host=self.hostname())
self.users[self.pseudoclient.uid] = self.pseudoclient self.users[self.pseudoclient.uid] = self.pseudoclient
# Note: clientbot clients are initialized with umode +i by default
def spawn_client(self, nick, ident='unknown', host='unknown.host', realhost=None, modes={('i', None)},
server=None, ip='0.0.0.0', realname='', ts=None, opertype=None,
manipulatable=False):
"""
STUB: Pretends to spawn a new client with a subset of the given options.
"""
server = server or self.sid
uid = self.uidgen.next_uid(prefix=nick)
ts = ts or int(time.time())
log.debug('(%s) spawn_client stub called, saving nick %s as PUID %s', self.name, nick, uid)
u = self.users[uid] = User(self, nick, ts, uid, server, ident=ident, host=host, realname=realname,
manipulatable=manipulatable, realhost=realhost, ip=ip)
self.servers[server].users.add(uid)
self.apply_modes(uid, modes)
return u
def spawn_server(self, name, sid=None, uplink=None, desc=None, internal=True):
"""
STUB: Pretends to spawn a new server with a subset of the given options.
"""
if internal:
# Use a custom pseudo-SID format for internal servers to prevent any server name clashes
sid = self.sidgen.next_sid(prefix=name)
else:
# For others servers, just use the server name as the SID.
sid = name
self.servers[sid] = Server(self, uplink, name, internal=internal)
return sid
def away(self, source, text):
"""STUB: sets away messages for clients internally."""
log.debug('(%s) away: target is %s, internal client? %s', self.name, source, self.is_internal_client(source))
if self.users[source].away != text:
if not self.is_internal_client(source):
log.debug('(%s) away: sending AWAY hook from %s with text %r', self.name, source, text)
self.call_hooks([source, 'AWAY', {'text': text}])
self.users[source].away = text
def invite(self, client, target, channel): def invite(self, client, target, channel):
"""Invites a user to a channel.""" """Invites a user to a channel."""
@ -169,12 +306,8 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
if self.pseudoclient and client == self.pseudoclient.uid: if self.pseudoclient and client == self.pseudoclient.uid:
self.send('JOIN %s' % channel) self.send('JOIN %s' % channel)
else: else:
self._channels[channel].users.add(client) # Pass on a virtual JOIN as a hook
self.users[client].channels.add(channel) super().join(client, channel)
log.debug('(%s) join: faking JOIN of client %s/%s to %s', self.name, client,
self.get_friendly_name(client), channel)
self.call_hooks([client, 'CLIENTBOT_JOIN', {'channel': channel}])
def kick(self, source, channel, target, reason=''): def kick(self, source, channel, target, reason=''):
"""Sends channel kicks.""" """Sends channel kicks."""
@ -182,7 +315,7 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
log.debug('(%s) kick: checking if target %s (nick: %s) is an internal client? %s', log.debug('(%s) kick: checking if target %s (nick: %s) is an internal client? %s',
self.name, target, self.get_friendly_name(target), self.name, target, self.get_friendly_name(target),
self.is_internal_client(target)) self.is_internal_client(target))
if self.is_internal_client(target): if self.is_internal_client(target) and (self.pseudoclient and source != self.pseudoclient.uid):
# Target was one of our virtual clients. Just remove them from the state. # Target was one of our virtual clients. Just remove them from the state.
self.handle_part(target, 'KICK', [channel, reason]) self.handle_part(target, 'KICK', [channel, reason])
@ -215,7 +348,8 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
if self.pseudoclient and self.pseudoclient.uid == source: if self.pseudoclient and self.pseudoclient.uid == source:
self.send('%s %s :%s' % (command, self._expandPUID(target), text)) self.send('%s %s :%s' % (command, self._expandPUID(target), text))
else: else:
self.call_hooks([source, 'CLIENTBOT_MESSAGE', {'target': target, 'is_notice': notice, 'text': text}]) # Pass the message on as a hook
super().message(source, target, text, notice=notice)
def mode(self, source, channel, modes, ts=None): def mode(self, source, channel, modes, ts=None):
"""Sends channel MODE changes.""" """Sends channel MODE changes."""
@ -244,7 +378,7 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
log.debug('(%s) mode: filtered modes for %s: %s', self.name, channel, extmodes) log.debug('(%s) mode: filtered modes for %s: %s', self.name, channel, extmodes)
if extmodes: if extmodes:
bufsize = self.S2S_BUFSIZE - len(':%s MODE %s ' % (self.get_hostmask(self.pseudoclient.uid), channel)) bufsize = self.S2S_BUFSIZE - len(':%s MODE %s ' % (self.get_hostmask(self.pseudoclient.uid), channel))
for msg in self.wrap_modes(extmodes, bufsize, max_modes_per_msg=int(self._caps.get('MODES', 0))): for msg in self.wrap_modes(extmodes, bufsize, max_modes_per_msg=int(self._caps.get('MODES') or 0)):
self.send('MODE %s %s' % (channel, msg)) self.send('MODE %s %s' % (channel, msg))
# Don't update the state here: the IRCd sill respond with a MODE reply if successful. # Don't update the state here: the IRCd sill respond with a MODE reply if successful.
@ -254,20 +388,7 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
self.send('NICK :%s' % newnick) self.send('NICK :%s' % newnick)
# No state update here: the IRCd will respond with a NICK acknowledgement if the change succeeds. # No state update here: the IRCd will respond with a NICK acknowledgement if the change succeeds.
else: else:
assert source, "No source given?" super().nick(source, newnick)
# Check that the new nick exists and isn't the same client as the sender
# (for changing nick case)
nick_uid = self.nick_to_uid(newnick)
if nick_uid and nick_uid != source:
log.warning('(%s) Blocking attempt from virtual client %s to change nick to %s (nick in use)', self.name, source, newnick)
return
self.call_hooks([source, 'CLIENTBOT_NICK', {'newnick': newnick}])
self.users[source].nick = newnick
def notice(self, source, target, text):
"""Sends notices to the target."""
# Wrap around message(), which does all the text formatting for us.
self.message(source, target, text, notice=True)
def _ping_uplink(self): def _ping_uplink(self):
""" """
@ -280,6 +401,13 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
for channel in self.pseudoclient.channels: for channel in self.pseudoclient.channels:
self._send_who(channel) self._send_who(channel)
# Join persistent channels if always_autorejoin is enabled and there are any we're not in
if self.serverdata.get('always_autorejoin') and self.has_cap('can-manage-bot-channels'):
for channel in world.services['pylink'].get_persistent_channels(self):
if channel not in self.pseudoclient.channels:
log.info('(%s) Attempting to rejoin %s', self.name, channel)
self.join(self.pseudoclient.uid, channel)
def part(self, source, channel, reason=''): def part(self, source, channel, reason=''):
"""STUB: Parts a user from a channel.""" """STUB: Parts a user from a channel."""
self._channels[channel].remove_user(source) self._channels[channel].remove_user(source)
@ -287,15 +415,10 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
# Only parts for the main PyLink client are actually forwarded. Others are ignored. # Only parts for the main PyLink client are actually forwarded. Others are ignored.
if self.pseudoclient and source == self.pseudoclient.uid: if self.pseudoclient and source == self.pseudoclient.uid:
self._channels[channel]._clientbot_part_requested = True
self.send('PART %s :%s' % (channel, reason)) self.send('PART %s :%s' % (channel, reason))
else: else:
self.call_hooks([source, 'CLIENTBOT_PART', {'channel': channel, 'text': reason}]) super().part(source, channel, reason=reason)
def quit(self, source, reason):
"""STUB: Quits a client."""
userdata = self.users[source]
self._remove_client(source)
self.call_hooks([source, 'CLIENTBOT_QUIT', {'text': reason, 'userdata': userdata}])
def sjoin(self, server, channel, users, ts=None, modes=set()): def sjoin(self, server, channel, users, ts=None, modes=set()):
"""STUB: bursts joins from a server.""" """STUB: bursts joins from a server."""
@ -314,99 +437,6 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
nicks = {self.get_friendly_name(u) for u in puids} nicks = {self.get_friendly_name(u) for u in puids}
self.call_hooks([server, 'CLIENTBOT_SJOIN', {'channel': channel, 'nicks': nicks}]) self.call_hooks([server, 'CLIENTBOT_SJOIN', {'channel': channel, 'nicks': nicks}])
def squit(self, source, target, text):
"""STUB: SQUITs a server."""
# What this actually does is just handle the SQUIT internally: i.e.
# Removing pseudoclients and pseudoservers.
squit_data = self._squit(source, 'CLIENTBOT_VIRTUAL_SQUIT', [target, text])
if squit_data and squit_data.get('nicks'):
self.call_hooks([source, 'CLIENTBOT_SQUIT', squit_data])
def _stub(self, *args):
"""Stub outgoing command function (does nothing)."""
return
topic = topic_burst = _stub # XXX: incomplete
def _stub_raise(self, *args):
"""Stub outgoing command function (raises an error)."""
raise NotImplementedError("Not supported on Clientbot")
kill = knock = numeric = _stub_raise
def update_client(self, target, field, text):
"""Updates the known ident, host, or realname of a client."""
if target not in self.users:
log.warning("(%s) Unknown target %s for update_client()", self.name, target)
return
u = self.users[target]
if field == 'IDENT' and u.ident != text:
u.ident = text
if not self.is_internal_client(target):
# We're updating the host of an external client in our state, so send the appropriate
# hook payloads.
self.call_hooks([self.sid, 'CHGIDENT',
{'target': target, 'newident': text}])
elif field == 'HOST' and u.host != text:
u.host = text
if not self.is_internal_client(target):
self.call_hooks([self.sid, 'CHGHOST',
{'target': target, 'newhost': text}])
elif field in ('REALNAME', 'GECOS') and u.realname != text:
u.realname = text
if not self.is_internal_client(target):
self.call_hooks([self.sid, 'CHGNAME',
{'target': target, 'newgecos': text}])
else:
return # Nothing changed
def _get_UID(self, nick, ident=None, host=None):
"""
Fetches the UID for the given nick, creating one if it does not already exist.
Limited (internal) nick collision checking is done here to prevent Clientbot users from
being confused with virtual clients, and vice versa."""
self._check_puid_collision(nick)
idsource = self.nick_to_uid(nick)
if self.is_internal_client(idsource) and self.pseudoclient and idsource != self.pseudoclient.uid:
# We got a message from a client with the same nick as an internal client.
# Fire a virtual nick collision to prevent mixing senders.
log.debug('(%s) Nick-colliding virtual client %s/%s', self.name, idsource, nick)
self.call_hooks([self.sid, 'SAVE', {'target': idsource}])
# Clear the UID for this nick and spawn a new client for the nick that was just freed.
idsource = None
if idsource is None:
# If this sender doesn't already exist, spawn a new client.
idsource = self.spawn_client(nick, ident or 'unknown', host or 'unknown',
server=self.uplink, realname=FALLBACK_REALNAME).uid
return idsource
def parse_message_tags(self, data):
"""
Parses a message with IRC v3.2 message tags, as described at http://ircv3.net/specs/core/message-tags-3.2.html
"""
# Example query:
# @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello
if data[0].startswith('@'):
tagdata = data[0].lstrip('@').split(';')
for idx, tag in enumerate(tagdata):
tag = tag.replace(r'\s', ' ')
tag = tag.replace(r'\\', '\\')
tag = tag.replace(r'\r', '\r')
tag = tag.replace(r'\n', '\n')
tag = tag.replace(r'\:', ';')
tagdata[idx] = tag
results = self.parse_isupport(tagdata, fallback=None)
log.debug('(%s) parsed message tags %s', self.name, results)
return results
return {}
def _set_account_name(self, uid, account): def _set_account_name(self, uid, account):
""" """
Updates the user's account metadata. Updates the user's account metadata.
@ -457,7 +487,7 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
except ValueError: except ValueError:
ident = host = None # Set ident and host as null for now. ident = host = None # Set ident and host as null for now.
nick = sender # Treat the sender prefix we received as a nick. nick = sender # Treat the sender prefix we received as a nick.
idsource = self._get_UID(nick, ident, host) idsource = self._get_UID(nick, ident, host, spawn_new=True)
if idsource in self.users: if idsource in self.users:
# Handle IRCv3.2 account-tag. # Handle IRCv3.2 account-tag.
@ -668,7 +698,7 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
""" """
Handles 353 / RPL_NAMREPLY. Handles 353 / RPL_NAMREPLY.
""" """
# <- :charybdis.midnight.vpn 353 ice = #test :ice @GL # <- :charybdis.midnight.vpn 353 ice = #test :ice @jlu5
# Mark "@"-type channels as secret automatically, per RFC2812. # Mark "@"-type channels as secret automatically, per RFC2812.
channel = args[2] channel = args[2]
@ -680,18 +710,26 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
prefix_to_mode = {v:k for k, v in self.prefixmodes.items()} prefix_to_mode = {v:k for k, v in self.prefixmodes.items()}
prefixes = ''.join(self.prefixmodes.values()) prefixes = ''.join(self.prefixmodes.values())
for name in args[-1].split(): # N.B. only split on spaces because of color in hosts nonsense...
# str.split() by default treats \x1f as whitespace
for name in args[-1].strip().split(' '):
nick = name.lstrip(prefixes) nick = name.lstrip(prefixes)
# Handle userhost-in-names where available. # Handle userhost-in-names where available.
if 'userhost-in-names' in self.ircv3_caps:
nick, ident, host = utils.split_hostmask(nick)
else:
ident = host = None ident = host = None
if 'userhost-in-names' in self.ircv3_caps:
try:
nick, ident, host = utils.split_hostmask(nick)
except ValueError:
log.exception('(%s) Failed to split hostmask %r from /names reply on %s; args=%s', self.name, nick, channel, args)
# If error, leave nick unsplit
if not nick:
continue
# Get the PUID for the given nick. If one doesn't exist, spawn # Get the PUID for the given nick. If one doesn't exist, spawn
# a new virtual user. # a new virtual user.
idsource = self._get_UID(nick, ident=ident, host=host) idsource = self._get_UID(nick, ident=ident, host=host, spawn_new=True)
# Queue these virtual users to be joined if they're not already in the channel, # Queue these virtual users to be joined if they're not already in the channel,
# or we're waiting for a kick acknowledgment for them. # or we're waiting for a kick acknowledgment for them.
@ -719,19 +757,12 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
# Send JOIN hook payloads only for users that we know the ident@host of already. # Send JOIN hook payloads only for users that we know the ident@host of already.
# This is mostly used to resync kicked Clientbot users that can't actually be kicked # This is mostly used to resync kicked Clientbot users that can't actually be kicked
# after a delay. # after a delay.
if names and hasattr(self.irc.channels[channel], '_clientbot_initial_who_received'): if names and hasattr(self.channels[channel], '_clientbot_initial_who_received'):
log.debug('(%s) handle_353: sending JOIN hook because /WHO was already received for %s', log.debug('(%s) handle_353: sending JOIN hook because /WHO was already received for %s',
self.irc.name, channel) self.name, channel)
return {'channel': channel, 'users': names, 'modes': self._channels[channel].modes, return {'channel': channel, 'users': names, 'modes': self._channels[channel].modes,
'parse_as': "JOIN"} 'parse_as': "JOIN"}
def _check_puid_collision(self, nick):
"""
Checks to make sure a nick doesn't clash with a PUID.
"""
if nick in self.users or nick in self.servers:
raise ProtocolError("Got bad nick %s from IRC which clashes with a PUID. Is someone trying to spoof users?" % nick)
def _send_who(self, channel): def _send_who(self, channel):
"""Sends /WHO to a channel, with WHOX args if that is supported.""" """Sends /WHO to a channel, with WHOX args if that is supported."""
# Note: %% = escaped % # Note: %% = escaped %
@ -748,10 +779,10 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
""" """
# parameter count: 0 1 2 3 4 5 6 7(-1) # parameter count: 0 1 2 3 4 5 6 7(-1)
# <- :charybdis.midnight.vpn 352 ice #test ~pylink 127.0.0.1 charybdis.midnight.vpn ice H+ :0 PyLink # <- :charybdis.midnight.vpn 352 ice #test ~pylink 127.0.0.1 charybdis.midnight.vpn ice H+ :0 PyLink
# <- :charybdis.midnight.vpn 352 ice #test ~gl 127.0.0.1 charybdis.midnight.vpn GL H*@ :0 realname # <- :charybdis.midnight.vpn 352 ice #test ~jlu5 127.0.0.1 charybdis.midnight.vpn jlu5 H*@ :0 realname
# with WHO %cuhsnfar (WHOX) - note, hopcount and realname are separate! # with WHO %cuhsnfar (WHOX) - note, hopcount and realname are separate!
# 0 1 2 3 4 5 6 7 8(-1) # 0 1 2 3 4 5 6 7 8(-1)
# <- :charybdis.midnight.vpn 354 ice #test ~gl localhost charybdis.midnight.vpn GL H*@ GL :realname # <- :charybdis.midnight.vpn 354 ice #test ~jlu5 localhost charybdis.midnight.vpn jlu5 H*@ jlu5 :realname
channel = args[1] channel = args[1]
ident = args[2] ident = args[2]
host = args[3] host = args[3]
@ -763,8 +794,7 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
if command == '352': if command == '352':
realname = realname.split(' ', 1)[-1] realname = realname.split(' ', 1)[-1]
self._check_puid_collision(nick) uid = self._get_UID(nick, spawn_new=False)
uid = self.nick_to_uid(nick)
if uid is None: if uid is None:
log.debug("(%s) Ignoring extraneous /WHO info for %s", self.name, nick) log.debug("(%s) Ignoring extraneous /WHO info for %s", self.name, nick)
@ -877,9 +907,9 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
Handles incoming JOINs, as well as JOIN acknowledgements for us. Handles incoming JOINs, as well as JOIN acknowledgements for us.
""" """
# Classic format: # Classic format:
# <- :GL|!~GL@127.0.0.1 JOIN #whatever # <- :jlu5|!~jlu5@127.0.0.1 JOIN #whatever
# With extended-join: # With extended-join:
# <- :GL|!~GL@127.0.0.1 JOIN #whatever accountname :realname # <- :jlu5|!~jlu5@127.0.0.1 JOIN #whatever accountname :realname
channel = args[0] channel = args[0]
self._channels[channel].users.add(source) self._channels[channel].users.add(source)
self.users[source].channels.add(channel) self.users[source].channels.add(channel)
@ -910,9 +940,9 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
""" """
Handles incoming KICKs. Handles incoming KICKs.
""" """
# <- :GL!~gl@127.0.0.1 KICK #whatever GL| :xd # <- :jlu5!~jlu5@127.0.0.1 KICK #whatever jlu5| :xd
channel = args[0] channel = args[0]
target = self.nick_to_uid(args[1]) target = self._get_UID(args[1], spawn_new=False)
try: try:
reason = args[2] reason = args[2]
@ -937,19 +967,21 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
except KeyError: except KeyError:
pass pass
if (not self.is_internal_client(source)) and not self.is_internal_server(source): # Don't repeat hooks if we're the kicker, unless we're also the target.
# Don't repeat hooks if we're the kicker. if self.is_internal_client(source) or self.is_internal_server(source):
if self.pseudoclient and target != self.pseudoclient.uid:
return
return {'channel': channel, 'target': target, 'text': reason} return {'channel': channel, 'target': target, 'text': reason}
def handle_mode(self, source, command, args): def handle_mode(self, source, command, args):
"""Handles MODE changes.""" """Handles MODE changes."""
# <- :GL!~gl@127.0.0.1 MODE #dev +v ice # <- :jlu5!~jlu5@127.0.0.1 MODE #dev +v ice
# <- :ice MODE ice :+Zi # <- :ice MODE ice :+Zi
target = args[0] target = args[0]
if self.is_channel(target): if self.is_channel(target):
oldobj = self._channels[target].deepcopy() oldobj = self._channels[target].deepcopy()
else: else:
target = self.nick_to_uid(target) target = self._get_UID(target, spawn_new=False)
oldobj = None oldobj = None
modes = args[1:] modes = args[1:]
changedmodes = self.parse_modes(target, modes) changedmodes = self.parse_modes(target, modes)
@ -968,8 +1000,8 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
def handle_324(self, source, command, args): def handle_324(self, source, command, args):
"""Handles MODE announcements via RPL_CHANNELMODEIS (i.e. the response to /mode #channel)""" """Handles MODE announcements via RPL_CHANNELMODEIS (i.e. the response to /mode #channel)"""
# -> MODE #test # -> MODE #test
# <- :midnight.vpn 324 GL #test +nt # <- :midnight.vpn 324 jlu5 #test +nt
# <- :midnight.vpn 329 GL #test 1491773459 # <- :midnight.vpn 329 jlu5 #test 1491773459
channel = args[1] channel = args[1]
modes = args[2:] modes = args[2:]
log.debug('(%s) Got RPL_CHANNELMODEIS (324) modes %s for %s', self.name, modes, channel) log.debug('(%s) Got RPL_CHANNELMODEIS (324) modes %s for %s', self.name, modes, channel)
@ -1005,7 +1037,7 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
def handle_nick(self, source, command, args): def handle_nick(self, source, command, args):
"""Handles NICK changes.""" """Handles NICK changes."""
# <- :GL|!~GL@127.0.0.1 NICK :GL_ # <- :jlu5|!~jlu5@127.0.0.1 NICK :jlu5_
newnick = args[0] newnick = args[0]
if not self.connected.is_set(): if not self.connected.is_set():
@ -1020,8 +1052,6 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
oldnick = self.users[source].nick oldnick = self.users[source].nick
# Check for any nick collisions with existing virtual clients.
self._check_nick_collision(newnick)
self.users[source].nick = newnick self.users[source].nick = newnick
return {'newnick': newnick, 'oldnick': oldnick} return {'newnick': newnick, 'oldnick': oldnick}
@ -1030,7 +1060,7 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
""" """
Handles incoming PARTs. Handles incoming PARTs.
""" """
# <- :GL|!~GL@127.0.0.1 PART #whatever # <- :jlu5|!~jlu5@127.0.0.1 PART #whatever
channels = args[0].split(',') channels = args[0].split(',')
try: try:
reason = args[1] reason = args[1]
@ -1041,7 +1071,21 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
self._channels[channel].remove_user(source) self._channels[channel].remove_user(source)
self.users[source].channels -= set(channels) self.users[source].channels -= set(channels)
return {'channels': channels, 'text': reason} # Only send the PART hook for parts not initiated by us - this is for consistency with other
# protocols
notify_channels = []
for channel in channels:
is_part_requested = getattr(self._channels[channel], '_clientbot_part_requested', False)
if is_part_requested:
log.debug('(%s) clientbot.handle_part: not forwarding part hook for %s since we requested it', self.name, channel)
self._channels[channel]._clientbot_part_requested = False
continue
else:
notify_channels.append(channel)
if notify_channels:
log.debug('(%s) clientbot.handle_part: returning part hook for %s (original: %s)', self.name, notify_channels, channels)
return {'channels': notify_channels, 'text': reason}
def handle_ping(self, source, command, args): def handle_ping(self, source, command, args):
""" """
@ -1061,7 +1105,7 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
real_target = target.lstrip(''.join(self.prefixmodes.values())) real_target = target.lstrip(''.join(self.prefixmodes.values()))
if not self.is_channel(real_target): if not self.is_channel(real_target):
target = self.nick_to_uid(target) target = self._get_UID(target, spawn_new=False)
if target: if target:
return {'target': target, 'text': args[1]} return {'target': target, 'text': args[1]}
@ -1072,9 +1116,13 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
if self.pseudoclient and source == self.pseudoclient.uid: if self.pseudoclient and source == self.pseudoclient.uid:
# Someone faked a quit from us? We should abort. # Someone faked a quit from us? We should abort.
raise ProtocolError("Received QUIT from uplink (%s)" % args[0]) raise ProtocolError("Received QUIT from uplink (%s)" % args[0])
elif source not in self.users:
log.debug('(%s) Ignoring QUIT on non-existent user %s', self.name, source)
return
userdata = self.users[source]
self.quit(source, args[0]) self.quit(source, args[0])
return {'text': args[0]} return {'text': args[0], 'userdata': userdata}
def handle_404(self, source, command, args): def handle_404(self, source, command, args):
""" """
@ -1109,7 +1157,7 @@ class ClientbotWrapperProtocol(IRCCommonProtocol):
channel = args[1] channel = args[1]
target = args[2] target = args[2]
if channel not in self.channels: if channel not in self.channels:
log.warning('(%s) got ban mode +%s %s on unknown channel %s?', self.name, banmode, target) log.warning('(%s) got ban mode +%s %s on unknown channel %s?', self.name, banmode, target, channel)
else: else:
# Just apply the mode; we send out a mode hook only when the corresponding ban list has finished sending. # Just apply the mode; we send out a mode hook only when the corresponding ban list has finished sending.
self.apply_modes(channel, [('+%s' % banmode, target)]) self.apply_modes(channel, [('+%s' % banmode, target)])

View File

@ -4,10 +4,13 @@ hybrid.py: IRCD-Hybrid protocol module for PyLink.
import time import time
from pylinkirc import utils, conf from pylinkirc import conf
from pylinkirc.log import log
from pylinkirc.classes import * from pylinkirc.classes import *
from pylinkirc.protocols.ts6 import * from pylinkirc.log import log
from pylinkirc.protocols.ts6 import TS6Protocol
__all__ = ['HybridProtocol']
# This protocol module inherits from the TS6 protocol. # This protocol module inherits from the TS6 protocol.
class HybridProtocol(TS6Protocol): class HybridProtocol(TS6Protocol):
@ -128,8 +131,8 @@ class HybridProtocol(TS6Protocol):
"""Updates the ident, host, or realname of a PyLink client.""" """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 # https://github.com/ircd-hybrid/ircd-hybrid/blob/58323b8/modules/m_svsmode.c#L40-L103
# parv[0] = command # parv[0] = command
# parv[1] = nickname <-- UID works too -GLolol # parv[1] = nickname <-- UID works too -jlu5
# parv[2] = TS <-- Of the user, not the current time. -GLolol # parv[2] = TS <-- Of the user, not the current time. -jlu5
# parv[3] = mode # parv[3] = mode
# parv[4] = optional argument (services account, vhost) # parv[4] = optional argument (services account, vhost)
field = field.upper() field = field.upper()
@ -139,11 +142,17 @@ class HybridProtocol(TS6Protocol):
if field == 'HOST': if field == 'HOST':
self.users[target].host = text self.users[target].host = text
# On Hybrid, it appears that host changing is actually just forcing umode # On Hybrid, it appears that host changing is actually just forcing umode
# "+x <hostname>" on the target. -GLolol # "+x <hostname>" on the target. -jlu5
self._send_with_prefix(self.sid, 'SVSMODE %s %s +x %s' % (target, ts, text)) self._send_with_prefix(self.sid, 'SVSMODE %s %s +x %s' % (target, ts, text))
else: else:
raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field) raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field)
def oper_notice(self, source, text):
"""
Send a message to all opers.
"""
self._send_with_prefix(source, 'GLOBOPS :%s' % text)
def set_server_ban(self, source, duration, user='*', host='*', reason='User banned'): def set_server_ban(self, source, duration, user='*', host='*', reason='User banned'):
""" """
Sets a server ban. Sets a server ban.
@ -208,11 +217,14 @@ class HybridProtocol(TS6Protocol):
# Call the OPERED UP hook if +o is being added to the mode list. # Call the OPERED UP hook if +o is being added to the mode list.
self._check_oper_status_change(uid, parsedmodes) self._check_oper_status_change(uid, parsedmodes)
# Track SSL/TLS status
has_ssl = self.users[uid].ssl = ('+S', None) in parsedmodes
# Set the account name if present # Set the account name if present
if account: if account:
self.call_hooks([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} return {'uid': uid, 'ts': ts, 'nick': nick, 'realname': realname, 'host': host, 'ident': ident, 'ip': ip, 'secure': has_ssl}
def handle_tburst(self, numeric, command, args): def handle_tburst(self, numeric, command, args):
"""Handles incoming topic burst (TBURST) commands.""" """Handles incoming topic burst (TBURST) commands."""
@ -252,7 +264,7 @@ class HybridProtocol(TS6Protocol):
# Login sequence (tested with Anope 2.0.4-git): # Login sequence (tested with Anope 2.0.4-git):
# A mode change +d accountname is used to propagate logins, # A mode change +d accountname is used to propagate logins,
# before setting umode +r on the target. # before setting umode +r on the target.
# <- :5ANAAAAAG SVSMODE 5HYAAAAAA 1460175209 +d GL # <- :5ANAAAAAG SVSMODE 5HYAAAAAA 1460175209 +d jlu5
# <- :5ANAAAAAG SVSMODE 5HYAAAAAA 1460175209 +r # <- :5ANAAAAAG SVSMODE 5HYAAAAAA 1460175209 +r
# Logout sequence: # Logout sequence:

View File

@ -1,25 +1,32 @@
""" """
inspircd.py: InspIRCd 2.x protocol module for PyLink. inspircd.py: InspIRCd 2.0, 3.x protocol module for PyLink.
""" """
import time
import threading import threading
import time
from pylinkirc import utils, conf from pylinkirc import conf
from pylinkirc.classes import * from pylinkirc.classes import *
from pylinkirc.log import log from pylinkirc.log import log
from pylinkirc.protocols.ts6_common import * from pylinkirc.protocols.ts6_common import TS6BaseProtocol
__all__ = ['InspIRCdProtocol']
class InspIRCdProtocol(TS6BaseProtocol): class InspIRCdProtocol(TS6BaseProtocol):
S2S_BUFSIZE = 0 # InspIRCd allows infinitely long S2S messages, so set bufsize to infinite S2S_BUFSIZE = 0 # InspIRCd allows infinitely long S2S messages, so set bufsize to infinite
SUPPORTED_IRCDS = ['insp20', 'insp3']
DEFAULT_IRCD = SUPPORTED_IRCDS[1]
MAX_PROTO_VER = 1205 # anything above this warns (not officially supported)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.protocol_caps |= {'slash-in-nicks', 'slash-in-hosts', 'underscore-in-hosts'} self.protocol_caps |= {'slash-in-nicks', 'slash-in-hosts', 'underscore-in-hosts'}
# Set our case mapping (rfc1459 maps "\" and "|" together, for example). # This is only the default value - on InspIRCd 3 it will be negotiated on connect in CAPAB CAPABILITIES
self.casemapping = 'rfc1459' self.casemapping = 'rfc1459'
# Raw commands sent from servers vary from protocol to protocol. Here, we map # Raw commands sent from servers vary from protocol to protocol. Here, we map
@ -28,11 +35,19 @@ class InspIRCdProtocol(TS6BaseProtocol):
self.hook_map = {'FJOIN': 'JOIN', 'RSQUIT': 'SQUIT', 'FMODE': 'MODE', self.hook_map = {'FJOIN': 'JOIN', 'RSQUIT': 'SQUIT', 'FMODE': 'MODE',
'FTOPIC': 'TOPIC', 'OPERTYPE': 'MODE', 'FHOST': 'CHGHOST', 'FTOPIC': 'TOPIC', 'OPERTYPE': 'MODE', 'FHOST': 'CHGHOST',
'FIDENT': 'CHGIDENT', 'FNAME': 'CHGNAME', 'SVSTOPIC': 'TOPIC', 'FIDENT': 'CHGIDENT', 'FNAME': 'CHGNAME', 'SVSTOPIC': 'TOPIC',
'SAKICK': 'KICK'} 'SAKICK': 'KICK', 'IJOIN': 'JOIN'}
self.min_proto_ver = 1202 ircd_target = self.serverdata.get('target_version', self.DEFAULT_IRCD).lower()
if ircd_target == 'insp20':
self.proto_ver = 1202 self.proto_ver = 1202
self.max_proto_ver = 1202 # Anything above should warn (not officially supported) elif ircd_target == 'insp3':
self.proto_ver = 1205
else:
raise ProtocolError("Unsupported target_version %r: supported values include %s" % (ircd_target, self.SUPPORTED_IRCDS))
log.debug('(%s) inspircd: using protocol version %s for target_version %r', self.name, self.proto_ver, ircd_target)
# Track prefix mode levels on InspIRCd 3
self._prefix_levels = {}
# Track the modules supported by the uplink. # Track the modules supported by the uplink.
self._modsupport = set() self._modsupport = set()
@ -187,10 +202,8 @@ class InspIRCdProtocol(TS6BaseProtocol):
self.name, target) self.name, target)
userobj.opertype = otype userobj.opertype = otype
# InspIRCd 2.x uses _ in OPERTYPE to denote spaces, while InspIRCd 3.x does not. This is not # InspIRCd 2.x uses _ in OPERTYPE to denote spaces, while InspIRCd 3.x does not.
# backwards compatible: spaces in InspIRCd 2.x will cause the oper type to get cut off at # This is one of the few things not fixed by 2.0/3.0 link compat, so here's a workaround
# the first word, while underscores in InspIRCd 3.x are shown literally as _.
# We can do the underscore fixing based on the version of our uplink:
if self.remote_proto_ver < 1205: if self.remote_proto_ver < 1205:
otype = otype.replace(" ", "_") otype = otype.replace(" ", "_")
else: else:
@ -239,13 +252,29 @@ class InspIRCdProtocol(TS6BaseProtocol):
self._remove_client(target) self._remove_client(target)
def topic_burst(self, numeric, target, text): def topic(self, source, target, text):
"""Sends a topic change from a PyLink client."""
if not self.is_internal_client(source):
raise LookupError('No such PyLink client exists.')
if self.proto_ver >= 1205:
self._send_with_prefix(source, 'FTOPIC %s %s %s :%s' % (target, self._channels[target].ts, int(time.time()), text))
else:
return super().topic(source, target, text)
def topic_burst(self, source, target, text):
"""Sends a topic change from a PyLink server. This is usually used on burst.""" """Sends a topic change from a PyLink server. This is usually used on burst."""
if not self.is_internal_server(numeric): if not self.is_internal_server(source):
raise LookupError('No such PyLink server exists.') raise LookupError('No such PyLink server exists.')
ts = int(time.time())
servername = self.servers[numeric].name topic_ts = int(time.time())
self._send_with_prefix(numeric, 'FTOPIC %s %s %s :%s' % (target, ts, servername, text)) servername = self.servers[source].name
if self.proto_ver >= 1205:
self._send_with_prefix(source, 'FTOPIC %s %s %s %s :%s' % (target, self._channels[target].ts, topic_ts, servername, text))
else:
self._send_with_prefix(source, 'FTOPIC %s %s %s :%s' % (target, topic_ts, servername, text))
self._channels[target].topic = text self._channels[target].topic = text
self._channels[target].topicset = True self._channels[target].topicset = True
@ -305,6 +334,12 @@ class InspIRCdProtocol(TS6BaseProtocol):
self.call_hooks([self.sid, 'CHGNAME', self.call_hooks([self.sid, 'CHGNAME',
{'target': target, 'newgecos': text}]) {'target': target, 'newgecos': text}])
def oper_notice(self, source, text):
"""
Send a message to all opers.
"""
# <- :70M SNONOTICE G :From jlu5: aaaaaa
self._send_with_prefix(self.sid, 'SNONOTICE G :From %s: %s' % (self.get_friendly_name(source), text))
def numeric(self, source, numeric, target, text): def numeric(self, source, numeric, target, text):
"""Sends raw numerics from a server to a remote client.""" """Sends raw numerics from a server to a remote client."""
@ -313,12 +348,25 @@ class InspIRCdProtocol(TS6BaseProtocol):
# given user. # given user.
# <- :70M PUSH 0ALAAAAAA ::midnight.vpn 422 PyLink-devel :Message of the day file is missing. # <- :70M PUSH 0ALAAAAAA ::midnight.vpn 422 PyLink-devel :Message of the day file is missing.
# Note: InspIRCd 2.2 uses a new NUM command in this format: # InspIRCd 3 uses a new NUM command in this format:
# :<sid> NUM <numeric source sid> <target uuid> <3 digit number> <params> # -> NUM <numeric source sid> <target uuid> <numeric ID> <params>
# Take this into consideration if we ever target InspIRCd 2.2, even though m_spanningtree if self.proto_ver >= 1205:
# does provide backwards compatibility for commands like this. -GLolol self._send('NUM %s %s %s %s' % (source, target, numeric, text))
else:
self._send_with_prefix(self.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 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.')
if self.proto_ver >= 1205: # insp3
# Note: insp3 supports optionally sending an invite expiration (after the TS argument),
# but we don't use / expose that feature yet.
self._send_with_prefix(source, 'INVITE %s %s %d' % (target, channel, self._channels[channel].ts))
else: # insp2
self._send_with_prefix(source, 'INVITE %s %s' % (target, channel))
def away(self, source, text): def away(self, source, text):
"""Sends an AWAY message from a PyLink client. <text> can be an empty string """Sends an AWAY message from a PyLink client. <text> can be an empty string
to unset AWAY status.""" to unset AWAY status."""
@ -364,6 +412,11 @@ class InspIRCdProtocol(TS6BaseProtocol):
raise ValueError('Invalid server name %r' % name) raise ValueError('Invalid server name %r' % name)
self.servers[sid] = Server(self, uplink, name, internal=True, desc=desc) self.servers[sid] = Server(self, uplink, name, internal=True, desc=desc)
if self.proto_ver >= 1205:
# <- :3IN SERVER services.abc.local 0SV :Some server
self._send_with_prefix(uplink, 'SERVER %s %s :%s' % (name, sid, desc))
else:
# <- :00A SERVER test.server * 1 00C :test
self._send_with_prefix(uplink, 'SERVER %s * %s %s :%s' % (name, self.servers[sid].hopcount, sid, desc)) self._send_with_prefix(uplink, 'SERVER %s * %s %s :%s' % (name, self.servers[sid].hopcount, sid, desc))
# Endburst delay clutter # Endburst delay clutter
@ -419,7 +472,14 @@ class InspIRCdProtocol(TS6BaseProtocol):
sdesc=self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc'])) sdesc=self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']))
self._send_with_prefix(self.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.
# InspIRCd sends VERSION data on link, instead of when requested by a client.
if self.proto_ver >= 1205:
verstr = self.version()
for version_type in {'version', 'rawversion'}:
self._send_with_prefix(self.sid, 'SINFO %s :%s' % (version_type, verstr.split(' ', 1)[0]))
self._send_with_prefix(self.sid, 'SINFO fullversion :%s' % verstr)
else:
self._send_with_prefix(self.sid, 'VERSION :%s' % self.version()) self._send_with_prefix(self.sid, 'VERSION :%s' % self.version())
self._send_with_prefix(self.sid, 'ENDBURST') self._send_with_prefix(self.sid, 'ENDBURST')
@ -439,12 +499,42 @@ class InspIRCdProtocol(TS6BaseProtocol):
Handles the CAPAB command, used for capability negotiation with our Handles the CAPAB command, used for capability negotiation with our
uplink. uplink.
""" """
# 6 CAPAB commands are usually sent on connect: CAPAB START, MODULES, # 5 CAPAB subcommands are usually sent on connect (excluding START and END):
# MODSUPPORT, CHANMODES, USERMODES, and CAPABILITIES. # CAPAB MODULES, MODSUPPORT, CHANMODES, USERMODES, and CAPABILITIES
# The only ones of interest to us are CHANMODES, USERMODES, # We check just about everything except MODULES
# CAPABILITIES, and MODSUPPORT.
if args[0] == 'CHANMODES': if args[0] == 'START':
# Check the protocol version
# insp20:
# <- CAPAB START 1202
# insp3:
# <- CAPAB START 1205
self.remote_proto_ver = protocol_version = int(args[1])
log.debug("(%s) handle_capab: got remote protocol version %s", self.name, protocol_version)
if protocol_version < self.proto_ver:
raise ProtocolError("Remote protocol version is too old! "
"At least %s is needed. (got %s)" %
(self.proto_ver, protocol_version))
elif protocol_version > self.MAX_PROTO_VER:
log.warning("(%s) PyLink support for InspIRCd > 3.x is experimental, "
"and should not be relied upon for anything important.",
self.name)
elif protocol_version >= 1205 > self.proto_ver:
log.warning("(%s) PyLink 3.0 introduces native support for InspIRCd 3. "
"You should enable this by setting the 'target_version' option in your "
"InspIRCd server block to 'insp3'. Otherwise, some features will not "
"work correctly!", self.name)
log.warning("(%s) Falling back to InspIRCd 2.0 (compatibility) mode.", self.name)
if self.proto_ver >= 1205:
# Clear mode lists, they will be negotiated during burst
self.cmodes = {'*A': '', '*B': '', '*C': '', '*D': ''}
self.umodes = {'*A': '', '*B': '', '*C': '', '*D': ''}
self.prefixmodes.clear()
if args[0] in {'CHANMODES', 'USERMODES'}:
# insp20:
# <- CAPAB CHANMODES :admin=&a allowinvite=A autoop=w ban=b # <- CAPAB CHANMODES :admin=&a allowinvite=A autoop=w ban=b
# banexception=e blockcolor=c c_registered=r exemptchanops=X # banexception=e blockcolor=c c_registered=r exemptchanops=X
# filter=g flood=f halfop=%h history=H invex=I inviteonly=i # filter=g flood=f halfop=%h history=H invex=I inviteonly=i
@ -453,23 +543,73 @@ class InspIRCdProtocol(TS6BaseProtocol):
# official-join=!Y op=@o operonly=O opmoderated=U owner=~q # official-join=!Y op=@o operonly=O opmoderated=U owner=~q
# permanent=P private=p redirect=L reginvite=R regmoderated=M # permanent=P private=p redirect=L reginvite=R regmoderated=M
# secret=s sslonly=z stripcolor=S topiclock=t voice=+v # secret=s sslonly=z stripcolor=S topiclock=t voice=+v
# <- CAPAB USERMODES :bot=B callerid=g cloak=x deaf_commonchan=c
# helpop=h hidechans=I hideoper=H invisible=i oper=o regdeaf=R
# servprotect=k showwhois=W snomask=s u_registered=r u_stripcolor=S
# wallops=w
# insp3:
# <- CAPAB CHANMODES :list:autoop=w list:ban=b list:banexception=e list:filter=g list:invex=I
# list:namebase=Z param-set:anticaps=B param-set:flood=f param-set:joinflood=j param-set:kicknorejoin=J
# param-set:limit=l param-set:nickflood=F param-set:redirect=L param:key=k prefix:10000:voice=+v
# prefix:20000:halfop=%h prefix:30000:op=@o prefix:40000:admin=&a prefix:50000:founder=~q
# prefix:9000000:official-join=!Y simple:allowinvite=A simple:auditorium=u simple:blockcolor=c
# simple:c_registered=r simple:censor=G simple:inviteonly=i simple:moderated=m simple:noctcp=C
# simple:noextmsg=n simple:nokick=Q simple:noknock=K simple:nonick=N simple:nonotice=T
# simple:operonly=O simple:permanent=P simple:private=p simple:reginvite=R simple:regmoderated=M
# simple:secret=s simple:sslonly=z simple:stripcolor=S simple:topiclock=t
# <- CAPAB USERMODES :param-set:snomask=s simple:antiredirect=L simple:bot=B simple:callerid=g simple:cloak=x
# simple:deaf_commonchan=c simple:helpop=h simple:hidechans=I simple:hideoper=H simple:invisible=i
# simple:oper=o simple:regdeaf=R simple:u_censor=G simple:u_registered=r simple:u_stripcolor=S
# simple:wallops=w
mydict = self.cmodes if args[0] == 'CHANMODES' else self.umodes
# Named modes are essential for a cross-protocol IRC service. We # Named modes are essential for a cross-protocol IRC service. We
# can use InspIRCd as a model here and assign a similar mode map to # can use InspIRCd as a model here and assign a similar mode map to
# our cmodes list. # our cmodes list.
for modepair in args[-1].split(): for modepair in args[-1].split():
name, char = modepair.split('=') name, char = modepair.rsplit('=', 1)
# Strip c_ prefixes to be consistent with other protocols. if self.proto_ver >= 1205:
name = name.lstrip('c_') # Detect mode types from the mode type tag
parts = name.split(':')
modetype = parts[0]
name = parts[-1]
# Modes are divided into A, B, C, and D classes
# See http://www.irc.org/tech_docs/005.html
if modetype == 'simple': # No parameter
mydict['*D'] += char
elif modetype == 'param-set': # Only parameter when setting (e.g. cmode +l)
mydict['*C'] += char
elif modetype == 'param': # Always has parameter (e.g. cmode +k)
mydict['*B'] += char
elif modetype == 'list': # List modes like ban, except, invex, ...
mydict['*A'] += char
elif modetype == 'prefix': # prefix:30000:op=@o
if args[0] != 'CHANMODES': # This should never happen...
log.warning("(%s) Possible desync? Got a prefix type modepair %r but not for channel modes", self.name, modepair)
else:
# We don't do anything with prefix levels yet, let's just store them for future use
self._prefix_levels[name] = int(parts[1])
# Map mode names to their prefixes
self.prefixmodes[char[-1]] = char[0]
# Strip c_, u_ prefixes to be consistent with other protocols.
if name.startswith(('c_', 'u_')):
name = name[2:]
if name == 'reginvite': # Reginvite? That's an odd name. if name == 'reginvite': # Reginvite? That's an odd name.
name = 'regonly' name = 'regonly'
if name == 'antiredirect': # User mode +L
name = 'noforward'
if name == 'founder': # Channel mode +q if name == 'founder': # Channel mode +q
# Founder, owner; same thing. m_customprefix allows you to # Founder, owner; same thing. m_customprefix allows you to name it anything you like,
# name it anything you like. The former is config default, # but PyLink uses the latter in its definitions
# but I personally prefer the latter.
name = 'owner' name = 'owner'
if name in ('repeat', 'kicknorejoin'): if name in ('repeat', 'kicknorejoin'):
@ -477,82 +617,84 @@ class InspIRCdProtocol(TS6BaseProtocol):
# be safely relayed. # be safely relayed.
name += '_insp' name += '_insp'
# We don't care about mode prefixes; just the mode char. # Add the mode char to our table
self.cmodes[name] = char[-1] mydict[name] = char[-1]
elif args[0] == 'USERMODES':
# <- CAPAB USERMODES :bot=B callerid=g cloak=x deaf_commonchan=c
# helpop=h hidechans=I hideoper=H invisible=i oper=o regdeaf=R
# servprotect=k showwhois=W snomask=s u_registered=r u_stripcolor=S
# wallops=w
# Ditto above.
for modepair in args[-1].split():
name, char = modepair.split('=')
# Strip u_ prefixes to be consistent with other protocols.
name = name.lstrip('u_')
self.umodes[name] = char
elif args[0] == 'CAPABILITIES': elif args[0] == 'CAPABILITIES':
# insp20:
# <- CAPAB CAPABILITIES :NICKMAX=21 CHANMAX=64 MAXMODES=20 # <- CAPAB CAPABILITIES :NICKMAX=21 CHANMAX=64 MAXMODES=20
# IDENTMAX=11 MAXQUIT=255 MAXTOPIC=307 MAXKICK=255 MAXGECOS=128 # IDENTMAX=11 MAXQUIT=255 MAXTOPIC=307 MAXKICK=255 MAXGECOS=128
# MAXAWAY=200 IP6SUPPORT=1 PROTOCOL=1202 PREFIX=(Yqaohv)!~&@%+ # MAXAWAY=200 IP6SUPPORT=1 PROTOCOL=1202 PREFIX=(Yqaohv)!~&@%+
# CHANMODES=IXbegw,k,FHJLfjl,ACKMNOPQRSTUcimnprstz # CHANMODES=IXbegw,k,FHJLfjl,ACKMNOPQRSTUcimnprstz
# USERMODES=,,s,BHIRSWcghikorwx GLOBOPS=1 SVSPART=1 # USERMODES=,,s,BHIRSWcghikorwx GLOBOPS=1 SVSPART=1
# insp3:
# CAPAB CAPABILITIES :NICKMAX=30 CHANMAX=64 MAXMODES=20 IDENTMAX=10 MAXQUIT=255 MAXTOPIC=307
# MAXKICK=255 MAXREAL=128 MAXAWAY=200 MAXHOST=64 CHALLENGE=xxxxxxxxx CASEMAPPING=ascii GLOBOPS=1
# First, turn the arguments into a dict # First, turn the arguments into a dict
caps = self.parse_isupport(args[-1]) caps = self.parse_isupport(args[-1])
log.debug("(%s) capabilities list: %s", self.name, caps) log.debug("(%s) handle_capab: capabilities list is %s", self.name, caps)
# Check the protocol version
self.remote_proto_ver = protocol_version = int(caps['PROTOCOL'])
if protocol_version < self.min_proto_ver:
raise ProtocolError("Remote protocol version is too old! "
"At least %s (InspIRCd 2.0.x) is "
"needed. (got %s)" % (self.min_proto_ver,
protocol_version))
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.name)
# Store the max nick and channel lengths # Store the max nick and channel lengths
if 'NICKMAX' in caps:
self.maxnicklen = int(caps['NICKMAX']) self.maxnicklen = int(caps['NICKMAX'])
if 'CHANMAX' in caps:
self.maxchanlen = int(caps['CHANMAX']) self.maxchanlen = int(caps['CHANMAX'])
# Casemapping - this is only sent in InspIRCd 3.x
if 'CASEMAPPING' in caps:
self.casemapping = caps['CASEMAPPING']
log.debug('(%s) handle_capab: updated casemapping to %s', self.name, self.casemapping)
# Modes are divided into A, B, C, and D classes # InspIRCd 2 only: mode & prefix definitions are sent as CAPAB CAPABILITIES CHANMODES/USERMODES/PREFIX
# See http://www.irc.org/tech_docs/005.html if self.proto_ver < 1205:
if 'CHANMODES' in caps:
# FIXME: Find a neater way to assign/store this.
self.cmodes['*A'], self.cmodes['*B'], self.cmodes['*C'], self.cmodes['*D'] \ self.cmodes['*A'], self.cmodes['*B'], self.cmodes['*C'], self.cmodes['*D'] \
= caps['CHANMODES'].split(',') = caps['CHANMODES'].split(',')
if 'USERMODES' in caps:
self.umodes['*A'], self.umodes['*B'], self.umodes['*C'], self.umodes['*D'] \ self.umodes['*A'], self.umodes['*B'], self.umodes['*C'], self.umodes['*D'] \
= caps['USERMODES'].split(',') = caps['USERMODES'].split(',')
if 'PREFIX' in caps:
# Separate the prefixes field (e.g. "(Yqaohv)!~&@%+") into a # Separate the prefixes field (e.g. "(Yqaohv)!~&@%+") into a
# dict mapping mode characters to mode prefixes. # dict mapping mode characters to mode prefixes.
self.prefixmodes = self.parse_isupport_prefixes(caps['PREFIX']) self.prefixmodes = self.parse_isupport_prefixes(caps['PREFIX'])
log.debug('(%s) self.prefixmodes set to %r', self.name, log.debug('(%s) handle_capab: self.prefixmodes set to %r', self.name,
self.prefixmodes) self.prefixmodes)
elif args[0] == 'MODSUPPORT': 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 # <- 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 |= set(args[-1].split()) self._modsupport |= set(args[-1].split())
def handle_kick(self, source, command, args):
"""Handles incoming KICKs."""
# InspIRCD 3 adds membership IDs to KICK messages when forwarding across servers
# <- :3INAAAAAA KICK #endlessvoid 3INAAAAAA :test (local)
# <- :3INAAAAAA KICK #endlessvoid 7PYAAAAAA 0 :test (remote)
if self.proto_ver >= 1205 and len(args) > 3:
del args[2]
return super().handle_kick(source, command, args)
def handle_ping(self, source, command, args): def handle_ping(self, source, command, args):
"""Handles incoming PING commands, so we don't time out.""" """Handles incoming PING commands, so we don't time out."""
# InspIRCd 2:
# <- :70M PING 70M 0AL # <- :70M PING 70M 0AL
# -> :0AL PONG 0AL 70M # -> :0AL PONG 0AL 70M
# InspIRCd 3:
# <- :3IN PING 808
# -> :808 PONG 3IN
if len(args) >= 2: if len(args) >= 2:
self._send_with_prefix(args[1], 'PONG %s %s' % (args[1], source), queue=False) self._send_with_prefix(args[1], 'PONG %s %s' % (args[1], source), queue=False)
else: else:
self._send_with_prefix(self.sid, 'PONG %s' % source, queue=False) self._send_with_prefix(args[0], 'PONG %s' % source, queue=False)
def handle_fjoin(self, servernumeric, command, args): def handle_fjoin(self, servernumeric, command, args):
"""Handles incoming FJOIN commands (InspIRCd equivalent of JOIN/SJOIN).""" """Handles incoming FJOIN commands (InspIRCd equivalent of JOIN/SJOIN)."""
# :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :o,1SRAABIT4 v,1IOAAF53R <...> # insp2:
# <- :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :o,1SRAABIT4 v,1IOAAF53R <...>
# insp3:
# <- :3IN FJOIN #test 1556842195 +nt :o,3INAAAAAA:4
channel = args[0] channel = args[0]
chandata = self._channels[channel].deepcopy() chandata = self._channels[channel].deepcopy()
# InspIRCd sends each channel's users in the form of 'modeprefix(es),UID' # InspIRCd sends each channel's users in the form of 'modeprefix(es),UID'
@ -568,6 +710,10 @@ class InspIRCdProtocol(TS6BaseProtocol):
for user in userlist: for user in userlist:
modeprefix, user = user.split(',', 1) modeprefix, user = user.split(',', 1)
if self.proto_ver >= 1205:
# XXX: we don't handle membership IDs yet
user = user.split(':', 1)[0]
# Don't crash when we get an invalid UID. # Don't crash when we get an invalid UID.
if user not in self.users: if user not in self.users:
log.debug('(%s) handle_fjoin: tried to introduce user %s not in our user list, ignoring...', log.debug('(%s) handle_fjoin: tried to introduce user %s not in our user list, ignoring...',
@ -595,9 +741,34 @@ class InspIRCdProtocol(TS6BaseProtocol):
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts, return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts,
'channeldata': chandata} 'channeldata': chandata}
def handle_ijoin(self, source, command, args):
"""Handles InspIRCd 3 joins with membership ID."""
# insp3:
# EX: regular /join on an existing channel
# <- :3INAAAAAA IJOIN #valhalla 6
# EX: /ojoin on an existing channel
# <- :3INAAAAAA IJOIN #valhalla 7 1559348434 Yo
# From insp3 source:
# <- :<uid> IJOIN <chan> <membid> [<ts> [<flags>]]
# args idx: 0 1 2 3
# For now we don't care about the membership ID
channel = args[0]
self.users[source].channels.add(channel)
self._channels[channel].users.add(source)
# Apply prefix modes if they exist and the TS check passes
if len(args) >= 4 and int(args[2]) <= self._channels[channel].ts:
self.apply_modes(source, {('+%s' % mode, source) for mode in args[3]})
return {'channel': channel, 'users': [source], 'modes':
self._channels[channel].modes}
def handle_uid(self, numeric, command, args): def handle_uid(self, numeric, command, args):
"""Handles incoming UID commands (user introduction).""" """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 # :70M UID 70MAAAAAB 1429934638 jlu5 0::1 hidden-7j810p.9mdf.lrek.0000.0000.IP jlu5 0::1 1429934638 +Wioswx +ACGKNOQXacfgklnoqvx :realname
uid, ts, nick, realhost, host, ident, ip = args[0:7] uid, ts, nick, realhost, host, ident, ip = args[0:7]
ts = int(ts) ts = int(ts)
@ -612,34 +783,41 @@ class InspIRCdProtocol(TS6BaseProtocol):
self._check_oper_status_change(uid, parsedmodes) self._check_oper_status_change(uid, parsedmodes)
self.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} # InspIRCd sends SSL status in the metadata command, so the info is not known at this point
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip, 'secure': None}
def handle_server(self, numeric, command, args): def handle_server(self, source, command, args):
"""Handles incoming SERVER commands (introduction of servers).""" """Handles incoming SERVER commands (introduction of servers)."""
# Initial SERVER command on connect.
if self.uplink is None: if self.uplink is None:
# <- SERVER whatever.net abcdefgh 0 10X :some server description # <- SERVER whatever.net abcdefgh 0 10X :some server description
servername = args[0].lower() servername = args[0].lower()
numeric = args[3] source = args[3]
if args[1] != self.serverdata['recvpass']: if args[1] != self.serverdata['recvpass']:
# Check if recvpass is correct # Check if recvpass is correct
raise ProtocolError('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] sdesc = args[-1]
self.servers[numeric] = Server(self, None, servername, desc=sdesc) self.servers[source] = Server(self, None, servername, desc=sdesc)
self.uplink = numeric self.uplink = source
log.debug('(%s) inspircd: found uplink %s', self.name, self.uplink)
return return
# Other server introductions. # Other server introductions.
# insp20:
# <- :00A SERVER test.server * 1 00C :testing raw message syntax # <- :00A SERVER test.server * 1 00C :testing raw message syntax
# insp3:
# <- :3IN SERVER services.abc.local 0SV :Some server
servername = args[0].lower() servername = args[0].lower()
sid = args[3] if self.proto_ver >= 1205:
sid = args[1] # insp3
else:
sid = args[3] # insp20
sdesc = args[-1] sdesc = args[-1]
self.servers[sid] = Server(self, numeric, servername, desc=sdesc) self.servers[sid] = Server(self, source, servername, desc=sdesc)
return {'name': servername, 'sid': args[3], 'text': sdesc} return {'name': servername, 'sid': sid, 'text': sdesc}
def handle_fmode(self, numeric, command, args): def handle_fmode(self, numeric, command, args):
"""Handles the FMODE command, used for channel mode changes.""" """Handles the FMODE command, used for channel mode changes."""
@ -672,18 +850,36 @@ class InspIRCdProtocol(TS6BaseProtocol):
# First arg = source, second = signon time, third = idle time # First arg = source, second = signon time, third = idle time
self._send_with_prefix(target, 'IDLE %s %s 0' % (source, start_time)) self._send_with_prefix(target, 'IDLE %s %s 0' % (source, start_time))
def handle_ftopic(self, numeric, command, args): def handle_ftopic(self, source, command, args):
"""Handles incoming FTOPIC (sets topic on burst).""" """Handles incoming topic changes."""
# <- :70M FTOPIC #channel 1434510754 GLo|o|!GLolol@escape.the.dreamland.ca :Some channel topic # insp2 (only used for server senders):
# <- :70M FTOPIC #channel 1434510754 jlu5!jlu5@escape.the.dreamland.ca :Some channel topic
# insp3 (used for server AND user senders):
# <- :3IN FTOPIC #qwerty 1556828864 1556844505 jlu5!jlu5@midnight-umk.of4.0.127.IP :1234abcd
# <- :3INAAAAAA FTOPIC #qwerty 1556828864 1556844248 :topic text
# chan creation time ^ ^ topic set time (the one we want)
# <- :00A SVSTOPIC #channel 1538402416 SomeUser :test
channel = args[0] channel = args[0]
if self.proto_ver >= 1205 and command == 'FTOPIC':
ts = args[2]
if source in self.users:
setter = source
else:
setter = args[3]
else:
ts = args[1] ts = args[1]
setter = args[2] setter = args[2]
ts = int(ts)
topic = args[-1] topic = args[-1]
self._channels[channel].topic = topic self._channels[channel].topic = topic
self._channels[channel].topicset = True self._channels[channel].topicset = True
return {'channel': channel, 'setter': setter, 'ts': ts, 'text': topic} return {'channel': channel, 'setter': setter, 'ts': ts, 'text': topic}
# SVSTOPIC is used by InspIRCd module m_topiclock - its arguments are the same as FTOPIC # SVSTOPIC is used by InspIRCd module m_topiclock - its arguments are the same as insp2 FTOPIC
handle_svstopic = handle_ftopic handle_svstopic = handle_ftopic
def handle_opertype(self, target, command, args): def handle_opertype(self, target, command, args):
@ -773,8 +969,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
def handle_metadata(self, numeric, command, args): def handle_metadata(self, numeric, command, args):
""" """
Handles the METADATA command, used by servers to send metadata (services Handles the METADATA command, used by servers to send metadata for various objects.
login name, certfp data, etc.) for clients.
""" """
uid = args[0] uid = args[0]
@ -782,8 +977,8 @@ class InspIRCdProtocol(TS6BaseProtocol):
# <- :00A METADATA 1MLAAAJET accountname : # <- :00A METADATA 1MLAAAJET accountname :
# <- :00A METADATA 1MLAAAJET accountname :tester # <- :00A METADATA 1MLAAAJET accountname :tester
# Sets the services login name of the client. # Sets the services login name of the client.
self.call_hooks([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: elif args[1] == 'modules' and numeric == self.uplink:
# Note: only handle METADATA from our uplink; otherwise leaf servers unloading modules # Note: only handle METADATA from our uplink; otherwise leaf servers unloading modules
# while shutting down will corrupt the state. # while shutting down will corrupt the state.
@ -798,6 +993,8 @@ class InspIRCdProtocol(TS6BaseProtocol):
self._modsupport.add(module[1:]) self._modsupport.add(module[1:])
else: else:
log.warning('(%s) Got unknown METADATA modules string: %r', self.name, args[-1]) log.warning('(%s) Got unknown METADATA modules string: %r', self.name, args[-1])
elif args[1] == 'ssl_cert' and uid in self.users:
self.users[uid].ssl = True
def handle_version(self, numeric, command, args): def handle_version(self, numeric, command, args):
""" """

View File

@ -2,54 +2,41 @@
ircs2s_common.py: Common base protocol class with functions shared by TS6 and P10-based protocols. ircs2s_common.py: Common base protocol class with functions shared by TS6 and P10-based protocols.
""" """
import time
import re import re
from collections import defaultdict import time
from pylinkirc import conf
from pylinkirc.classes import IRCNetwork, ProtocolError from pylinkirc.classes import IRCNetwork, ProtocolError
from pylinkirc.log import log from pylinkirc.log import log
from pylinkirc import utils, conf
class IncrementalUIDGenerator(): __all__ = ['UIDGenerator', 'IRCCommonProtocol', 'IRCS2SProtocol']
class UIDGenerator():
""" """
Incremental UID Generator module, adapted from InspIRCd source: Generate UIDs for IRC S2S.
https://github.com/inspircd/inspircd/blob/f449c6b296ab/src/server.cpp#L85-L156
""" """
def __init__(self, sid): def __init__(self, uidchars, length, sid):
if not (hasattr(self, 'allowedchars') and hasattr(self, 'length')): self.uidchars = uidchars # corpus of characters to choose from
raise RuntimeError("Allowed characters list not defined. Subclass " self.length = length # desired length of uid part, padded with uidchars[0]
"%s by defining self.allowedchars and self.length " self.sid = str(sid) # server id (prefixed to every result)
"and then calling super().__init__()." % self.__class__.__name__) self.counter = 0
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): def next_uid(self):
""" """
Returns the next unused UID for the server. Returns the next unused UID for the server.
""" """
uid = self.sid + ''.join(self.uidchars) uid = ''
self.increment() num = self.counter
return uid if num >= (len(self.uidchars) ** self.length):
raise RuntimeError("UID overflowed")
while num > 0:
num, index = divmod(num, len(self.uidchars))
uid = self.uidchars[index] + uid
self.counter += 1
uid = uid.rjust(self.length, self.uidchars[0])
return self.sid + uid
class IRCCommonProtocol(IRCNetwork): class IRCCommonProtocol(IRCNetwork):
@ -60,6 +47,7 @@ class IRCCommonProtocol(IRCNetwork):
self._caps = {} self._caps = {}
self._use_builtin_005_handling = False # Disabled by default for greater security self._use_builtin_005_handling = False # Disabled by default for greater security
self.protocol_caps |= {'has-irc-modes', 'can-manage-bot-channels'}
def post_connect(self): def post_connect(self):
self._caps.clear() self._caps.clear()
@ -77,30 +65,6 @@ class IRCCommonProtocol(IRCNetwork):
"Invalid port %r for network %s" "Invalid port %r for network %s"
% (port, self.name)) % (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 @staticmethod
def parse_args(args): def parse_args(args):
""" """
@ -118,6 +82,7 @@ class IRCCommonProtocol(IRCNetwork):
joined_arg = ' '.join(args[idx:])[1:] # Cut off the leading : as well joined_arg = ' '.join(args[idx:])[1:] # Cut off the leading : as well
real_args.append(joined_arg) real_args.append(joined_arg)
break break
elif arg.strip(' '): # Skip empty args that aren't part of the multi-word arg
real_args.append(arg) real_args.append(arg)
return real_args return real_args
@ -130,63 +95,6 @@ class IRCCommonProtocol(IRCNetwork):
args[0] = args[0].split(':', 1)[1] args[0] = args[0].split(':', 1)[1]
return args 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 @staticmethod
def parse_isupport(args, fallback=''): def parse_isupport(args, fallback=''):
""" """
@ -217,6 +125,33 @@ class IRCCommonProtocol(IRCNetwork):
prefixsearch = re.search(r'\(([A-Za-z]+)\)(.*)', args) prefixsearch = re.search(r'\(([A-Za-z]+)\)(.*)', args)
return dict(zip(prefixsearch.group(1), prefixsearch.group(2))) return dict(zip(prefixsearch.group(1), prefixsearch.group(2)))
@classmethod
def parse_message_tags(cls, data):
"""
Parses IRCv3.2 message tags from a message, as described at http://ircv3.net/specs/core/message-tags-3.2.html
data is a list of command arguments, split by spaces.
"""
# Example query:
# @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello
if data[0].startswith('@'):
tagdata = data[0].lstrip('@').split(';')
for idx, tag in enumerate(tagdata):
tag = tag.replace('\\s', ' ')
tag = tag.replace('\\r', '\r')
tag = tag.replace('\\n', '\n')
tag = tag.replace('\\:', ';')
# We want to drop lone \'s but keep \\ as \ ...
tag = tag.replace('\\\\', '\x00')
tag = tag.replace('\\', '')
tag = tag.replace('\x00', '\\')
tagdata[idx] = tag
results = cls.parse_isupport(tagdata, fallback='')
return results
return {}
def handle_away(self, source, command, args): def handle_away(self, source, command, args):
"""Handles incoming AWAY messages.""" """Handles incoming AWAY messages."""
# TS6: # TS6:
@ -333,7 +268,7 @@ class IRCS2SProtocol(IRCCommonProtocol):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.protocol_caps = {'can-spawn-clients', 'has-ts', 'can-host-relay', self.protocol_caps |= {'can-spawn-clients', 'has-ts', 'can-host-relay',
'can-track-servers'} 'can-track-servers'}
# Alias # Alias
@ -351,22 +286,27 @@ class IRCS2SProtocol(IRCCommonProtocol):
the SID of the uplink server. the SID of the uplink server.
""" """
data = data.split(" ") data = data.split(" ")
tags = self.parse_message_tags(data)
if tags:
# If we have message tags, split off the first argument.
data = data[1:]
args = self.parse_args(data) args = self.parse_args(data)
sender = args[0] sender = args[0]
sender = sender.lstrip(':') if sender.startswith(':'):
sender = sender[1:]
# If the sender isn't in numeric format, try to convert it automatically. # If the sender isn't in numeric format, try to convert it automatically.
sender_sid = self._get_SID(sender) sender_sid = self._get_SID(sender)
sender_uid = self._get_UID(sender) sender_uid = self._get_UID(sender)
if sender_sid in self.servers: if sender_sid in self.servers:
# Sender is a server (converting from name to SID gave a valid result).
sender = sender_sid sender = sender_sid
elif sender_uid in self.users: elif sender_uid in self.users:
# Sender is a user (converting from name to UID gave a valid result).
sender = sender_uid sender = sender_uid
elif not (args[0].startswith(':')): else:
# No sender prefix; treat as coming from uplink IRCd. # No sender prefix; treat as coming from uplink IRCd.
sender = self.uplink sender = self.uplink
args.insert(0, sender) args.insert(0, sender)
@ -387,7 +327,7 @@ class IRCS2SProtocol(IRCCommonProtocol):
if command == 'ENCAP': if command == 'ENCAP':
# Special case for TS6 encapsulated commands (ENCAP), in forms like this: # Special case for TS6 encapsulated commands (ENCAP), in forms like this:
# <- :00A ENCAP * SU 42XAAAAAC :GLolol # <- :00A ENCAP * SU 42XAAAAAC :jlu5
command = args[1] command = args[1]
args = args[2:] args = args[2:]
log.debug("(%s) Rewriting incoming ENCAP to command %s (args: %s)", self.name, command, args) log.debug("(%s) Rewriting incoming ENCAP to command %s (args: %s)", self.name, command, args)
@ -399,10 +339,12 @@ class IRCS2SProtocol(IRCCommonProtocol):
else: else:
parsed_args = func(sender, command, args) parsed_args = func(sender, command, args)
if parsed_args is not None: if parsed_args is not None:
if tags:
parsed_args['tags'] = tags # Add message tags to this hook payload.
return [sender, command, parsed_args] return [sender, command, parsed_args]
def invite(self, source, target, channel): def invite(self, source, target, channel):
"""Sends an INVITE from a PyLink client..""" """Sends an INVITE from a PyLink client."""
if not self.is_internal_client(source): if not self.is_internal_client(source):
raise LookupError('No such PyLink client exists.') raise LookupError('No such PyLink client exists.')
@ -428,6 +370,12 @@ class IRCS2SProtocol(IRCCommonProtocol):
# handle_part() does that just fine. # handle_part() does that just fine.
self.handle_part(target, 'KICK', [channel]) self.handle_part(target, 'KICK', [channel])
def oper_notice(self, source, text):
"""
Send a message to all opers.
"""
self._send_with_prefix(source, 'WALLOPS :%s' % text)
def numeric(self, source, numeric, target, text): def numeric(self, source, numeric, target, text):
"""Sends raw numerics from a server to a remote client. This is used for WHOIS replies.""" """Sends raw numerics from a server to a remote client. This is used for WHOIS replies."""
# Mangle the target for IRCds that require it. # Mangle the target for IRCds that require it.
@ -451,7 +399,7 @@ class IRCS2SProtocol(IRCCommonProtocol):
This is mostly used by PyLink internals to check whether the remote link is up.""" This is mostly used by PyLink internals to check whether the remote link is up."""
if self.sid and self.connected.is_set(): if self.sid and self.connected.is_set():
self._send_with_prefix(self.sid, 'PING %s' % self._expandPUID(self.sid)) self._send_with_prefix(self.sid, 'PING %s' % self._expandPUID(self.uplink))
def quit(self, numeric, reason): def quit(self, numeric, reason):
"""Quits a PyLink client.""" """Quits a PyLink client."""
@ -538,17 +486,18 @@ class IRCS2SProtocol(IRCCommonProtocol):
def handle_kill(self, source, command, args): def handle_kill(self, source, command, args):
"""Handles incoming KILLs.""" """Handles incoming KILLs."""
killed = self._get_UID(args[0]) killed = self._get_UID(args[0])
# Depending on whether the IRCd sends explicit QUIT messages for # Some IRCds send explicit QUIT messages for their killed clients in addition to KILL,
# killed clients, the user may or may not have automatically been # meaning that our target client may have been removed already. If this is the case,
# removed from our user list. # don't bother forwarding this message on.
# If not, we have to assume that KILL = QUIT and remove them # Generally, we only need to distinguish between KILL and QUIT if the target is
# ourselves. # one of our clients, in which case the above statement isn't really applicable.
data = self.users.get(killed) if killed in self.users:
if data: userdata = self._remove_client(killed)
self._remove_client(killed) else:
return
# TS6-style kills look something like this: # TS6-style kills look something like this:
# <- :GL KILL 38QAAAAAA :hidden-1C620195!GL (test) # <- :jlu5 KILL 38QAAAAAA :hidden-1C620195!jlu5 (test)
# What we actually want is to format a pretty kill message, in the form # What we actually want is to format a pretty kill message, in the form
# "Killed (killername (reason))". # "Killed (killername (reason))".
@ -564,21 +513,21 @@ class IRCS2SProtocol(IRCCommonProtocol):
# Get the reason, which is enclosed in brackets. # Get the reason, which is enclosed in brackets.
killmsg = ' '.join(args[1].split(" ")[1:])[1:-1] killmsg = ' '.join(args[1].split(" ")[1:])[1:-1]
if not killmsg: if not killmsg:
log.warning('(%s) Failed to extract kill reason: %r', irc.name, args) log.warning('(%s) Failed to extract kill reason: %r', self.name, args)
killmsg = '<No reason given>' killmsg = args[1]
else: else:
# We already have a preformatted kill, so just pass it on as is. # 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 # XXX: this does create a convoluted kill string if we want to forward kills
# over relay. # over relay.
# InspIRCd: # InspIRCd:
# <- :1MLAAAAA1 KILL 0ALAAAAAC :Killed (GL (test)) # <- :1MLAAAAA1 KILL 0ALAAAAAC :Killed (jlu5 (test))
# ngIRCd: # ngIRCd:
# <- :GL KILL PyLink-devel :KILLed by GL: ? # <- :jlu5 KILL PyLink-devel :KILLed by jlu5: ?
killmsg = args[1] killmsg = args[1]
return {'target': killed, 'text': killmsg, 'userdata': data} return {'target': killed, 'text': killmsg, 'userdata': userdata}
def _check_cloak_change(self, uid): def _check_cloak_change(self, uid): # Stub by default
return return
def _check_umode_away_change(self, uid): def _check_umode_away_change(self, uid):
@ -615,7 +564,7 @@ class IRCS2SProtocol(IRCCommonProtocol):
# <- :70MAAAAAA MODE 70MAAAAAA -i+xc # <- :70MAAAAAA MODE 70MAAAAAA -i+xc
# P10: # P10:
# <- ABAAA M GL -w # <- ABAAA M jlu5 -w
# <- ABAAA M #test +v ABAAB 1460747615 # <- ABAAA M #test +v ABAAB 1460747615
# <- ABAAA OM #test +h ABAAA # <- ABAAA OM #test +h ABAAA
target = self._get_UID(args[0]) target = self._get_UID(args[0])
@ -725,9 +674,14 @@ class IRCS2SProtocol(IRCCommonProtocol):
# TS6: # TS6:
# <- :1SRAAGB4T QUIT :Quit: quit message goes here # <- :1SRAAGB4T QUIT :Quit: quit message goes here
# P10: # P10:
# <- ABAAB Q :Killed (GL_ (bangbang)) # <- ABAAB Q :Killed (jlu5_ (bangbang))
self._remove_client(numeric) userdata = self._remove_client(numeric)
return {'text': args[0]} if userdata:
try:
reason = args[0]
except IndexError:
reason = ''
return {'text': reason, 'userdata': userdata}
def handle_stats(self, numeric, command, args): def handle_stats(self, numeric, command, args):
"""Handles the IRC STATS command.""" """Handles the IRC STATS command."""

View File

@ -3,7 +3,10 @@ nefarious.py: Migration stub to the new P10 protocol module.
""" """
from pylinkirc.log import log from pylinkirc.log import log
from pylinkirc.protocols.p10 import * from pylinkirc.protocols.p10 import P10Protocol
__all__ = ['NefariousProtocol']
class NefariousProtocol(P10Protocol): class NefariousProtocol(P10Protocol):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -7,14 +7,17 @@ ngircd.py: PyLink protocol module for ngIRCd.
# and https://tools.ietf.org/html/rfc2813 # and https://tools.ietf.org/html/rfc2813
## ##
import time
import re import re
import time
from pylinkirc import utils, conf, __version__ from pylinkirc import __version__, conf, utils
from pylinkirc.classes import * from pylinkirc.classes import *
from pylinkirc.log import log from pylinkirc.log import log
from pylinkirc.protocols.ircs2s_common import * from pylinkirc.protocols.ircs2s_common import *
__all__ = ['NgIRCdProtocol']
class NgIRCdProtocol(IRCS2SProtocol): class NgIRCdProtocol(IRCS2SProtocol):
def __init__(self, irc): def __init__(self, irc):
super().__init__(irc) super().__init__(irc)
@ -58,15 +61,31 @@ class NgIRCdProtocol(IRCS2SProtocol):
self._caps.clear() self._caps.clear()
self.cmodes.update({ self.cmodes.update({
'banexception': 'e', 'invex': 'I', 'regmoderated': 'M', 'nonick': 'N', 'banexception': 'e',
'operonly': 'O', 'permanent': 'P', 'nokick': 'Q', 'registered': 'r', 'invex': 'I',
'regonly': 'R', 'noinvite': 'V', 'sslonly': 'z' 'noinvite': 'V',
'nokick': 'Q',
'nonick': 'N',
'operonly': 'O',
'permanent': 'P',
'registered': 'r',
'regmoderated': 'M',
'regonly': 'R',
'sslonly': 'z'
}) })
self.umodes.update({ self.umodes.update({
'away': 'a', 'deaf': 'b', 'bot': 'B', 'sno_clientconnections': 'c', 'away': 'a',
'deaf_commonchan': 'C', 'floodexempt': 'f', 'hidechans': 'I', 'bot': 'B',
'servprotect': 'q', 'restricted': 'r', 'registered': 'R', 'cloak': 'x' 'cloak': 'x',
'deaf_commonchan': 'C',
'floodexempt': 'F',
'hidechans': 'I',
'privdeaf': 'b',
'registered': 'R',
'restricted': 'r',
'servprotect': 'q',
'sno_clientconnections': 'c'
}) })
def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(), def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(),
@ -98,7 +117,7 @@ class NgIRCdProtocol(IRCS2SProtocol):
# Grab our server token; this is used instead of server name to denote where the client is. # Grab our server token; this is used instead of server name to denote where the client is.
server_token = server.rsplit('@')[-1] server_token = server.rsplit('@')[-1]
# <- :ngircd.midnight.local NICK GL 1 ~gl localhost 1 +io :realname # <- :ngircd.midnight.local NICK jlu5 1 ~jlu5 localhost 1 +io :realname
self._send_with_prefix(server, 'NICK %s %s %s %s %s %s :%s' % (nick, self.servers[server].hopcount, 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)) ident, host, server_token, self.join_modes(modes), realname))
return userobj return userobj
@ -266,7 +285,7 @@ class NgIRCdProtocol(IRCS2SProtocol):
""" """
Sets a server ban. Sets a server ban.
""" """
# <- :GL GLINE *!*@bad.user 3d :test # <- :jlu5 GLINE *!*@bad.user 3d :test
assert not (user == host == '*'), "Refusing to set ridiculous ban on *@*" assert not (user == host == '*'), "Refusing to set ridiculous ban on *@*"
self._send_with_prefix(source, 'GLINE *!%s@%s %s :%s' % (user, host, duration, reason)) self._send_with_prefix(source, 'GLINE *!%s@%s %s :%s' % (user, host, duration, reason))
@ -361,8 +380,8 @@ class NgIRCdProtocol(IRCS2SProtocol):
def handle_join(self, source, command, args): 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 # 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: # Basically, we expect messages of the forms:
# <- :GL JOIN #test\x07o # <- :jlu5 JOIN #test\x07o
# <- :GL JOIN #moretest # <- :jlu5 JOIN #moretest
for chanpair in args[0].split(','): for chanpair in args[0].split(','):
# Normalize channel case. # Normalize channel case.
try: try:
@ -407,7 +426,7 @@ class NgIRCdProtocol(IRCS2SProtocol):
def handle_metadata(self, source, command, args): def handle_metadata(self, source, command, args):
"""Handles various user metadata for ngIRCd (cloaked host, account name, etc.)""" """Handles various user metadata for ngIRCd (cloaked host, account name, etc.)"""
# <- :ngircd.midnight.local METADATA GL cloakhost :hidden-3a2a739e.ngircd.midnight.local # <- :ngircd.midnight.local METADATA jlu5 cloakhost :hidden-3a2a739e.ngircd.midnight.local
target = self._get_UID(args[0]) target = self._get_UID(args[0])
if target not in self.users: if target not in self.users:
@ -442,7 +461,7 @@ class NgIRCdProtocol(IRCS2SProtocol):
""" """
if len(args) >= 2: if len(args) >= 2:
# User introduction: # User introduction:
# <- :ngircd.midnight.local NICK GL 1 ~gl localhost 1 +io :realname # <- :ngircd.midnight.local NICK jlu5 1 ~jlu5 localhost 1 +io :realname
nick = args[0] nick = args[0]
assert source in self.servers, "Server %r tried to introduce nick %r but isn't in the servers index?" % (source, nick) 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) self._check_nick_collision(nick)
@ -469,13 +488,13 @@ class NgIRCdProtocol(IRCS2SProtocol):
'parse_as': 'UID', 'ip': '0.0.0.0'} 'parse_as': 'UID', 'ip': '0.0.0.0'}
else: else:
# Nick changes: # Nick changes:
# <- :GL NICK :GL_ # <- :jlu5 NICK :jlu5_
oldnick = self.users[source].nick oldnick = self.users[source].nick
newnick = self.users[source].nick = args[0] newnick = self.users[source].nick = args[0]
return {'newnick': newnick, 'oldnick': oldnick} return {'newnick': newnick, 'oldnick': oldnick}
def handle_njoin(self, source, command, args): def handle_njoin(self, source, command, args):
# <- :ngircd.midnight.local NJOIN #test :tester,@%GL # <- :ngircd.midnight.local NJOIN #test :tester,@%jlu5
channel = args[0] channel = args[0]
chandata = self._channels[channel].deepcopy() chandata = self._channels[channel].deepcopy()
@ -510,7 +529,8 @@ class NgIRCdProtocol(IRCS2SProtocol):
if recvpass != self.serverdata['recvpass']: if recvpass != self.serverdata['recvpass']:
raise ProtocolError("RECVPASS from uplink does not match configuration!") 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" if 'IRC+' not in args[1]:
raise ProtocolError("Linking to non-ngIRCd server using this protocol module is not supported")
def handle_ping(self, source, command, args): def handle_ping(self, source, command, args):
""" """

View File

@ -3,22 +3,27 @@ p10.py: P10 protocol module for PyLink, supporting Nefarious IRCu and others.
""" """
import base64 import base64
import socket
import string
import struct import struct
from ipaddress import ip_address
import time import time
from ipaddress import ip_address
from pylinkirc import utils, structures, conf from pylinkirc import conf, structures, utils
from pylinkirc.classes import * from pylinkirc.classes import *
from pylinkirc.log import log from pylinkirc.log import log
from pylinkirc.protocols.ircs2s_common import * from pylinkirc.protocols.ircs2s_common import *
class P10UIDGenerator(IncrementalUIDGenerator): __all__ = ['P10Protocol']
"""Implements an incremental P10 UID Generator."""
class P10UIDGenerator(UIDGenerator):
"""Implements a P10 UID Generator."""
def __init__(self, sid): def __init__(self, sid):
self.allowedchars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789[]' uidchars = string.ascii_uppercase + string.ascii_lowercase + string.digits + '[]'
self.length = 3 length = 3
super().__init__(sid) super().__init__(uidchars, length, sid)
def p10b64encode(num, length=2): def p10b64encode(num, length=2):
""" """
@ -194,7 +199,7 @@ class P10Protocol(IRCS2SProtocol):
@staticmethod @staticmethod
def decode_p10_ip(ip): def decode_p10_ip(ip):
"""Decodes a P10 IP.""" """Decodes a P10 IP."""
# Many thanks to Jobe @ evilnet for the code on what to do here. :) -GL # Many thanks to Jobe @ evilnet for the code on what to do here. :) -jlu5
if len(ip) == 6: # IPv4 if len(ip) == 6: # IPv4
# Pad the characters with two \x00's (represented in P10 B64 as AA) # Pad the characters with two \x00's (represented in P10 B64 as AA)
@ -402,7 +407,7 @@ class P10Protocol(IRCS2SProtocol):
def kill(self, numeric, target, reason): def kill(self, numeric, target, reason):
"""Sends a kill from a PyLink client/server.""" """Sends a kill from a PyLink client/server."""
# <- ABAAA D AyAAA :nefarious.midnight.vpn!GL (test) # <- ABAAA D AyAAA :nefarious.midnight.vpn!jlu5 (test)
if (not self.is_internal_client(numeric)) and \ if (not self.is_internal_client(numeric)) and \
(not self.is_internal_server(numeric)): (not self.is_internal_server(numeric)):
@ -459,7 +464,7 @@ class P10Protocol(IRCS2SProtocol):
def mode(self, numeric, target, modes, ts=None): def mode(self, numeric, target, modes, ts=None):
"""Sends mode changes from a PyLink client/server.""" """Sends mode changes from a PyLink client/server."""
# <- ABAAA M GL -w # <- ABAAA M jlu5 -w
# <- ABAAA M #test +v ABAAB 1460747615 # <- ABAAA M #test +v ABAAB 1460747615
if (not self.is_internal_client(numeric)) and \ if (not self.is_internal_client(numeric)) and \
@ -490,7 +495,7 @@ class P10Protocol(IRCS2SProtocol):
real_target = target real_target = target
else: else:
assert target in self.users, "Unknown mode target %s" % target assert target in self.users, "Unknown mode target %s" % target
# P10 uses nicks in user MODE targets, NOT UIDs. ~GL # P10 uses nicks in user MODE targets, NOT UIDs. ~jlu5
real_target = self.users[target].nick real_target = self.users[target].nick
self.apply_modes(target, modes) self.apply_modes(target, modes)
@ -506,7 +511,7 @@ class P10Protocol(IRCS2SProtocol):
def nick(self, numeric, newnick): def nick(self, numeric, newnick):
"""Changes the nick of a PyLink client.""" """Changes the nick of a PyLink client."""
# <- ABAAA N GL_ 1460753763 # <- ABAAA N jlu5_ 1460753763
if not self.is_internal_client(numeric): if not self.is_internal_client(numeric):
raise LookupError('No such PyLink client exists.') raise LookupError('No such PyLink client exists.')
@ -519,9 +524,15 @@ class P10Protocol(IRCS2SProtocol):
def numeric(self, source, numeric, target, text): def numeric(self, source, numeric, target, text):
"""Sends raw numerics from a server to a remote client. This is used for WHOIS """Sends raw numerics from a server to a remote client. This is used for WHOIS
replies.""" replies."""
# <- AB 311 AyAAA GL ~gl nefarious.midnight.vpn * :realname # <- AB 311 AyAAA jlu5 ~jlu5 nefarious.midnight.vpn * :realname
self._send_with_prefix(source, '%s %s %s' % (numeric, target, text)) self._send_with_prefix(source, '%s %s %s' % (numeric, target, text))
def oper_notice(self, source, text):
"""
Send a message to all opers.
"""
self._send_with_prefix(source, 'WA :%s' % text)
def part(self, client, channel, reason=None): def part(self, client, channel, reason=None):
"""Sends a part from a PyLink client.""" """Sends a part from a PyLink client."""
@ -554,7 +565,7 @@ class P10Protocol(IRCS2SProtocol):
assert not (user == host == '*'), "Refusing to set ridiculous ban on *@*" assert not (user == host == '*'), "Refusing to set ridiculous ban on *@*"
# https://github.com/evilnet/nefarious2/blob/master/doc/p10.txt#L535 # https://github.com/evilnet/nefarious2/blob/master/doc/p10.txt#L535
# <- ABAAA GL * +test@test.host 30 1500300185 1500300215 :haha, you're banned now!!!!1 # <- ABAAA jlu5 * +test@test.host 30 1500300185 1500300215 :haha, you're banned now!!!!1
currtime = int(time.time()) currtime = int(time.time())
if duration == 0 or duration > GLINE_MAX_EXPIRE: if duration == 0 or duration > GLINE_MAX_EXPIRE:
@ -743,7 +754,7 @@ class P10Protocol(IRCS2SProtocol):
def topic(self, source, target, text): def topic(self, source, target, text):
"""Sends a TOPIC change from a PyLink client or server.""" """Sends a TOPIC change from a PyLink client or server."""
# <- ABAAA T #test GL!~gl@nefarious.midnight.vpn 1460852591 1460855795 :blah # <- ABAAA T #test jlu5!~jlu5@nefarious.midnight.vpn 1460852591 1460855795 :blah
# First timestamp is channel creation time, second is current time, # First timestamp is channel creation time, second is current time,
if (not self.is_internal_client(source)) and (not self.is_internal_server(source)): if (not self.is_internal_client(source)) and (not self.is_internal_server(source)):
raise LookupError('No such PyLink client/server exists.') raise LookupError('No such PyLink client/server exists.')
@ -811,6 +822,16 @@ class P10Protocol(IRCS2SProtocol):
### HANDLERS ### HANDLERS
def handle_events(self, data):
"""
Events handler for the P10 protocol. This is mostly the same as RFC1459, with extra handling
for the fact that P10 does not send source numerics prefixed with a ":".
"""
# After the initial PASS & SERVER message, every following message should be prefixed
if self.uplink and not data.startswith(":"):
data = ':' + data
return super().handle_events(data)
def post_connect(self): def post_connect(self):
"""Initializes a connection to a server.""" """Initializes a connection to a server."""
ts = self.start_ts ts = self.start_ts
@ -825,7 +846,7 @@ class P10Protocol(IRCS2SProtocol):
# 4 <link TS> # 4 <link TS>
# 5 <protocol> # 5 <protocol>
# 6 <numeric of new server><max client numeric> # 6 <numeric of new server><max client numeric>
# 7 <flags> <-- Mark ourselves as a service with IPv6 support (+s & +6) -GLolol # 7 <flags> <-- Mark ourselves as a service with IPv6 support (+s & +6) -jlu5
# -1 <description of new server> # -1 <description of new server>
name = self.serverdata["hostname"] name = self.serverdata["hostname"]
@ -914,7 +935,7 @@ class P10Protocol(IRCS2SProtocol):
def handle_nick(self, source, command, args): def handle_nick(self, source, command, args):
"""Handles the NICK command, used for user introductions and nick changes.""" """Handles the NICK command, used for user introductions and nick changes."""
if len(args) > 2: if len(args) > 2:
# <- AB N GL 1 1460673049 ~gl nefarious.midnight.vpn +iw B]AAAB ABAAA :realname # <- AB N jlu5 1 1460673049 ~jlu5 nefarious.midnight.vpn +iw B]AAAB ABAAA :realname
nick = args[0] nick = args[0]
self._check_nick_collision(nick) self._check_nick_collision(nick)
@ -931,6 +952,7 @@ class P10Protocol(IRCS2SProtocol):
ident, host, realname, realhost, ip) ident, host, realname, realhost, ip)
uobj = self.users[uid] = User(self, nick, ts, uid, source, ident, host, realname, realhost, ip) uobj = self.users[uid] = User(self, nick, ts, uid, source, ident, host, realname, realhost, ip)
uobj.ssl = False
self.servers[source].users.add(uid) self.servers[source].users.add(uid)
# https://github.com/evilnet/nefarious2/blob/master/doc/p10.txt#L708 # https://github.com/evilnet/nefarious2/blob/master/doc/p10.txt#L708
@ -948,15 +970,18 @@ class P10Protocol(IRCS2SProtocol):
accountname = modepair[1].split(':', 1)[0] accountname = modepair[1].split(':', 1)[0]
self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}]) self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}])
elif modepair[0][-1] == self.umodes.get('ssl'): # track SSL status where available
uobj.ssl = True
# Call the OPERED UP hook if +o is being added to the mode list. # Call the OPERED UP hook if +o is being added to the mode list.
self._check_oper_status_change(uid, parsedmodes) self._check_oper_status_change(uid, parsedmodes)
self._check_cloak_change(uid) self._check_cloak_change(uid)
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip, 'parse_as': 'UID'} return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip, 'parse_as': 'UID', 'secure': uobj.ssl}
else: else:
# <- ABAAA N GL_ 1460753763 # <- ABAAA N jlu5_ 1460753763
oldnick = self.users[source].nick oldnick = self.users[source].nick
newnick = self.users[source].nick = args[0] newnick = self.users[source].nick = args[0]
@ -1036,7 +1061,7 @@ class P10Protocol(IRCS2SProtocol):
# -> X3 Z Channels.CollectiveIRC.Net 1460745823.89510 0 1460745823.089840 # -> X3 Z Channels.CollectiveIRC.Net 1460745823.89510 0 1460745823.089840
# Arguments of a PONG: our server hostname, the original TS of PING, # Arguments of a PONG: our server hostname, the original TS of PING,
# difference between PING and PONG in seconds, the current TS. # difference between PING and PONG in seconds, the current TS.
# Why is this the way it is? I don't know... -GL # Why is this the way it is? I don't know... -jlu5
target = args[1] target = args[1]
sid = self._get_SID(target) sid = self._get_SID(target)
@ -1212,7 +1237,7 @@ class P10Protocol(IRCS2SProtocol):
def handle_topic(self, source, command, args): def handle_topic(self, source, command, args):
"""Handles TOPIC changes.""" """Handles TOPIC changes."""
# <- ABAAA T #test GL!~gl@nefarious.midnight.vpn 1460852591 1460855795 :blah # <- ABAAA T #test jlu5!~jlu5@nefarious.midnight.vpn 1460852591 1460855795 :blah
channel = args[0] channel = args[0]
topic = args[-1] topic = args[-1]
@ -1265,7 +1290,7 @@ class P10Protocol(IRCS2SProtocol):
target = args[0] target = args[0]
if self.serverdata.get('use_extended_accounts'): if self.serverdata.get('use_extended_accounts'):
# Registration: <- AA AC ABAAA R GL 1459019072 # Registration: <- AA AC ABAAA R jlu5 1459019072
# Logout: <- AA AC ABAAA U # Logout: <- AA AC ABAAA U
# 1 <target user numeric> # 1 <target user numeric>
@ -1274,11 +1299,16 @@ class P10Protocol(IRCS2SProtocol):
# Any other subcommands listed at https://github.com/evilnet/nefarious2/blob/master/doc/p10.txt#L354 # Any other subcommands listed at https://github.com/evilnet/nefarious2/blob/master/doc/p10.txt#L354
# shouldn't apply to us. # shouldn't apply to us.
if args[1] in ('R', 'M'): if args[1] in ('R', 'M'):
accountname = args[2] accountname = args[2]
elif args[1] == 'U': elif args[1] == 'U':
accountname = '' # logout accountname = '' # logout
elif len(args[1]) > 1:
log.warning('(%s) Got subcommand %r for %s in ACCOUNT message, is use_extended_accounts set correctly?',
self.name, args[1], target)
return
else:
return
else: else:
# ircu or nefarious with F:EXTENDED_ACCOUNTS = FALSE # ircu or nefarious with F:EXTENDED_ACCOUNTS = FALSE

View File

@ -2,13 +2,16 @@
ts6.py: PyLink protocol module for TS6-based IRCds (charybdis, elemental-ircd). ts6.py: PyLink protocol module for TS6-based IRCds (charybdis, elemental-ircd).
""" """
import time
import re import re
import time
from pylinkirc import utils, conf from pylinkirc import conf, utils
from pylinkirc.classes import * from pylinkirc.classes import *
from pylinkirc.log import log from pylinkirc.log import log
from pylinkirc.protocols.ts6_common import * from pylinkirc.protocols.ts6_common import TS6BaseProtocol
__all__ = ['TS6Protocol']
class TS6Protocol(TS6BaseProtocol): class TS6Protocol(TS6BaseProtocol):
@ -20,7 +23,7 @@ class TS6Protocol(TS6BaseProtocol):
else 'charybdis') else 'charybdis')
self._ircd = self._ircd.lower() self._ircd = self._ircd.lower()
if self._ircd not in self.SUPPORTED_IRCDS: if self._ircd not in self.SUPPORTED_IRCDS:
log.warning("(%s) Unsupported IRCd %r; falling back to 'charybdis' instead", self.name, target_ircd) log.warning("(%s) Unsupported IRCd %r; falling back to 'charybdis' instead", self.name, self._ircd)
self._ircd = 'charybdis' self._ircd = 'charybdis'
self._can_chghost = False self._can_chghost = False
@ -90,6 +93,19 @@ class TS6Protocol(TS6BaseProtocol):
return u return u
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.
"""
desc = desc or self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']
if self.serverdata.get('hidden', False):
desc = '(H) ' + desc
return super().spawn_server(name, sid=sid, uplink=uplink, desc=desc)
def join(self, client, channel): def join(self, client, channel):
"""Joins a PyLink client to a channel.""" """Joins a PyLink client to a channel."""
# JOIN: # JOIN:
@ -101,6 +117,15 @@ class TS6Protocol(TS6BaseProtocol):
self._channels[channel].users.add(client) self._channels[channel].users.add(client)
self.users[client].channels.add(channel) self.users[client].channels.add(channel)
def oper_notice(self, source, text):
"""
Send a message to all opers.
"""
if self.is_internal_server(source):
# Charybdis TS6 only allows OPERWALL from users
source = self.pseudoclient.uid
self._send_with_prefix(source, 'OPERWALL :%s' % text)
def sjoin(self, server, channel, users, ts=None, modes=set()): def sjoin(self, server, channel, users, ts=None, modes=set()):
"""Sends an SJOIN for a group of users to a channel. """Sends an SJOIN for a group of users to a channel.
@ -301,19 +326,19 @@ class TS6Protocol(TS6BaseProtocol):
'quiet': 'q', 'redirect': 'f', 'freetarget': 'F', 'quiet': 'q', 'redirect': 'f', 'freetarget': 'F',
'joinflood': 'j', 'largebanlist': 'L', 'permanent': 'P', 'joinflood': 'j', 'largebanlist': 'L', 'permanent': 'P',
'noforwards': 'Q', 'stripcolor': 'c', 'allowinvite': 'noforwards': 'Q', 'stripcolor': 'c', 'allowinvite':
'g', 'opmoderated': 'z', 'noctcp': 'C', 'ssl': 'Z', 'g', 'opmoderated': 'z', 'noctcp': 'C',
# charybdis modes provided by extensions # charybdis modes provided by extensions
'operonly': 'O', 'adminonly': 'A', 'sslonly': 'S', 'operonly': 'O', 'adminonly': 'A', 'sslonly': 'S',
'nonotice': 'T', 'nonotice': 'T',
'*A': 'beIq', '*B': 'k', '*C': 'lfj', '*D': 'mnprstFLPQcgzCZOAST' '*A': 'beIq', '*B': 'k', '*C': 'lfj', '*D': 'mnprstFLPQcgzCOAST'
}) })
self.umodes.update({ self.umodes.update({
'deaf': 'D', 'servprotect': 'S', 'admin': 'a', 'deaf': 'D', 'servprotect': 'S', 'admin': 'a',
'invisible': 'i', 'oper': 'o', 'wallops': 'w', 'invisible': 'i', 'oper': 'o', 'wallops': 'w',
'snomask': 's', 'noforward': 'Q', 'regdeaf': 'R', 'snomask': 's', 'noforward': 'Q', 'regdeaf': 'R',
'callerid': 'g', 'operwall': 'z', 'locops': 'l', 'callerid': 'g', 'operwall': 'z', 'locops': 'l',
'cloak': 'x', 'override': 'p', 'cloak': 'x', 'override': 'p', 'ssl': 'Z',
'*A': '', '*B': '', '*C': '', '*D': 'DSaiowsQRgzlxp' '*A': '', '*B': '', '*C': '', '*D': 'DSaiowsQRgzlxpZ'
}) })
# Charybdis extbans # Charybdis extbans
@ -361,7 +386,7 @@ class TS6Protocol(TS6BaseProtocol):
self.cmodes.update(chatircd_cmodes) self.cmodes.update(chatircd_cmodes)
self.cmodes['*D'] += ''.join(chatircd_cmodes.values()) self.cmodes['*D'] += ''.join(chatircd_cmodes.values())
chatircd_umodes = {'netadmin': 'n', 'bot': 'B', 'callerid_sslonly': 't'} chatircd_umodes = {'netadmin': 'n', 'bot': 'B', 'sslonlymsg': 't'}
self.umodes.update(chatircd_umodes) self.umodes.update(chatircd_umodes)
self.umodes['*D'] += ''.join(chatircd_umodes.values()) self.umodes['*D'] += ''.join(chatircd_umodes.values())
@ -397,8 +422,10 @@ class TS6Protocol(TS6BaseProtocol):
# KLN: supports remote KLINEs # KLN: supports remote KLINEs
f('CAPAB :QS ENCAP EX CHW IE KNOCK SAVE SERVICES TB EUID RSFNC EOPMOD SAVETS_100 KLN') f('CAPAB :QS ENCAP EX CHW IE KNOCK SAVE SERVICES TB EUID RSFNC EOPMOD SAVETS_100 KLN')
f('SERVER %s 0 :%s' % (self.serverdata["hostname"], sdesc = self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']
self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc'])) if self.serverdata.get('hidden', False):
sdesc = '(H) ' + sdesc
f('SERVER %s 0 :%s' % (self.serverdata["hostname"], sdesc))
# Finally, end all the initialization with a PING - that's Charybdis' # Finally, end all the initialization with a PING - that's Charybdis'
# way of saying end-of-burst :) # way of saying end-of-burst :)
@ -413,7 +440,7 @@ class TS6Protocol(TS6BaseProtocol):
if args[0] != self.serverdata['recvpass']: if args[0] != self.serverdata['recvpass']:
# Check if recvpass is correct # Check if recvpass is correct
raise ProtocolError('Recvpass from uplink server %s does not match configuration!' % servername) raise ProtocolError('Recvpass from uplink server %r does not match configuration!' % numeric)
if args[1] != 'TS' and args[2] != '6': if args[1] != 'TS' and args[2] != '6':
raise ProtocolError("Remote protocol version is too old! Is this even TS6?") raise ProtocolError("Remote protocol version is too old! Is this even TS6?")
@ -557,7 +584,7 @@ class TS6Protocol(TS6BaseProtocol):
def handle_euid(self, numeric, command, args): def handle_euid(self, numeric, command, args):
"""Handles incoming EUID commands (user introduction).""" """Handles incoming EUID commands (user introduction)."""
# <- :42X EUID GL 1 1437505322 +ailoswz ~gl 127.0.0.1 127.0.0.1 42XAAAAAB * * :realname # <- :42X EUID jlu5 1 1437505322 +ailoswz ~jlu5 127.0.0.1 127.0.0.1 42XAAAAAB * * :realname
nick = args[0] 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, modes, ident, host, ip, uid, realhost, accountname, realname = args[2:11]
@ -587,7 +614,11 @@ class TS6Protocol(TS6BaseProtocol):
if accountname != "*": if accountname != "*":
self.call_hooks([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} # charybdis and derivatives have a usermode (+Z) to mark SSL connections
# ratbox doesn't appear to have this
has_ssl = self.users[uid].ssl = ('+%s' % self.umodes.get('ssl'), None) in parsedmodes
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip, 'secure': has_ssl}
def handle_uid(self, numeric, command, args): def handle_uid(self, numeric, command, args):
"""Handles legacy user introductions (UID).""" """Handles legacy user introductions (UID)."""
@ -626,7 +657,7 @@ class TS6Protocol(TS6BaseProtocol):
return return
# <- :services.int SERVER a.bc 2 :(H) [GL] a # <- :services.int SERVER a.bc 2 :(H) [jlu5] a
return super().handle_server(numeric, command, args) return super().handle_server(numeric, command, args)
def handle_tmode(self, numeric, command, args): def handle_tmode(self, numeric, command, args):
@ -644,7 +675,7 @@ class TS6Protocol(TS6BaseProtocol):
def handle_tb(self, numeric, command, args): def handle_tb(self, numeric, command, args):
"""Handles incoming topic burst (TB) commands.""" """Handles incoming topic burst (TB) commands."""
# <- :42X TB #chat 1467427448 GL!~gl@127.0.0.1 :test # <- :42X TB #chat 1467427448 jlu5!~jlu5@127.0.0.1 :test
channel = args[0] channel = args[0]
ts = args[1] ts = args[1]
setter = args[2] setter = args[2]
@ -655,7 +686,7 @@ class TS6Protocol(TS6BaseProtocol):
def handle_etb(self, numeric, command, args): def handle_etb(self, numeric, command, args):
"""Handles extended topic burst (ETB).""" """Handles extended topic burst (ETB)."""
# <- :00AAAAAAC ETB 0 #test 1470021157 GL :test | abcd # <- :00AAAAAAC ETB 0 #test 1470021157 jlu5 :test | abcd
# Same as TB, with extra TS and extensions arguments. # Same as TB, with extra TS and extensions arguments.
channel = args[1] channel = args[1]
ts = args[2] ts = args[2]
@ -692,7 +723,7 @@ class TS6Protocol(TS6BaseProtocol):
the administrator that certain extensions should be loaded for the best the administrator that certain extensions should be loaded for the best
compatibility. compatibility.
""" """
# <- :charybdis.midnight.vpn 472 GL|devel O :is an unknown mode char to me # <- :charybdis.midnight.vpn 472 jlu5|devel O :is an unknown mode char to me
badmode = args[1] badmode = args[1]
reason = args[-1] reason = args[-1]
setter = args[0] setter = args[0]
@ -709,7 +740,7 @@ class TS6Protocol(TS6BaseProtocol):
""" """
Handles SU, which is used for setting login information. Handles SU, which is used for setting login information.
""" """
# <- :00A ENCAP * SU 42XAAAAAC :GLolol # <- :00A ENCAP * SU 42XAAAAAC :jlu5
# <- :00A ENCAP * SU 42XAAAAAC # <- :00A ENCAP * SU 42XAAAAAC
try: try:
account = args[1] # Account name is being set account = args[1] # Account name is being set
@ -728,7 +759,7 @@ class TS6Protocol(TS6BaseProtocol):
def handle_realhost(self, uid, command, args): def handle_realhost(self, uid, command, args):
"""Handles real host propagation.""" """Handles real host propagation."""
log.debug('(%s) Got REALHOST %s for %s', args[0], uid) log.debug('(%s) Got REALHOST %s for %s', self.name, args[0], uid)
self.users[uid].realhost = args[0] self.users[uid].realhost = args[0]
def handle_login(self, uid, command, args): def handle_login(self, uid, command, args):

View File

@ -5,11 +5,14 @@ ts6_common.py: Common base protocol class with functions shared by the UnrealIRC
import string import string
import time import time
from pylinkirc import utils, structures, conf from pylinkirc import conf, structures
from pylinkirc.classes import * from pylinkirc.classes import *
from pylinkirc.log import log from pylinkirc.log import log
from pylinkirc.protocols.ircs2s_common import * from pylinkirc.protocols.ircs2s_common import *
__all__ = ['TS6BaseProtocol']
class TS6SIDGenerator(): class TS6SIDGenerator():
""" """
TS6 SID Generator. <query> is a 3 character string with any combination of TS6 SID Generator. <query> is a 3 character string with any combination of
@ -84,18 +87,16 @@ class TS6SIDGenerator():
sid = ''.join(self.output) sid = ''.join(self.output)
return sid return sid
class TS6UIDGenerator(IncrementalUIDGenerator): class TS6UIDGenerator(UIDGenerator):
"""Implements an incremental TS6 UID Generator.""" """Implements an incremental TS6 UID Generator."""
def __init__(self, sid): def __init__(self, sid):
# Define the options for IncrementalUIDGenerator, and then
# initialize its functions.
# TS6 UIDs are 6 characters in length (9 including the SID). # TS6 UIDs are 6 characters in length (9 including the SID).
# They go from ABCDEFGHIJKLMNOPQRSTUVWXYZ -> 0123456789 -> wrap around: # They go from ABCDEFGHIJKLMNOPQRSTUVWXYZ -> 0123456789 -> wrap around:
# e.g. AAAAAA, AAAAAB ..., AAAAA8, AAAAA9, AAAABA, etc. # e.g. AAAAAA, AAAAAB ..., AAAAA8, AAAAA9, AAAABA, etc.
self.allowedchars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456879' uidchars = string.ascii_uppercase + string.digits
self.length = 6 length = 6
super().__init__(sid) super().__init__(uidchars, length, sid)
class TS6BaseProtocol(IRCS2SProtocol): class TS6BaseProtocol(IRCS2SProtocol):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -215,7 +216,7 @@ class TS6BaseProtocol(IRCS2SProtocol):
def handle_nick(self, numeric, command, args): def handle_nick(self, numeric, command, args):
"""Handles incoming NICK changes.""" """Handles incoming NICK changes."""
# <- :70MAAAAAA NICK GL-devel 1434744242 # <- :70MAAAAAA NICK jlu5-devel 1434744242
oldnick = self.users[numeric].nick oldnick = self.users[numeric].nick
newnick = self.users[numeric].nick = args[0] newnick = self.users[numeric].nick = args[0]
@ -246,7 +247,7 @@ class TS6BaseProtocol(IRCS2SProtocol):
def handle_server(self, numeric, command, args): def handle_server(self, numeric, command, args):
"""Handles the SERVER command, used for introducing older (TS5) servers.""" """Handles the SERVER command, used for introducing older (TS5) servers."""
# <- :services.int SERVER a.bc 2 :(H) [GL] test jupe # <- :services.int SERVER a.bc 2 :(H) [jlu5] test jupe
servername = args[0].lower() servername = args[0].lower()
sdesc = args[-1] sdesc = args[-1]
self.servers[servername] = Server(self, numeric, servername, desc=sdesc) self.servers[servername] = Server(self, numeric, servername, desc=sdesc)
@ -269,5 +270,5 @@ class TS6BaseProtocol(IRCS2SProtocol):
# This is rewritten to SVSNICK with args ['902AAAAAB', 'Guest53593', '1468299404'] # This is rewritten to SVSNICK with args ['902AAAAAB', 'Guest53593', '1468299404']
# UnrealIRCd: # UnrealIRCd:
# <- :services.midnight.vpn SVSNICK GL Guest87795 1468303726 # <- :services.midnight.vpn SVSNICK jlu5 Guest87795 1468303726
return {'target': self._get_UID(args[0]), 'newnick': args[1]} return {'target': self._get_UID(args[0]), 'newnick': args[1]}

View File

@ -1,16 +1,19 @@
""" """
unreal.py: UnrealIRCd 4.x protocol module for PyLink. unreal.py: UnrealIRCd 4.x-5.x protocol module for PyLink.
""" """
import time
import codecs import codecs
import socket
import re import re
import socket
import time
from pylinkirc import utils, conf from pylinkirc import conf, utils
from pylinkirc.classes import * from pylinkirc.classes import *
from pylinkirc.log import log from pylinkirc.log import log
from pylinkirc.protocols.ts6_common import * from pylinkirc.protocols.ts6_common import TS6BaseProtocol
__all__ = ['UnrealProtocol']
SJOIN_PREFIXES = {'q': '*', 'a': '~', 'o': '@', 'h': '%', 'v': '+', 'b': '&', 'e': '"', 'I': "'"} SJOIN_PREFIXES = {'q': '*', 'a': '~', 'o': '@', 'h': '%', 'v': '+', 'b': '&', 'e': '"', 'I': "'"}
@ -18,16 +21,70 @@ class UnrealProtocol(TS6BaseProtocol):
# I'm not sure what the real limit is, but the text posted at # 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/jlu5/PyLink/issues/378 suggests 427 characters.
# https://github.com/unrealircd/unrealircd/blob/4cad9cb/src/modules/m_server.c#L1260 may # https://github.com/unrealircd/unrealircd/blob/4cad9cb/src/modules/m_server.c#L1260 may
# also help. (but why BUFSIZE-*80*?) -GL # also help. (but why BUFSIZE-*80*?) -jlu5
S2S_BUFSIZE = 427 S2S_BUFSIZE = 427
_KNOWN_CMODES = {'ban': 'b',
'banexception': 'e',
'blockcolor': 'c',
'censor': 'G',
'delayjoin': 'D',
'flood_unreal': 'f',
'invex': 'I',
'inviteonly': 'i',
'issecure': 'Z',
'key': 'k',
'limit': 'l',
'moderated': 'm',
'noctcp': 'C',
'noextmsg': 'n',
'noinvite': 'V',
'nokick': 'Q',
'noknock': 'K',
'nonick': 'N',
'nonotice': 'T',
'op': 'o',
'operonly': 'O',
'permanent': 'P',
'private': 'p',
'registered': 'r',
'regmoderated': 'M',
'regonly': 'R',
'secret': 's',
'sslonly': 'z',
'stripcolor': 'S',
'topiclock': 't',
'voice': 'v'}
_KNOWN_UMODES = {'bot': 'B',
'censor': 'G',
'cloak': 'x',
'deaf': 'd',
'filter': 'G',
'hidechans': 'p',
'hideidle': 'I',
'hideoper': 'H',
'invisible': 'i',
'noctcp': 'T',
'protected': 'q',
'regdeaf': 'R',
'registered': 'r',
'sslonlymsg': 'Z',
'servprotect': 'S',
'showwhois': 'W',
'snomask': 's',
'ssl': 'z',
'vhost': 't',
'wallops': 'w'}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.protocol_caps |= {'slash-in-nicks', 'underscore-in-hosts'} self.protocol_caps |= {'slash-in-nicks', 'underscore-in-hosts', 'slash-in-hosts'}
# Set our case mapping (rfc1459 maps "\" and "|" together, for example) # Set our case mapping (rfc1459 maps "\" and "|" together, for example)
self.casemapping = 'ascii' self.casemapping = 'ascii'
self.proto_ver = 4017
# Unreal protocol version
self.proto_ver = 4203
self.min_proto_ver = 4000 self.min_proto_ver = 4000
self.hook_map = {'UMODE2': 'MODE', 'SVSKILL': 'KILL', 'SVSMODE': 'MODE', self.hook_map = {'UMODE2': 'MODE', 'SVSKILL': 'KILL', 'SVSMODE': 'MODE',
'SVS2MODE': 'MODE', 'SJOIN': 'JOIN', 'SETHOST': 'CHGHOST', 'SVS2MODE': 'MODE', 'SJOIN': 'JOIN', 'SETHOST': 'CHGHOST',
'SETIDENT': 'CHGIDENT', 'SETNAME': 'CHGNAME', 'SETIDENT': 'CHGIDENT', 'SETNAME': 'CHGNAME',
@ -92,7 +149,7 @@ class UnrealProtocol(TS6BaseProtocol):
# Now, strip the trailing \n and decode into a string again. # Now, strip the trailing \n and decode into a string again.
encoded_ip = encoded_ip.strip().decode() encoded_ip = encoded_ip.strip().decode()
# <- :001 UID GL 0 1441306929 gl localhost 0018S7901 0 +iowx * midnight-1C620195 fwAAAQ== :realname # <- :001 UID jlu5 0 1441306929 jlu5 localhost 0018S7901 0 +iowx * midnight-1C620195 fwAAAQ== :realname
self._send_with_prefix(server, "UID {nick} {hopcount} {ts} {ident} {realhost} {uid} 0 {modes} " self._send_with_prefix(server, "UID {nick} {hopcount} {ts} {ident} {realhost} {uid} 0 {modes} "
"{host} * {ip} :{realname}".format(ts=ts, host=host, "{host} * {ip} :{realname}".format(ts=ts, host=host,
nick=nick, ident=ident, uid=uid, nick=nick, ident=ident, uid=uid,
@ -106,9 +163,14 @@ class UnrealProtocol(TS6BaseProtocol):
"""Joins a PyLink client to a channel.""" """Joins a PyLink client to a channel."""
if not self.is_internal_client(client): if not self.is_internal_client(client):
raise LookupError('No such PyLink client exists.') raise LookupError('No such PyLink client exists.')
self._send_with_prefix(client, "JOIN %s" % channel)
self._channels[channel].users.add(client) # Forward this on to SJOIN, as using JOIN in Unreal S2S seems to cause TS corruption bugs.
self.users[client].channels.add(channel) # This seems to be what Unreal itself does anyways.
if channel not in self.channels:
prefix = 'o' # Create new channels with the first joiner as op
else:
prefix = ''
self.sjoin(self.sid, channel, [(prefix, client)])
def sjoin(self, server, channel, users, ts=None, modes=set()): def sjoin(self, server, channel, users, ts=None, modes=set()):
"""Sends an SJOIN for a group of users to a channel. """Sends an SJOIN for a group of users to a channel.
@ -197,7 +259,7 @@ class UnrealProtocol(TS6BaseProtocol):
Sends mode changes from a PyLink client/server. The mode list should be 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.parse_modes() output. a list of (mode, arg) tuples, i.e. the format of utils.parse_modes() output.
""" """
# <- :unreal.midnight.vpn MODE #test +ntCo GL 1444361345 # <- :unreal.midnight.vpn MODE #test +ntCo jlu5 1444361345
if (not self.is_internal_client(numeric)) and \ if (not self.is_internal_client(numeric)) and \
(not self.is_internal_server(numeric)): (not self.is_internal_server(numeric)):
@ -206,12 +268,8 @@ class UnrealProtocol(TS6BaseProtocol):
self.apply_modes(target, modes) self.apply_modes(target, modes)
if self.is_channel(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 modes = list(modes) # Needed for indexing
# Make sure we expand any PUIDs when sending outgoing modes... # Make sure we expand any PUIDs when sending outgoing modes...
for idx, mode in enumerate(modes): for idx, mode in enumerate(modes):
if mode[0][-1] in self.prefixmodes: if mode[0][-1] in self.prefixmodes:
@ -254,6 +312,12 @@ class UnrealProtocol(TS6BaseProtocol):
joinedmodes = self.join_modes(modes) joinedmodes = self.join_modes(modes)
self._send_with_prefix(target, 'UMODE2 %s' % joinedmodes) self._send_with_prefix(target, 'UMODE2 %s' % joinedmodes)
def oper_notice(self, source, text):
"""
Send a message to all opers.
"""
self._send_with_prefix(source, 'GLOBOPS :%s' % text)
def set_server_ban(self, source, duration, user='*', host='*', reason='User banned'): def set_server_ban(self, source, duration, user='*', host='*', reason='User banned'):
""" """
Sets a server ban. Sets a server ban.
@ -317,11 +381,21 @@ class UnrealProtocol(TS6BaseProtocol):
self.call_hooks([self.sid, 'CHGNAME', self.call_hooks([self.sid, 'CHGNAME',
{'target': target, 'newgecos': text}]) {'target': target, 'newgecos': text}])
def kill(self, source, target, reason):
"""Sends a kill 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, 'KILL %s :%s' % (target, reason))
self._remove_client(target)
def knock(self, numeric, target, text): def knock(self, numeric, target, text):
"""Sends a KNOCK from a PyLink client.""" """Sends a KNOCK from a PyLink client."""
# KNOCKs in UnrealIRCd are actually just specially formatted NOTICEs, # KNOCKs in UnrealIRCd are actually just specially formatted NOTICEs,
# sent to all ops in a channel. # sent to all ops in a channel.
# <- :unreal.midnight.vpn NOTICE @#test :[Knock] by GL|!gl@hidden-1C620195 (test) # <- :unreal.midnight.vpn NOTICE @#test :[Knock] by jlu5|!jlu5@hidden-1C620195 (test)
assert self.is_channel(target), "Can only knock on channels!" assert self.is_channel(target), "Can only knock on channels!"
sender = self.get_server(numeric) sender = self.get_server(numeric)
s = '[Knock] by %s (%s)' % (self.get_hostmask(numeric), text) s = '[Knock] by %s (%s)' % (self.get_hostmask(numeric), text)
@ -337,15 +411,6 @@ class UnrealProtocol(TS6BaseProtocol):
# Track usages of legacy (Unreal 3.2) nicks. # Track usages of legacy (Unreal 3.2) nicks.
self.legacy_uidgen = PUIDGenerator('U32user') self.legacy_uidgen = PUIDGenerator('U32user')
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',
'filter': 'G', 'hideoper': 'H', 'hideidle': 'I',
'regdeaf': 'R', 'servprotect': 'S',
'noctcp': 'T', 'showwhois': 'W',
'*A': '', '*B': '', '*C': '', '*D': 'dipqrstwBxzGHIRSTW'})
f = self.send f = self.send
host = self.serverdata["hostname"] host = self.serverdata["hostname"]
@ -368,17 +433,32 @@ class UnrealProtocol(TS6BaseProtocol):
# not work for any UnrealIRCd 3.2 users. # not work for any UnrealIRCd 3.2 users.
# ESVID - Supports account names in services stamps instead of just the signon time. # ESVID - Supports account names in services stamps instead of just the signon time.
# AFAIK this doesn't actually affect services' behaviour? # 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.serverdata["hostname"], self.sid)) # EXTSWHOIS - support multiple SWHOIS lines (purely informational for us)
f('PROTOCTL SJOIN SJ3 NOQUIT NICKv2 VL UMODE2 PROTOCTL NICKIP EAUTH=%s SID=%s VHP ESVID EXTSWHOIS' % (self.serverdata["hostname"], self.sid))
sdesc = self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc'] 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('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') self._send_with_prefix(self.sid, 'EOS')
# Extban definitions # Extban definitions
self.extbans_acting = {'quiet': '~q:', 'ban_nonick': '~n:', 'ban_nojoins': '~j:', self.extbans_acting = {'quiet': '~q:',
'filter': '~T:block:', 'filter_censor': '~T:censor:'} 'ban_nonick': '~n:',
self.extbans_matching = {'ban_account': '~a:', 'ban_inchannel': '~c:', 'ban_opertype': '~O:', 'ban_nojoins': '~j:',
'ban_realname': '~r:', 'ban_account_legacy': '~R:', 'ban_certfp': '~S:'} 'filter': '~T:block:',
'filter_censor': '~T:censor:',
'msgbypass_external': '~m:external:',
'msgbypass_censor': '~m:censor:',
'msgbypass_moderated': '~m:moderated:',
# These two sort of map to InspIRCd +e S: and +e T:
'ban_stripcolor': '~m:color:',
'ban_nonotice': '~m:notice:',
'timedban_unreal': '~t:'}
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): def handle_eos(self, numeric, command, args):
"""EOS is used to denote end of burst.""" """EOS is used to denote end of burst."""
@ -388,8 +468,8 @@ class UnrealProtocol(TS6BaseProtocol):
return {} return {}
def handle_uid(self, numeric, command, args): def handle_uid(self, numeric, command, args):
# <- :001 UID GL 0 1441306929 gl localhost 0018S7901 0 +iowx * midnight-1C620195 fwAAAQ== :realname # <- :001 UID jlu5 0 1441306929 jlu5 localhost 0018S7901 0 +iowx * midnight-1C620195 fwAAAQ== :realname
# <- :001 UID GL| 0 1441389007 gl 10.120.0.6 001ZO8F03 0 +iwx * 391A9CB9.26A16454.D9847B69.IP CngABg== :realname # <- :001 UID jlu5| 0 1441389007 jlu5 10.120.0.6 001ZO8F03 0 +iwx * 391A9CB9.26A16454.D9847B69.IP CngABg== :realname
# arguments: nick, hopcount?, ts, ident, real-host, UID, services account (0 if none), modes, # arguments: nick, hopcount?, ts, ident, real-host, UID, services account (0 if none), modes,
# displayed host, cloaked (+x) host, base64-encoded IP, and realname # displayed host, cloaked (+x) host, base64-encoded IP, and realname
nick = args[0] nick = args[0]
@ -444,13 +524,16 @@ class UnrealProtocol(TS6BaseProtocol):
if ('+r', None) in parsedmodes and accountname.isdigit(): if ('+r', None) in parsedmodes and accountname.isdigit():
accountname = nick accountname = nick
# Track SSL/TLS status
has_ssl = self.users[uid].ssl = ('+z', None) in parsedmodes
if not accountname.isdigit(): if not accountname.isdigit():
self.call_hooks([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 # parse_as is used here to prevent legacy user introduction from being confused
# with a nick change. # with a nick change.
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host,
'ident': ident, 'ip': ip, 'parse_as': 'UID'} 'ident': ident, 'ip': ip, 'parse_as': 'UID', 'secure': has_ssl}
def handle_pass(self, numeric, command, args): def handle_pass(self, numeric, command, args):
# <- PASS :abcdefg # <- PASS :abcdefg
@ -476,7 +559,7 @@ class UnrealProtocol(TS6BaseProtocol):
sdesc = args[-1].split(" ", 1) sdesc = args[-1].split(" ", 1)
# Get our protocol version. I really don't know why the version and the server # Get our protocol version. I really don't know why the version and the server
# description aren't two arguments instead of one... -GLolol # description aren't two arguments instead of one... -jlu5
vline = sdesc[0].split('-', 1) vline = sdesc[0].split('-', 1)
sdesc = " ".join(sdesc[1:]) sdesc = " ".join(sdesc[1:])
@ -492,28 +575,30 @@ class UnrealProtocol(TS6BaseProtocol):
"(Unreal 4.x), got %s)" % (self.min_proto_ver, protover)) "(Unreal 4.x), got %s)" % (self.min_proto_ver, protover))
self.servers[numeric] = Server(self, None, sname, desc=sdesc) self.servers[numeric] = Server(self, None, sname, desc=sdesc)
# Prior to 4203, Unreal did not send PROTOCTL USERMODES (see handle_protoctl() )
if protover < 4203:
self.umodes.update(self._KNOWN_UMODES)
self.umodes['*D'] = ''.join(self._KNOWN_UMODES.values())
else: else:
# Legacy (non-SID) servers can still be introduced using the SERVER command. # Legacy (non-SID) servers can still be introduced using the SERVER command.
# <- :services.int SERVER a.bc 2 :(H) [GL] a # <- :services.int SERVER a.bc 2 :(H) [jlu5] a
return super().handle_server(numeric, command, args) return super().handle_server(numeric, command, args)
def handle_protoctl(self, numeric, command, args): def handle_protoctl(self, numeric, command, args):
"""Handles protocol negotiation.""" """Handles protocol negotiation."""
cmodes = {'noknock': 'K', 'limit': 'l', 'registered': 'r', 'flood_unreal': 'f',
'censor': 'G', 'noextmsg': 'n', 'invex': 'I', 'permanent': 'P',
'sslonly': 'z', 'operonly': 'O', 'moderated': 'm', 'blockcolor': 'c',
'regmoderated': 'M', 'noctcp': 'C', 'secret': 's', 'ban': 'b',
'nokick': 'Q', 'private': 'p', 'stripcolor': 'S', 'key': 'k',
'op': 'o', 'voice': 'v', 'regonly': 'R', 'noinvite': 'V',
'banexception': 'e', 'nonick': 'N', 'issecure': 'Z', 'topiclock': 't',
'nonotice': 'T', 'delayjoin': 'D', 'inviteonly': 'i'}
# Make a list of all our capability names. # Make a list of all our capability names.
self.caps += [arg.split('=')[0] for arg in args] self.caps += [arg.split('=')[0] for arg in args]
# Unreal 4.0.x:
# <- PROTOCTL NOQUIT NICKv2 SJOIN SJOIN2 UMODE2 VL SJ3 TKLEXT TKLEXT2 NICKIP ESVID # <- PROTOCTL NOQUIT NICKv2 SJOIN SJOIN2 UMODE2 VL SJ3 TKLEXT TKLEXT2 NICKIP ESVID
# <- PROTOCTL CHANMODES=beI,k,l,psmntirzMQNRTOVKDdGPZSCc NICKCHARS= SID=001 MLOCK TS=1441314501 EXTSWHOIS # <- PROTOCTL CHANMODES=beI,k,l,psmntirzMQNRTOVKDdGPZSCc NICKCHARS= SID=001 MLOCK TS=1441314501 EXTSWHOIS
# Unreal 4.2.x:
# <- PROTOCTL NOQUIT NICKv2 SJOIN SJOIN2 UMODE2 VL SJ3 TKLEXT TKLEXT2 NICKIP ESVID SJSBY
# <- PROTOCTL CHANMODES=beI,kLf,l,psmntirzMQNRTOVKDdGPZSCc USERMODES=iowrsxzdHtIDZRqpWGTSB BOOTED=1574014839 PREFIX=(qaohv)~&@%+ NICKCHARS= SID=001 MLOCK TS=1574020869 EXTSWHOIS
# Unreal 5.0.0-rc1:
# <- PROTOCTL NOQUIT NICKv2 SJOIN SJOIN2 UMODE2 VL SJ3 TKLEXT TKLEXT2 NICKIP ESVID SJSBY MTAGS
# <- PROTOCTL CHANMODES=beI,kLf,lH,psmntirzMQNRTOVKDdGPZSCc USERMODES=iowrsxzdHtIDZRqpWGTSB BOOTED=1574020755 PREFIX=(qaohv)~&@%+ SID=001 MLOCK TS=1574020823 EXTSWHOIS
# <- PROTOCTL NICKCHARS= CHANNELCHARS=utf8
for cap in args: for cap in args:
if cap.startswith('SID'): if cap.startswith('SID'):
self.uplink = cap.split('=', 1)[1] self.uplink = cap.split('=', 1)[1]
@ -521,10 +606,14 @@ class UnrealProtocol(TS6BaseProtocol):
# Parse all the supported channel modes. # Parse all the supported channel modes.
supported_cmodes = cap.split('=', 1)[1] supported_cmodes = cap.split('=', 1)[1]
self.cmodes['*A'], self.cmodes['*B'], self.cmodes['*C'], self.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(): for namedmode, modechar in self._KNOWN_CMODES.items():
if modechar in supported_cmodes: if modechar in supported_cmodes:
self.cmodes[namedmode] = modechar self.cmodes[namedmode] = modechar
self.cmodes['*B'] += 'f' # Add +f to the list too, dunno why it isn't there. elif cap.startswith('USERMODES'): # Only for protover >= 4203
self.umodes['*D'] = supported_umodes = cap.split('=', 1)[1]
for namedmode, modechar in self._KNOWN_UMODES.items():
if modechar in supported_umodes:
self.umodes[namedmode] = modechar
# Add in the supported prefix modes. # Add in the supported prefix modes.
self.cmodes.update({'halfop': 'h', 'admin': 'a', 'owner': 'q', self.cmodes.update({'halfop': 'h', 'admin': 'a', 'owner': 'q',
@ -532,7 +621,7 @@ class UnrealProtocol(TS6BaseProtocol):
def handle_join(self, numeric, command, args): def handle_join(self, numeric, command, args):
"""Handles the UnrealIRCd JOIN command.""" """Handles the UnrealIRCd JOIN command."""
# <- :GL JOIN #pylink,#test # <- :jlu5 JOIN #pylink,#test
if args[0] == '0': if args[0] == '0':
# /join 0; part the user from all channels # /join 0; part the user from all channels
oldchans = self.users[numeric].channels.copy() oldchans = self.users[numeric].channels.copy()
@ -603,6 +692,11 @@ class UnrealProtocol(TS6BaseProtocol):
continue continue
user = self._get_UID(user) # Normalize nicks to UIDs for Unreal 3.2 links user = self._get_UID(user) # Normalize nicks to UIDs for Unreal 3.2 links
if user not in self.users:
# Work around a potential race when sending kills on join
log.debug("(%s) Ignoring user %s in SJOIN to %s, they don't exist anymore", self.name, user, channel)
continue
# Unreal uses slightly different prefixes in SJOIN. +q is * instead of ~, # Unreal uses slightly different prefixes in SJOIN. +q is * instead of ~,
# and +a is ~ instead of &. # and +a is ~ instead of &.
modeprefix = (r.group(1) or '').replace("~", "&").replace("*", "~") modeprefix = (r.group(1) or '').replace("~", "&").replace("*", "~")
@ -641,9 +735,9 @@ class UnrealProtocol(TS6BaseProtocol):
# <- NICK Global 3 1456843578 services novernet.com services.novernet.com 0 +ioS * :Global Noticer # <- NICK Global 3 1456843578 services novernet.com services.novernet.com 0 +ioS * :Global Noticer
# & nick hopcount timestamp username hostname server service-identifier-token :realname # & nick hopcount timestamp username hostname server service-identifier-token :realname
# With NICKIP and VHP enabled: # With NICKIP and VHP enabled:
# <- NICK GL32 2 1470699865 gl localhost unreal32.midnight.vpn GL +iowx hidden-1C620195 AAAAAAAAAAAAAAAAAAAAAQ== :realname # <- NICK legacy32 2 1470699865 jlu5 localhost unreal32.midnight.vpn jlu5 +iowx hidden-1C620195 AAAAAAAAAAAAAAAAAAAAAQ== :realname
# to this: # to this:
# <- :001 UID GL 0 1441306929 gl localhost 0018S7901 0 +iowx * hidden-1C620195 fwAAAQ== :realname # <- :001 UID jlu5 0 1441306929 jlu5 localhost 0018S7901 0 +iowx * hidden-1C620195 fwAAAQ== :realname
log.debug('(%s) got legacy NICK args: %s', self.name, ' '.join(args)) log.debug('(%s) got legacy NICK args: %s', self.name, ' '.join(args))
new_args = args[:] # Clone the old args list new_args = args[:] # Clone the old args list
@ -665,15 +759,15 @@ class UnrealProtocol(TS6BaseProtocol):
return self.handle_uid(servername, 'UID_LEGACY', new_args) return self.handle_uid(servername, 'UID_LEGACY', new_args)
else: else:
# Normal NICK change, just let ts6_common handle it. # Normal NICK change, just let ts6_common handle it.
# :70MAAAAAA NICK GL-devel 1434744242 # :70MAAAAAA NICK jlu5-devel 1434744242
return super().handle_nick(numeric, command, args) return super().handle_nick(numeric, command, args)
def handle_mode(self, numeric, command, args): def handle_mode(self, numeric, command, args):
# <- :unreal.midnight.vpn MODE #test +bb test!*@* *!*@bad.net # <- :unreal.midnight.vpn MODE #test +bb test!*@* *!*@bad.net
# <- :unreal.midnight.vpn MODE #test +q GL 1444361345 # <- :unreal.midnight.vpn MODE #test +q jlu5 1444361345
# <- :unreal.midnight.vpn MODE #test +ntCo GL 1444361345 # <- :unreal.midnight.vpn MODE #test +ntCo jlu5 1444361345
# <- :unreal.midnight.vpn MODE #test +mntClfo 5 [10t]:5 GL 1444361345 # <- :unreal.midnight.vpn MODE #test +mntClfo 5 [10t]:5 jlu5 1444361345
# <- :GL MODE #services +v GL # <- :jlu5 MODE #services +v jlu5
# This seems pretty relatively inconsistent - why do some commands have a TS at the end while others don't? # This seems pretty relatively inconsistent - why do some commands have a TS at the end while others don't?
# Answer: the first syntax (MODE sent by SERVER) is used for channel bursts - according to Unreal 3.2 docs, # Answer: the first syntax (MODE sent by SERVER) is used for channel bursts - according to Unreal 3.2 docs,
@ -762,30 +856,30 @@ class UnrealProtocol(TS6BaseProtocol):
# which is supported by atheme and Anope 2.x). # which is supported by atheme and Anope 2.x).
# Logging in (with account info, atheme): # Logging in (with account info, atheme):
# <- :NickServ SVS2MODE GL +rd GL # <- :NickServ SVS2MODE jlu5 +rd jlu5
# Logging in (without account info, anope 2.0?): # Logging in (without account info, anope 2.0?):
# <- :NickServ SVS2MODE 001WCO6YK +r # <- :NickServ SVS2MODE 001WCO6YK +r
# Logging in (without account info, anope 1.8): # Logging in (without account info, anope 1.8):
# Note: ignore the timestamp. # Note: ignore the timestamp.
# <- :services.abc.net SVS2MODE GLolol +rd 1470696723 # <- :services.abc.net SVS2MODE jlu5 +rd 1470696723
# Logging out (atheme): # Logging out (atheme):
# <- :NickServ SVS2MODE GL -r+d 0 # <- :NickServ SVS2MODE jlu5 -r+d 0
# Logging out (anope 1.8): # Logging out (anope 1.8):
# <- :services.abc.net SVS2MODE GLolol -r+d 1 # <- :services.abc.net SVS2MODE jlu5 -r+d 1
# Logging out (anope 2.0): # Logging out (anope 2.0):
# <- :NickServ SVS2MODE 009EWLA03 -r # <- :NickServ SVS2MODE 009EWLA03 -r
# Logging in to account from a different nick (atheme): # Logging in to account from a different nick (atheme):
# Note: no +r is being set. # Note: no +r is being set.
# <- :NickServ SVS2MODE somenick +d GL # <- :NickServ SVS2MODE somenick +d jlu5
# Logging in to account from a different nick (anope): # Logging in to account from a different nick (anope):
# <- :NickServ SVS2MODE 001SALZ01 +d GL # <- :NickServ SVS2MODE 001SALZ01 +d jlu5
# <- :NickServ SVS2MODE 001SALZ01 +r # <- :NickServ SVS2MODE 001SALZ01 +r
target = self._get_UID(args[0]) target = self._get_UID(args[0])
@ -843,13 +937,13 @@ class UnrealProtocol(TS6BaseProtocol):
def handle_umode2(self, source, command, args): def handle_umode2(self, source, command, args):
"""Handles UMODE2, used to set user modes on oneself.""" """Handles UMODE2, used to set user modes on oneself."""
# <- :GL UMODE2 +W # <- :jlu5 UMODE2 +W
target = self._get_UID(source) target = self._get_UID(source)
return self._handle_umode(target, self.parse_modes(target, args)) return self._handle_umode(target, self.parse_modes(target, args))
def handle_topic(self, numeric, command, args): def handle_topic(self, numeric, command, args):
"""Handles the TOPIC command.""" """Handles the TOPIC command."""
# <- GL TOPIC #services GL 1444699395 :weeee # <- jlu5 TOPIC #services jlu5 1444699395 :weeee
# <- TOPIC #services devel.relay 1452399682 :test # <- TOPIC #services devel.relay 1452399682 :test
channel = args[0] channel = args[0]
topic = args[-1] topic = args[-1]
@ -886,17 +980,32 @@ class UnrealProtocol(TS6BaseProtocol):
self.users[numeric].realname = newgecos = args[0] self.users[numeric].realname = newgecos = args[0]
return {'target': numeric, 'newgecos': newgecos} return {'target': numeric, 'newgecos': newgecos}
def handle_chgident(self, numeric, command, args): def handle_chgident(self, source, command, args):
"""Handles CHGIDENT, used for denoting ident changes.""" """Handles CHGIDENT, used for denoting ident changes."""
# <- :GL CHGIDENT GL test # <- :jlu5 CHGIDENT jlu5 test
target = self._get_UID(args[0]) target = self._get_UID(args[0])
# Bounce attempts to change fields of protected PyLink clients
if self.is_internal_client(target):
log.warning("(%s) Bouncing attempt from %s to change ident of PyLink client %s",
self.name, self.get_friendly_name(source), self.get_friendly_name(target))
self.update_client(target, 'IDENT', self.users[target].ident)
return
self.users[target].ident = newident = args[1] self.users[target].ident = newident = args[1]
return {'target': target, 'newident': newident} return {'target': target, 'newident': newident}
def handle_chghost(self, numeric, command, args): def handle_chghost(self, source, command, args):
"""Handles CHGHOST, used for denoting hostname changes.""" """Handles CHGHOST, used for denoting hostname changes."""
# <- :GL CHGHOST GL some.host # <- :jlu5 CHGHOST jlu5 some.host
target = self._get_UID(args[0]) target = self._get_UID(args[0])
# Bounce attempts to change fields of protected PyLink clients
if self.is_internal_client(target):
log.warning("(%s) Bouncing attempt from %s to change host of PyLink client %s",
self.name, self.get_friendly_name(source), self.get_friendly_name(target))
self.update_client(target, 'HOST', self.users[target].host)
return
self.users[target].host = newhost = args[1] self.users[target].host = newhost = args[1]
# When SETHOST or CHGHOST is used, modes +xt are implicitly set on the # When SETHOST or CHGHOST is used, modes +xt are implicitly set on the
@ -905,16 +1014,23 @@ class UnrealProtocol(TS6BaseProtocol):
return {'target': target, 'newhost': newhost} return {'target': target, 'newhost': newhost}
def handle_chgname(self, numeric, command, args): def handle_chgname(self, source, command, args):
"""Handles CHGNAME, used for denoting real name/gecos changes.""" """Handles CHGNAME, used for denoting real name/gecos changes."""
# <- :GL CHGNAME GL :afdsafasf # <- :jlu5 CHGNAME jlu5 :afdsafasf
target = self._get_UID(args[0]) target = self._get_UID(args[0])
# Bounce attempts to change fields of protected PyLink clients
if self.is_internal_client(target):
log.warning("(%s) Bouncing attempt from %s to change gecos of PyLink client %s",
self.name, self.get_friendly_name(source), self.get_friendly_name(target))
self.update_client(target, 'REALNAME', self.users[target].realname)
return
self.users[target].realname = newgecos = args[1] self.users[target].realname = newgecos = args[1]
return {'target': target, 'newgecos': newgecos} return {'target': target, 'newgecos': newgecos}
def handle_tsctl(self, source, command, args): def handle_tsctl(self, source, command, args):
"""Handles /TSCTL alltime requests.""" """Handles /TSCTL alltime requests."""
# <- :GL TSCTL alltime # <- :jlu5 TSCTL alltime
if args[0] == 'alltime': if args[0] == 'alltime':
self._send_with_prefix(self.sid, 'NOTICE %s :*** Server=%s time()=%d' % (source, self.hostname(), time.time())) self._send_with_prefix(self.sid, 'NOTICE %s :*** Server=%s time()=%d' % (source, self.hostname(), time.time()))

1
pylink
View File

@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys import sys
try: try:
from pylinkirc import launcher from pylinkirc import launcher
except ImportError: except ImportError:

View File

@ -3,9 +3,10 @@
Password hashing utility for PyLink IRC Services. Password hashing utility for PyLink IRC Services.
""" """
from pylinkirc.coremods.login import pwd_context
import getpass import getpass
from pylinkirc.coremods.login import pwd_context
if __name__ == '__main__': if __name__ == '__main__':
import argparse import argparse
@ -23,4 +24,4 @@ if __name__ == '__main__':
password = password.strip() password = password.strip()
assert password, "Password cannot be empty!" assert password, "Password cannot be empty!"
print(pwd_context.encrypt(password)) print(pwd_context.hash(password))

4
requirements-docker.txt Normal file
View File

@ -0,0 +1,4 @@
cachetools
passlib
pyyaml
setuptools

View File

@ -9,6 +9,9 @@ import threading
from pylinkirc import world from pylinkirc import world
from pylinkirc.log import log from pylinkirc.log import log
__all__ = ['register', 'unregister', 'start']
SELECT_TIMEOUT = 0.5 SELECT_TIMEOUT = 0.5
selector = selectors.DefaultSelector() selector = selectors.DefaultSelector()

View File

@ -1,16 +1,16 @@
"""Setup module for PyLink IRC Services.""" """Setup module for PyLink IRC Services."""
import subprocess
import sys import sys
from codecs import open
if sys.version_info < (3, 4): if sys.version_info < (3, 7):
raise RuntimeError("PyLink requires Python 3.4 or higher.") raise RuntimeError("PyLink requires Python 3.7 or higher.")
try: try:
from setuptools import setup, find_packages from setuptools import setup, find_packages
except ImportError: except ImportError:
raise ImportError("Please install Setuptools and try again.") raise ImportError("Please install Setuptools and try again.")
from codecs import open
import subprocess
with open('VERSION', encoding='utf-8') as f: with open('VERSION', encoding='utf-8') as f:
version = f.read().strip() version = f.read().strip()
@ -70,24 +70,21 @@ setup(
'Natural Language :: English', 'Natural Language :: English',
'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
], ],
keywords='IRC services relay', keywords='IRC services relay',
install_requires=['pyyaml', 'ircmatch'], install_requires=['pyyaml', 'cachetools'],
extras_require={ extras_require={
'password-hashing': ['passlib'], 'password-hashing': ['passlib>=1.7.0'],
'cron-support': ['psutil'], 'cron-support': ['psutil'],
'servprotect': ['expiringdict>=1.1.4'], 'relay-unicode': ['unidecode'],
}, },
dependency_links=[
"git+https://github.com/mailgun/expiringdict.git@v1.1.4#egg=expiringdict-1.1.4"
],
# Folders (packages of code) # Folders (packages of code)
packages=['pylinkirc', 'pylinkirc.protocols', 'pylinkirc.plugins', 'pylinkirc.coremods'], packages=['pylinkirc', 'pylinkirc.protocols', 'pylinkirc.plugins', 'pylinkirc.coremods'],

View File

@ -7,14 +7,21 @@ This module contains custom data structures that may be useful in various situat
import collections import collections
import collections.abc import collections.abc
import json import json
import pickle
import os import os
import pickle
import string
import threading import threading
from copy import copy, deepcopy from copy import copy, deepcopy
import string
from .log import log
from . import conf from . import conf
from .log import log
__all__ = ['KeyedDefaultdict', 'CopyWrapper', 'CaseInsensitiveFixedSet',
'CaseInsensitiveDict', 'IRCCaseInsensitiveDict',
'CaseInsensitiveSet', 'IRCCaseInsensitiveSet',
'CamelCaseToSnakeCase', 'DataStore', 'JSONDataStore',
'PickleDataStore']
_BLACKLISTED_COPY_TYPES = [] _BLACKLISTED_COPY_TYPES = []
@ -25,7 +32,7 @@ class KeyedDefaultdict(collections.defaultdict):
def __missing__(self, key): def __missing__(self, key):
if self.default_factory is None: if self.default_factory is None:
# If there is no default factory, just let defaultdict handle it # If there is no default factory, just let defaultdict handle it
super().__missing__(self, key) super().__missing__(key)
else: else:
value = self[key] = self.default_factory(key) value = self[key] = self.default_factory(key)
return value return value
@ -42,11 +49,11 @@ class CopyWrapper():
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
"""Returns a deep copy of the channel object.""" """Returns a deep copy of the channel object."""
newobj = copy(self) newobj = copy(self)
log.debug('CopyWrapper: _BLACKLISTED_COPY_TYPES = %s', _BLACKLISTED_COPY_TYPES) #log.debug('CopyWrapper: _BLACKLISTED_COPY_TYPES = %s', _BLACKLISTED_COPY_TYPES)
for attr, val in self.__dict__.items(): for attr, val in self.__dict__.items():
# We can't pickle IRCNetwork, so just return a reference of it. # We can't pickle IRCNetwork, so just return a reference of it.
if not isinstance(val, tuple(_BLACKLISTED_COPY_TYPES)): if not isinstance(val, tuple(_BLACKLISTED_COPY_TYPES)):
log.debug('CopyWrapper: copying attr %r', attr) #log.debug('CopyWrapper: copying attr %r', attr)
setattr(newobj, attr, deepcopy(val)) setattr(newobj, attr, deepcopy(val))
memo[id(self)] = newobj memo[id(self)] = newobj
@ -64,7 +71,6 @@ class CaseInsensitiveFixedSet(collections.abc.Set, CopyWrapper):
def __init__(self, *, data=None): def __init__(self, *, data=None):
if data is not None: if data is not None:
assert isinstance(data, set)
self._data = data self._data = data
else: else:
self._data = set() self._data = set()
@ -76,6 +82,11 @@ class CaseInsensitiveFixedSet(collections.abc.Set, CopyWrapper):
return key.lower() return key.lower()
return key return key
@classmethod
def _from_iterable(cls, it):
"""Returns a new iterable instance given the data in 'it'."""
return cls(data=it)
def __repr__(self): def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, self._data) return "%s(%s)" % (self.__class__.__name__, self._data)
@ -97,7 +108,6 @@ class CaseInsensitiveDict(collections.abc.MutableMapping, CaseInsensitiveFixedSe
""" """
def __init__(self, *, data=None): def __init__(self, *, data=None):
if data is not None: if data is not None:
assert isinstance(data, dict)
self._data = data self._data = data
else: else:
self._data = {} self._data = {}
@ -127,6 +137,10 @@ class IRCCaseInsensitiveDict(CaseInsensitiveDict):
return self._irc.to_lower(key) return self._irc.to_lower(key)
return key return key
def _from_iterable(self, it):
"""Returns a new iterable instance given the data in 'it'."""
return self.__class__(self._irc, data=it)
def __copy__(self): def __copy__(self):
return self.__class__(self._irc, data=self._data.copy()) return self.__class__(self._irc, data=self._data.copy())
@ -155,6 +169,10 @@ class IRCCaseInsensitiveSet(CaseInsensitiveSet):
return self._irc.to_lower(key) return self._irc.to_lower(key)
return key return key
def _from_iterable(self, it):
"""Returns a new iterable instance given the data in 'it'."""
return self.__class__(self._irc, data=it)
def __copy__(self): def __copy__(self):
return self.__class__(self._irc, data=self._data.copy()) return self.__class__(self._irc, data=self._data.copy())
@ -190,7 +208,12 @@ class DataStore:
Generic database class. Plugins should use a subclass of this such as JSONDataStore or Generic database class. Plugins should use a subclass of this such as JSONDataStore or
PickleDataStore. PickleDataStore.
""" """
def __init__(self, name, filename, save_frequency=None, default_db=None): def __init__(self, name, filename, save_frequency=None, default_db=None, data_dir=None):
if data_dir is None:
data_dir = conf.conf['pylink'].get('data_dir', '')
filename = os.path.join(data_dir, filename)
self.name = name self.name = name
self.filename = filename self.filename = filename
self.tmp_filename = filename + '.tmp' self.tmp_filename = filename + '.tmp'

1
test/parser-tests Submodule

@ -0,0 +1 @@
Subproject commit 26c9dd841467b9746fb325e16292a0b7a93bc802

View File

@ -0,0 +1,977 @@
"""
A test fixture for PyLink protocol modules.
"""
import time
import unittest
import collections
import itertools
from unittest.mock import patch
from pylinkirc import conf, world
from pylinkirc.log import log
from pylinkirc.classes import User, Server, Channel
class DummySocket():
def __init__(self):
#self.recv_messages = collections.deque()
self.sent_messages = collections.deque()
@staticmethod
def connect(address):
return
'''
def recv(bufsize, *args):
if self.recv_messages:
data = self.recv_messages.popleft()
print('<-', data)
return data
else:
return None
'''
def recv(self, bufsize, *args):
raise NotImplementedError
def send(self, data):
print('->', data)
self.sent_messages.append(data)
class BaseProtocolTest(unittest.TestCase):
proto_class = None
netname = 'test'
serverdata = conf.conf['servers'][netname]
def setUp(self):
if not self.proto_class:
raise RuntimeError("Must set target protocol module in proto_class")
self.p = self.proto_class(self.netname)
# Stub connect() and the socket for now...
self.p.connect = lambda self: None
self.p.socket = DummySocket()
if self.serverdata:
self.p.serverdata = self.serverdata
def _make_user(self, nick, uid, ts=None, sid=None, **kwargs):
"""
Creates a user for testing.
"""
if ts is None:
ts = int(time.time())
userobj = User(self.p, nick, ts, uid, sid, **kwargs)
self.p.users[uid] = userobj
return userobj
### STATEKEEPING FUNCTIONS
def test_nick_to_uid(self):
self.assertEqual(self.p.nick_to_uid('TestUser'), None)
self._make_user('TestUser', 'testuid1')
self.assertEqual(self.p.nick_to_uid('TestUser'), 'testuid1')
self.assertEqual(self.p.nick_to_uid('TestUser', multi=True), ['testuid1'])
self.assertEqual(self.p.nick_to_uid('BestUser'), None)
self.assertEqual(self.p.nick_to_uid('RestUser', multi=True), [])
self._make_user('TestUser', 'testuid2')
self.assertEqual(self.p.nick_to_uid('TestUser', multi=True), ['testuid1', 'testuid2'])
def test_is_internal(self):
self.p.servers['internalserver'] = Server(self.p, None, 'internal.server', internal=True)
self.p.sid = 'internalserver'
self.p.servers['externalserver'] = Server(self.p, None, 'external.server', internal=False)
iuser = self._make_user('someone', 'uid1', sid='internalserver')
euser = self._make_user('sometwo', 'uid2', sid='externalserver')
self.assertTrue(self.p.is_internal_server('internalserver'))
self.assertFalse(self.p.is_internal_server('externalserver'))
self.assertTrue(self.p.is_internal_client('uid1'))
self.assertFalse(self.p.is_internal_client('uid2'))
def test_is_manipulatable(self):
self.p.servers['serv1'] = Server(self.p, None, 'myserv.local', internal=True)
iuser = self._make_user('yes', 'uid1', sid='serv1', manipulatable=True)
euser = self._make_user('no', 'uid2', manipulatable=False)
self.assertTrue(self.p.is_manipulatable_client('uid1'))
self.assertFalse(self.p.is_manipulatable_client('uid2'))
def test_get_service_bot(self):
self.assertFalse(self.p.get_service_bot('nonexistent'))
regular_user = self._make_user('Guest12345', 'Guest12345@1')
service_user = self._make_user('MyServ', 'MyServ@2')
service_user.service = 'myserv'
self.assertFalse(self.p.get_service_bot('Guest12345@1'))
with patch.dict(world.services, {'myserv': 'myserv instance'}, clear=True):
self.assertEqual(self.p.get_service_bot('MyServ@2'), 'myserv instance')
def test_to_lower(self):
check = lambda inp, expected: self.assertEqual(self.p.to_lower(inp), expected)
check_unchanged = lambda inp: self.assertEqual(self.p.to_lower(inp), inp)
check('BLAH!', 'blah!')
check('BLAH!', 'blah!') # since we memoize
check_unchanged('zabcdefghijklmnopqrstuvwxy')
check('123Xyz !@#$%^&*()-=+', '123xyz !@#$%^&*()-=+')
if self.p.casemapping == 'rfc1459':
check('hello [] {} |\\ ~^', 'hello [] [] \\\\ ^^')
check('{Test Case}', '[test case]')
else:
check_unchanged('hello [] {} |\\ ~^')
check('{Test Case}', '{test case}')
def test_is_nick(self):
assertT = lambda inp: self.assertTrue(self.p.is_nick(inp))
assertF = lambda inp: self.assertFalse(self.p.is_nick(inp))
assertT('test')
assertT('PyLink')
assertT('[bracketman]')
assertT('{RACKETman}')
assertT('bar|tender')
assertT('\\bar|bender\\')
assertT('jlu5|ovd')
assertT('B')
assertT('`')
assertT('Hello123')
assertT('test-')
assertT('test-test')
assertT('_jkl9')
assertT('_-jkl9')
assertF('')
assertF('?')
assertF('nick@network')
assertF('Space flight')
assertF(' ')
assertF(' Space blight')
assertF('0')
assertF('-test')
assertF('#lounge')
assertF('\\bar/bender\\')
assertF('jlu5/ovd') # Technically not valid, but some IRCds don't care ;)
assertF('100AAAAAC') # TS6 UID
self.assertFalse(self.p.is_nick('longnicklongnicklongnicklongnicklongnicklongnick', nicklen=20))
self.assertTrue(self.p.is_nick('ninechars', nicklen=9))
self.assertTrue(self.p.is_nick('ChanServ', nicklen=20))
self.assertTrue(self.p.is_nick('leneight', nicklen=9))
self.assertFalse(self.p.is_nick('bitmonster', nicklen=9))
self.assertFalse(self.p.is_nick('ninecharsplus', nicklen=12))
def test_is_channel(self):
assertT = lambda inp: self.assertTrue(self.p.is_channel(inp))
assertF = lambda inp: self.assertFalse(self.p.is_channel(inp))
assertT('#test')
assertT('#')
assertT('#a#b#c')
assertF('nick!user@host')
assertF('&channel') # we don't support these yet
assertF('lorem ipsum')
def test_is_server_name(self):
self.assertTrue(self.p.is_server_name('test.local'))
self.assertTrue(self.p.is_server_name('IRC.example.com'))
self.assertTrue(self.p.is_server_name('services.'))
self.assertFalse(self.p.is_server_name('.org'))
self.assertFalse(self.p.is_server_name('bacon'))
def test_is_hostmask(self):
assertT = lambda inp: self.assertTrue(self.p.is_hostmask(inp))
assertF = lambda inp: self.assertFalse(self.p.is_hostmask(inp))
assertT('nick!user@host')
assertT('abc123!~ident@ip1-2-3-4.example.net')
assertF('brick!user')
assertF('user@host')
assertF('!@')
assertF('!')
assertF('@abcd')
assertF('#channel')
assertF('test.host')
assertF('nick ! user @ host')
assertF('alpha!beta@example.net#otherchan') # Janus workaround
def test_get_SID(self):
self.p.servers['serv1'] = Server(self.p, None, 'myserv.local', internal=True)
check = lambda inp, expected: self.assertEqual(self.p._get_SID(inp), expected)
check('myserv.local', 'serv1')
check('MYSERV.local', 'serv1')
check('serv1', 'serv1')
check('other.server', 'other.server')
def test_get_UID(self):
u = self._make_user('you', uid='100')
check = lambda inp, expected: self.assertEqual(self.p._get_UID(inp), expected)
check('you', '100') # nick to UID
check('YOu', '100')
check('100', '100') # already a UID
check('Test', 'Test') # non-existent
def test_get_hostmask(self):
u = self._make_user('lorem', 'testUID', ident='ipsum', host='sit.amet')
self.assertEqual(self.p.get_hostmask(u.uid), 'lorem!ipsum@sit.amet')
def test_get_friendly_name(self):
u = self._make_user('lorem', 'testUID', ident='ipsum', host='sit.amet')
s = self.p.servers['mySID'] = Server(self.p, None, 'irc.example.org')
c = self.p._channels['#abc'] = Channel('#abc')
self.assertEqual(self.p.get_friendly_name(u.uid), 'lorem')
self.assertEqual(self.p.get_friendly_name('#abc'), '#abc')
self.assertEqual(self.p.get_friendly_name('mySID'), 'irc.example.org')
# TODO: _squit wrapper
### MISC UTILS
def test_get_service_option(self):
f = self.p.get_service_option
self.assertEqual(f('myserv', 'myopt'), None) # No value anywhere
self.assertEqual(f('myserv', 'myopt', default=0), 0)
# Define global option
with patch.dict(conf.conf, {'myserv': {'youropt': True, 'myopt': 1234}}):
self.assertEqual(f('myserv', 'myopt'), 1234) # Read global option
# Both global and local options exist
with patch.dict(self.p.serverdata, {'myserv_myopt': 2345}):
self.assertEqual(f('myserv', 'myopt'), 2345) # Read local option
self.assertEqual(f('myserv', 'myopt', global_option='youropt'), 2345)
# Custom global_option setting
self.assertEqual(f('myserv', 'abcdef', global_option='youropt'), True)
# Define local option
with patch.dict(self.p.serverdata, {'myserv_myopt': 998877}):
self.assertEqual(f('myserv', 'myopt'), 998877) # Read local option
self.assertEqual(f('myserv', 'myopt', default='unused'), 998877)
def test_get_service_options_list(self):
f = self.p.get_service_options
self.assertEqual(f('myserv', 'items', list), []) # No value anywhere
# Define global option
with patch.dict(conf.conf, {'myserv': {'items': [1, 10, 100], 'empty': []}}):
self.assertEqual(f('myserv', 'items', list), [1, 10, 100]) # Global value only
self.assertEqual(f('myserv', 'empty', list), [])
# Both global and local options exist
with patch.dict(self.p.serverdata, {'myserv_items': [2, 4, 6, 8], 'empty': []}):
self.assertEqual(f('myserv', 'items', list), [1, 10, 100, 2, 4, 6, 8])
# Custom global_option setting
self.assertEqual(f('myserv', 'items', list, global_option='nonexistent'), [2, 4, 6, 8])
self.assertEqual(f('myserv', 'empty', list), [])
# Define local option
with patch.dict(self.p.serverdata, {'myserv_items': [1, 0, 0, 3]}):
self.assertEqual(f('myserv', 'items', list), [1, 0, 0, 3]) # Read local option
def test_get_service_options_dict(self):
f = self.p.get_service_options
self.assertEqual(f('chanman', 'items', dict), {}) # No value anywhere
# This is just mildly relevant test data, it's not actually used anywhere.
globalopt = {'o': '@', 'v': '+', 'a': '!', 'h': '%'}
localopt = {'a': '&', 'q': '~'}
# Define global option
with patch.dict(conf.conf, {'chanman': {'prefixes': globalopt, 'empty': {}}}):
self.assertEqual(f('chanman', 'prefixes', dict), globalopt) # Global value only
self.assertEqual(f('chanman', 'empty', dict), {})
# Both global and local options exist
with patch.dict(self.p.serverdata, {'chanman_prefixes': localopt, 'empty': {}}):
self.assertEqual(f('chanman', 'prefixes', dict), {**globalopt, **localopt})
self.assertEqual(f('chanman', 'items', dict), {}) # No value anywhere
self.assertEqual(f('chanman', 'empty', dict), {})
# Define local option
with patch.dict(self.p.serverdata, {'chanman_prefixes': localopt}):
self.assertEqual(f('chanman', 'prefixes', dict), localopt) # Read local option
### MODE HANDLING
def test_parse_modes_channel_rfc(self):
# These are basic tests that only use RFC 1459 defined modes.
# IRCds supporting more complex modes can define new test cases if needed.
u = self._make_user('testuser', uid='100')
c = self.p.channels['#testruns'] = Channel(self.p, name='#testruns')
self.assertEqual(
self.p.parse_modes('#testruns', ['+m']),
[('+m', None)]
)
self.assertEqual(
self.p.parse_modes('#testruns', ['+l', '3']),
[('+l', '3')]
)
self.assertEqual(
self.p.parse_modes('#testruns', ['+ntl', '59']),
[('+n', None), ('+t', None), ('+l', '59')]
)
self.assertEqual(
self.p.parse_modes('#testruns', ['+k-n', 'test']),
[('+k', 'test'), ('-n', None)]
)
self.assertEqual(
self.p.parse_modes('#testruns', ['+o', '102']), # unknown UID
[]
)
c.users.add(u)
u.channels.add(c)
self.assertEqual(
self.p.parse_modes('#testruns', ['+o', '100']),
[('+o', '100')]
)
self.assertEqual(
self.p.parse_modes('#testruns', ['+vip', '100']),
[('+v', '100'), ('+i', None), ('+p', None)]
)
def test_parse_modes_channel_rfc2(self):
# These are basic tests that only use RFC 1459 defined modes.
# IRCds supporting more complex modes can define new test cases if needed.
c = self.p.channels['#testruns'] = Channel(self.p, name='#testruns')
# Note: base case is not defined and raises AssertionError
self.assertEqual(
self.p.parse_modes('#testruns', ['+m']), # add modes
[('+m', None)]
)
self.assertEqual(
self.p.parse_modes('#testruns', ['-tn']), # remove modes
[('-t', None), ('-n', None)]
)
self.assertEqual(
self.p.parse_modes('#TESTRUNS', ['-tn']), # different case target
[('-t', None), ('-n', None)]
)
self.assertEqual(
self.p.parse_modes('#testruns', ['+l', '3']), # modes w/ arguments
[('+l', '3')]
)
self.assertEqual(
self.p.parse_modes('#testruns', ['+nlt', '59']), # combination
[('+n', None), ('+l', '59'), ('+t', None)]
)
self.assertEqual(
self.p.parse_modes('#testruns', ['+k-n', 'test']), # swapping +/-
[('+k', 'test'), ('-n', None)]
)
self.assertEqual(
self.p.parse_modes('#testruns', ['n-s']), # sloppy syntax
[('+n', None), ('-s', None)]
)
self.assertEqual(
self.p.parse_modes('#testruns', ['+bmi', '*!test@example.com']),
[('+b', '*!test@example.com'), ('+m', None), ('+i', None)]
)
def test_parse_modes_prefixmodes_rfc(self):
c = self.p.channels['#testruns'] = Channel(self.p, name='#testruns')
self.assertEqual(
self.p.parse_modes('#testruns', ['+ov', '102', '101']), # unknown UIDs are ignored
[]
)
u = self._make_user('test100', uid='100')
c.users.add(u)
u.channels.add(c)
self.assertEqual(
self.p.parse_modes('#testruns', ['+o', '100']),
[('+o', '100')]
)
self.assertEqual(
self.p.parse_modes('#testruns', ['+vip', '100']),
[('+v', '100'), ('+i', None), ('+p', None)]
)
self.assertEqual(
self.p.parse_modes('#testruns', ['-o+bn', '100', '*!test@example.com']),
[('-o', '100'), ('+b', '*!test@example.com'), ('+n', None)]
)
self.assertEqual(
# 2nd user missing
self.p.parse_modes('#testruns', ['+oovv', '100', '102', '100', '102']),
[('+o', '100'), ('+v', '100')]
)
u2 = self._make_user('test102', uid='102')
c.users.add(u2)
u2.channels.add(c)
self.assertEqual(
# two users interleaved
self.p.parse_modes('#testruns', ['+oovv', '100', '102', '100', '102']),
[('+o', '100'), ('+o', '102'), ('+v', '100'), ('+v', '102')]
)
self.assertEqual(
# Mode cycle
self.p.parse_modes('#testruns', ['+o-o', '100', '100']),
[('+o', '100'), ('-o', '100')]
)
def test_parse_modes_channel_ban_complex(self):
c = self.p.channels['#testruns'] = Channel(self.p, name='#testruns')
self.assertEqual(
# add first ban, but don't remove second because it doesn't exist
self.p.parse_modes('#testruns', ['+b-b', '*!*@test1', '*!*@test2']),
[('+b', '*!*@test1')],
"First ban should have been added, and second ignored"
)
self.p.apply_modes('#testruns', [('+b', '*!*@test1')])
self.assertEqual(
# remove first ban because it matches case
self.p.parse_modes('#testruns', ['-b', '*!*@test1']),
[('-b', '*!*@test1')],
"First ban should have been removed (same case)"
)
self.assertEqual(
# remove first ban despite different case
self.p.parse_modes('#testruns', ['-b', '*!*@TEST1']),
[('-b', '*!*@test1')],
"First ban should have been removed (different case)"
)
self.p.apply_modes('#testruns', [('+b', '*!*@Test2')])
self.assertEqual(
# remove second ban despite different case
self.p.parse_modes('#testruns', ['-b', '*!*@test2']),
[('-b', '*!*@Test2')],
"Second ban should have been removed (different case)"
)
def test_parse_modes_channel_ban_cycle(self):
c = self.p.channels['#testruns'] = Channel(self.p, name='#testruns')
self.assertEqual(
self.p.parse_modes('#testruns', ['+b-b', '*!*@example.com', '*!*@example.com']),
[('+b', '*!*@example.com'), ('-b', '*!*@example.com')],
"Cycling a ban +b-b should remove it"
)
self.assertEqual(
self.p.parse_modes('#testruns', ['-b+b', '*!*@example.com', '*!*@example.com']),
[('+b', '*!*@example.com')],
"Cycling a ban -b+b should add it"
)
self.assertEqual(
self.p.parse_modes('#testruns', ['+b-b', '*!*@example.com', '*!*@Example.com']),
[('+b', '*!*@example.com'), ('-b', '*!*@example.com')],
"Cycling a ban +b-b should remove it (different case)"
)
self.assertEqual(
self.p.parse_modes('#testruns', ['+b-b', '*!*@Example.com', '*!*@example.com']),
[('+b', '*!*@Example.com'), ('-b', '*!*@Example.com')],
"Cycling a ban +b-b should remove it (different case)"
)
def test_parse_modes_channel_prefixmode_has_nick(self):
c = self.p.channels['#'] = Channel(self.p, name='#')
u = self._make_user('mynick', uid='myuid')
c.users.add(u)
u.channels.add(c)
self.assertEqual(
self.p.parse_modes('#', ['+o', 'myuid']),
[('+o', 'myuid')],
"+o on UID should be registered"
)
self.assertEqual(
self.p.parse_modes('#', ['+o', 'mynick']),
[('+o', 'myuid')],
"+o on nick should be registered"
)
self.assertEqual(
self.p.parse_modes('#', ['+o', 'MyUiD']),
[],
"+o on wrong case UID should be ignored"
)
self.assertEqual(
self.p.parse_modes('#', ['+o', 'MyNick']),
[('+o', 'myuid')],
"+o on different case nick should be registered"
)
self.assertEqual(
self.p.parse_modes('#', ['-o', 'myuid']),
[('-o', 'myuid')],
"-o on UID should be registered"
)
self.assertEqual(
self.p.parse_modes('#', ['-o', 'mynick']),
[('-o', 'myuid')],
"-o on nick should be registered"
)
# TODO: check the output of parse_modes() here too
def test_parse_modes_channel_key(self):
c = self.p.channels['#pylink'] = Channel(self.p, name='#pylink')
c.modes = {('k', 'foobar')}
modes = self.p.parse_modes('#pylink', ['-k', 'foobar'])
self.assertEqual(modes, [('-k', 'foobar')], "Parse result should include -k")
modes = self.p.parse_modes('#pylink', ['-k', 'aBcDeF'])
self.assertEqual(modes, [], "Incorrect key removal should be ignored")
modes = self.p.parse_modes('#pylink', ['+k', 'aBcDeF'])
self.assertEqual(modes, [('+k', 'aBcDeF')], "Parse result should include +k (replace key)")
# Mismatched case - treat this as remove
# Some IRCds allow this (Unreal, P10), others do not (InspIRCd). However, if such a message
# makes actually its way to us it is most likely valid.
# Note: Charybdis and ngIRCd do -k * removal instead so this case will never happen there
modes = self.p.parse_modes('#pylink', ['-k', 'FooBar'])
self.assertEqual(modes, [('-k', 'foobar')], "Parse result should include -k (different case)")
modes = self.p.parse_modes('#pylink', ['-k', '*'])
self.assertEqual(modes, [('-k', 'foobar')], "Parse result should include -k (-k *)")
def test_parse_modes_user_rfc(self):
u = self._make_user('testuser', uid='100')
self.assertEqual(
self.p.parse_modes('100', ['+i-w+x']),
[('+i', None), ('-w', None), ('+x', None)]
)
self.assertEqual(
# Sloppy syntax, but OK
self.p.parse_modes('100', ['wx']),
[('+w', None), ('+x', None)]
)
def test_apply_modes_channel_simple(self):
c = self.p.channels['#'] = Channel(self.p, name='#')
self.p.apply_modes('#', [('+m', None)])
self.assertEqual(c.modes, {('m', None)})
self.p.apply_modes('#', []) # No-op
self.assertEqual(c.modes, {('m', None)})
self.p.apply_modes('#', [('-m', None)])
self.assertFalse(c.modes) # assert is empty
def test_apply_modes_channel_add_remove(self):
c = self.p.channels['#'] = Channel(self.p, name='#')
self.p.apply_modes('#', [('+i', None)])
self.assertEqual(c.modes, {('i', None)})
self.p.apply_modes('#', [('+n', None), ('-i', None)])
self.assertEqual(c.modes, {('n', None)})
c = self.p.channels['#Magic'] = Channel(self.p, name='#Magic')
self.p.apply_modes('#Magic', [('+m', None), ('+n', None), ('+i', None)])
self.assertEqual(c.modes, {('m', None), ('n', None), ('i', None)}, "Modes should be added")
self.p.apply_modes('#Magic', [('-i', None), ('-n', None)])
self.assertEqual(c.modes, {('m', None)}, "Modes should be removed")
def test_apply_modes_channel_remove_nonexistent(self):
c = self.p.channels['#abc'] = Channel(self.p, name='#abc')
self.p.apply_modes('#abc', [('+t', None)])
self.assertEqual(c.modes, {('t', None)})
self.p.apply_modes('#abc', [('-n', None), ('-i', None)])
self.assertEqual(c.modes, {('t', None)})
def test_apply_modes_channel_limit(self):
c = self.p.channels['#abc'] = Channel(self.p, name='#abc')
self.p.apply_modes('#abc', [('+t', None), ('+l', '30')])
self.assertEqual(c.modes, {('t', None), ('l', '30')})
self.p.apply_modes('#abc', [('+l', '55')])
self.assertEqual(c.modes, {('t', None), ('l', '55')})
self.p.apply_modes('#abc', [('-l', None)])
self.assertEqual(c.modes, {('t', None)})
def test_apply_modes_channel_key(self):
c = self.p.channels['#pylink'] = Channel(self.p, name='#pylink')
self.p.apply_modes('#pylink', [('+k', 'password123'), ('+s', None)])
self.assertEqual(c.modes, {('s', None), ('k', 'password123')})
self.p.apply_modes('#pylink', [('+k', 'qwerty')])
self.assertEqual(c.modes, {('s', None), ('k', 'qwerty')})
self.p.apply_modes('#pylink', [('-k', 'abcdef')])
# Trying to remove with wrong key is a no-op
self.assertEqual(c.modes, {('s', None), ('k', 'qwerty')})
self.p.apply_modes('#pylink', [('-k', 'qwerty')])
self.assertEqual(c.modes, {('s', None)})
self.p.apply_modes('#pylink', [('+k', 'qwerty')])
self.assertEqual(c.modes, {('s', None), ('k', 'qwerty')})
self.p.apply_modes('#pylink', [('-k', 'QWERTY')]) # Remove with different case
self.assertEqual(c.modes, {('s', None)})
self.p.apply_modes('#pylink', [('+k', '12345')]) # Replace existing key
self.assertEqual(c.modes, {('s', None), ('k', '12345')})
def test_apply_modes_channel_ban(self):
c = self.p.channels['#Magic'] = Channel(self.p, name='#Magic')
self.p.apply_modes('#Magic', [('+b', '*!*@test.host'), ('+b', '*!*@best.host')])
self.assertEqual(c.modes, {('b', '*!*@test.host'), ('b', '*!*@best.host')}, "Bans should be added")
# This should be a no-op
self.p.apply_modes('#Magic', [('-b', '*!*non-existent')])
self.assertEqual(c.modes, {('b', '*!*@test.host'), ('b', '*!*@best.host')}, "Trying to unset non-existent ban should be no-op")
# Simple removal
self.p.apply_modes('#Magic', [('-b', '*!*@test.host')])
self.assertEqual(c.modes, {('b', '*!*@best.host')}, "Ban on *!*@test.host be removed (same case as original)")
# Removal but different case than original
self.p.apply_modes('#Magic', [('-b', '*!*@BEST.HOST')])
self.assertFalse(c.modes, "Ban on *!*@best.host should be removed (different case)")
def test_apply_modes_channel_ban_multiple(self):
c = self.p.channels['#Magic'] = Channel(self.p, name='#Magic')
self.p.apply_modes('#Magic', [('+b', '*!*@test.host'), ('+b', '*!*@best.host'), ('+b', '*!*@guest.host')])
self.assertEqual(c.modes, {('b', '*!*@test.host'), ('b', '*!*@best.host'), ('b', '*!*@guest.host')},
"Bans should be added")
self.p.apply_modes('#Magic', [('-b', '*!*@best.host'), ('-b', '*!*@guest.host'), ('-b', '*!*@test.host')])
self.assertEqual(c.modes, set(), "Bans should be removed")
def test_apply_modes_channel_mode_cycle(self):
c = self.p.channels['#Magic'] = Channel(self.p, name='#Magic')
self.p.apply_modes('#Magic', [('+b', '*!*@example.net'), ('-b', '*!*@example.net')])
self.assertEqual(c.modes, set(), "Ban should have been removed (same case)")
self.p.apply_modes('#Magic', [('+b', '*!*@example.net'), ('-b', '*!*@Example.net')])
self.assertEqual(c.modes, set(), "Ban should have been removed (different case)")
u = self._make_user('nick', uid='user')
c.users.add(u.uid)
u.channels.add(c)
self.p.apply_modes('#Magic', [('+o', 'user'), ('-o', 'user')])
self.assertEqual(c.modes, set(), "No prefixmodes should have been set")
self.assertFalse(c.is_op('user'))
self.assertFalse(c.get_prefix_modes('user'))
def test_apply_modes_channel_prefixmodes(self):
# Make some users
c = self.p.channels['#staff'] = Channel(self.p, name='#staff')
u1 = self._make_user('user100', uid='100')
u2 = self._make_user('user101', uid='101')
c.users.add(u1.uid)
u1.channels.add(c)
c.users.add(u2.uid)
u2.channels.add(c)
# Set modes
self.p.apply_modes('#staff', [('+o', '100'), ('+v', '101'), ('+t', None)])
self.assertEqual(c.modes, {('t', None)})
# State checks, lots of them... TODO: move this into Channel class tests
self.assertTrue(c.is_op('100'))
self.assertTrue(c.is_op_plus('100'))
self.assertFalse(c.is_halfop('100'))
self.assertTrue(c.is_halfop_plus('100'))
self.assertFalse(c.is_voice('100'))
self.assertTrue(c.is_voice_plus('100'))
self.assertEqual(c.get_prefix_modes('100'), ['op'])
self.assertTrue(c.is_voice('101'))
self.assertTrue(c.is_voice_plus('101'))
self.assertFalse(c.is_halfop('101'))
self.assertFalse(c.is_halfop_plus('101'))
self.assertFalse(c.is_op('101'))
self.assertFalse(c.is_op_plus('101'))
self.assertEqual(c.get_prefix_modes('101'), ['voice'])
self.assertFalse(c.prefixmodes['owner'])
self.assertFalse(c.prefixmodes['admin'])
self.assertEqual(c.prefixmodes['op'], {'100'})
self.assertFalse(c.prefixmodes['halfop'])
self.assertEqual(c.prefixmodes['voice'], {'101'})
self.p.apply_modes('#staff', [('-o', '100')])
self.assertEqual(c.modes, {('t', None)})
self.assertFalse(c.get_prefix_modes('100'))
self.assertEqual(c.get_prefix_modes('101'), ['voice'])
def test_apply_modes_user(self):
u = self._make_user('nick', uid='user')
self.p.apply_modes('user', [('+o', None), ('+w', None)])
self.assertEqual(u.modes, {('o', None), ('w', None)})
self.p.apply_modes('user', [('-o', None), ('+i', None)])
self.assertEqual(u.modes, {('i', None), ('w', None)})
def test_reverse_modes_simple(self):
c = self.p.channels['#foobar'] = Channel(self.p, name='#foobar')
c.modes = {('m', None), ('n', None)}
# This function supports both strings and mode lists
# Base cases
for inp in {'', '+', '-'}:
self.assertEqual(self.p.reverse_modes('#foobar', inp), '+')
out = self.p.reverse_modes('#foobar', [])
self.assertEqual(out, [])
# One simple
out = self.p.reverse_modes('#foobar', '+t')
self.assertEqual(out, '-t')
out = self.p.reverse_modes('#foobar', [('+t', None)])
self.assertEqual(out, [('-t', None)])
out = self.p.reverse_modes('#foobar', '+m')
self.assertEqual(out, '+', 'Calling reverse_modes() on an already set mode is a no-op')
out = self.p.reverse_modes('#foobar', [('+m', None)])
self.assertEqual(out, [], 'Calling reverse_modes() on an already set mode is a no-op')
out = self.p.reverse_modes('#foobar', [('-i', None)])
self.assertEqual(out, [], 'Calling reverse_modes() on non-existent mode is a no-op')
def test_reverse_modes_multi(self):
c = self.p.channels['#foobar'] = Channel(self.p, name='#foobar')
c.modes = {('t', None), ('n', None)}
# reverse_modes should ignore modes that were already set
out = self.p.reverse_modes('#foobar', '+nt')
self.assertEqual(out, '+')
out = self.p.reverse_modes('#foobar', [('+m', None), ('+i', None)])
self.assertEqual(out, [('-m', None), ('-i', None)])
out = self.p.reverse_modes('#foobar', '+mint')
self.assertEqual(out, '-mi') # Ignore +nt since it already exists
out = self.p.reverse_modes('#foobar', '+m-t')
self.assertEqual(out, '-m+t')
out = self.p.reverse_modes('#foobar', '+mn-t')
self.assertEqual(out, '-m+t')
out = self.p.reverse_modes('#foobar', [('+m', None), ('+n', None), ('-t', None)])
self.assertEqual(out, [('-m', None), ('+t', None)])
def test_reverse_modes_bans(self):
c = self.p.channels['#foobar'] = Channel(self.p, name='#foobar')
c.modes = {('t', None), ('n', None), ('b', '*!*@example.com')}
out = self.p.reverse_modes('#foobar', [('+b', '*!*@test')])
self.assertEqual(out, [('-b', '*!*@test')], "non-existent ban should be removed")
out = self.p.reverse_modes('#foobar', '+b *!*@example.com')
self.assertEqual(out, '+', "+b existing ban should be no-op")
out = self.p.reverse_modes('#foobar', '+b *!*@Example.com')
self.assertEqual(out, '+', "Should ignore attempt to change case of ban mode")
out = self.p.reverse_modes('#foobar', '-b *!*@example.com')
self.assertEqual(out, '+b *!*@example.com', "-b existing ban should reset it")
out = self.p.reverse_modes('#foobar', '-b *!*@Example.com')
self.assertEqual(out, '+b *!*@example.com', "-b existing ban should reset it using original case")
out = self.p.reverse_modes('#foobar', '-b *!*@*')
self.assertEqual(out, '+', "Removing non-existent ban is no-op")
out = self.p.reverse_modes('#foobar', '+bbbm 1 2 3')
self.assertEqual(out, '-bbbm 1 2 3')
def test_reverse_modes_limit(self):
c = self.p.channels['#foobar'] = Channel(self.p, name='#foobar')
c.modes = {('t', None), ('n', None), ('l', '50')}
out = self.p.reverse_modes('#foobar', [('+l', '100')])
self.assertEqual(out, [('+l', '50')], "Setting +l should reset original mode")
out = self.p.reverse_modes('#foobar', [('-l', None)])
self.assertEqual(out, [('+l', '50')], "Unsetting +l should reset original mode")
out = self.p.reverse_modes('#foobar', [('+l', '50')])
self.assertEqual(out, [], "Setting +l with original value is no-op")
c.modes.clear()
out = self.p.reverse_modes('#foobar', [('+l', '111'), ('+m', None)])
self.assertEqual(out, [('-l', None), ('-m', None)], "Setting +l on channel without it should remove")
def test_reverse_modes_prefixmodes(self):
c = self.p.channels['#foobar'] = Channel(self.p, name='#foobar')
c.modes = {('t', None), ('n', None)}
u = self._make_user('nick', uid='user')
u.channels.add(c)
c.users.add(u)
c.prefixmodes['op'].add(u.uid)
out = self.p.reverse_modes('#foobar', '-o user')
self.assertEqual(out, '+o user')
out = self.p.reverse_modes('#foobar', '+o user')
self.assertEqual(out, '+')
out = self.p.reverse_modes('#foobar', '+ov user user')
self.assertEqual(out, '-v user') # ignore +o
out = self.p.reverse_modes('#foobar', '-ovt user user')
self.assertEqual(out, '+ot user') # ignore -v
def test_reverse_modes_cycle_simple(self):
c = self.p.channels['#weirdstuff'] = Channel(self.p, name='#weirdstuff')
c.modes = {('t', None), ('n', None)}
out = self.p.reverse_modes('#weirdstuff', '+n-n') # +- cycle existing mode
self.assertEqual(out, '+n')
out = self.p.reverse_modes('#weirdstuff', '-n+n') # -+ cycle existing mode
self.assertEqual(out, '+n') # Ugly but OK
out = self.p.reverse_modes('#weirdstuff', '+m-m') # +- cycle non-existent mode
self.assertEqual(out, '-m')
out = self.p.reverse_modes('#weirdstuff', '-m+m') # -+ cycle non-existent mode
self.assertEqual(out, '-m') # Ugly but OK
def test_reverse_modes_cycle_bans(self):
c = self.p.channels['#weirdstuff'] = Channel(self.p, name='#weirdstuff')
c.modes = {('t', None), ('n', None), ('b', '*!*@test.host')}
out = self.p.reverse_modes('#weirdstuff', '+b-b *!*@test.host *!*@test.host') # +- cycle existing ban
self.assertEqual(out, '+b *!*@test.host')
out = self.p.reverse_modes('#weirdstuff', '-b+b *!*@test.host *!*@test.host') # -+ cycle existing ban
self.assertEqual(out, '+b *!*@test.host') # Ugly but OK
out = self.p.reverse_modes('#weirdstuff', '+b-b *!*@* *!*@*') # +- cycle existing ban
self.assertEqual(out, '-b *!*@*')
out = self.p.reverse_modes('#weirdstuff', '-b+b *!*@* *!*@*') # -+ cycle existing ban
self.assertEqual(out, '-b *!*@*') # Ugly but OK
def test_reverse_modes_cycle_arguments(self):
# All of these cases are ugly, sometimes unsetting modes that don't exist...
c = self.p.channels['#weirdstuff'] = Channel(self.p, name='#weirdstuff')
out = self.p.reverse_modes('#weirdstuff', '+l-l 30')
self.assertEqual(out, '-l')
out = self.p.reverse_modes('#weirdstuff', '-l+l 30')
self.assertEqual(out, '-l')
out = self.p.reverse_modes('#weirdstuff', '+k-k aaaaaaaaaaaa aaaaaaaaaaaa')
self.assertEqual(out, '-k aaaaaaaaaaaa')
out = self.p.reverse_modes('#weirdstuff', '-k+k aaaaaaaaaaaa aaaaaaaaaaaa')
self.assertEqual(out, '-k aaaaaaaaaaaa')
c.modes = {('l', '555'), ('k', 'NO-PLEASE')}
out = self.p.reverse_modes('#weirdstuff', '+l-l 30')
self.assertEqual(out, '+l 555')
out = self.p.reverse_modes('#weirdstuff', '-l+l 30')
self.assertEqual(out, '+l 555')
out = self.p.reverse_modes('#weirdstuff', '+k-k aaaaaaaaaaaa aaaaaaaaaaaa')
self.assertEqual(out, '+k NO-PLEASE')
out = self.p.reverse_modes('#weirdstuff', '-k+k aaaaaaaaaaaa aaaaaaaaaaaa')
self.assertEqual(out, '+k NO-PLEASE')
def test_reverse_modes_cycle_prefixmodes(self):
# All of these cases are ugly, sometimes unsetting modes that don't exist...
c = self.p.channels['#weirdstuff'] = Channel(self.p, name='#weirdstuff')
u = self._make_user('nick', uid='user')
u.channels.add(c)
c.users.add(u)
# user not already opped
out = self.p.reverse_modes('#weirdstuff', '+o-o user user')
self.assertEqual(out, '-o user')
out = self.p.reverse_modes('#weirdstuff', '-o+o user user')
self.assertEqual(out, '-o user')
c.prefixmodes['op'].add(u.uid)
# user was opped
out = self.p.reverse_modes('#weirdstuff', '+o-o user user')
self.assertEqual(out, '+o user')
out = self.p.reverse_modes('#weirdstuff', '-o+o user user')
self.assertEqual(out, '+o user')
def test_join_modes(self):
# join_modes operates independently of state; the input just has to be valid modepairs
check = lambda inp, expected, sort=False: self.assertEqual(self.p.join_modes(inp, sort=sort), expected)
check([], '+') # base case
check([('+b', '*!*@test')], '+b *!*@test')
check([('-S', None)], '-S')
check([('+n', None), ('+t', None)], '+nt')
check([('+t', None), ('+n', None)], '+tn')
check([('+t', None), ('+n', None)], '+nt', sort=True)
check([('-n', None), ('-s', None)], '-ns')
check([('+q', '*'), ('-q', '*')], '+q-q * *')
check([('+l', '5'), ('-n', None), ('+R', None)], '+l-n+R 5')
# Sloppy syntax: assume leading mode is + if not otherwise stated
check([('o', '100AAAAAC'), ('m', None), ('-v', '100AAAAAC')], '+om-v 100AAAAAC 100AAAAAC')
def test_wrap_modes_small(self):
# wrap_modes is also state independent; it only calls join_modes
N_USERS = 5
modes = [('+m', None)] + [('-b', 'user%s!*@example.org' % num) for num in range(N_USERS)]
wr = self.p.wrap_modes(modes, 200)
log.debug('wrap_modes input: %s', modes)
log.debug('wrap_modes output: %s', wr)
self.assertEqual(len(wr), 1) # no split should have occurred
self.assertEqual(wr[0], self.p.join_modes(modes))
def test_wrap_modes_split_length_limit(self):
# wrap_modes is also state independent; it only calls join_modes
N_USERS = 50
modes = [('+o', 'user%s' % num) for num in range(N_USERS)]
wr = self.p.wrap_modes(modes, 120)
log.debug('wrap_modes input: %s', modes)
log.debug('wrap_modes output: %s', wr)
self.assertTrue(len(wr) > 1) # we should have induced a split
for s in wr:
# Check that each split item starts with the right mode char
self.assertTrue(s.startswith('+oooo'))
all_args = itertools.chain.from_iterable(s.split() for s in wr)
for num in range(N_USERS):
# Check that no users are missing
self.assertIn('user%s' % num, all_args)
def test_wrap_modes_split_max_modes(self):
# wrap_modes is also state independent; it only calls join_modes
N_USERS = 50
N_MAX_PER_MSG = 8
modes = [('+v', 'user%s' % num) for num in range(N_USERS)]
wr = self.p.wrap_modes(modes, 200, N_MAX_PER_MSG)
log.debug('wrap_modes input: %s', modes)
log.debug('wrap_modes output: %s', wr)
self.assertTrue(len(wr) > 1) # we should have induced a split
splits = [s.split() for s in wr]
for s in splits:
# Check that each message sets <= N_MAX_PER_MSG modes
self.assertTrue(len(s[0]) <= (N_MAX_PER_MSG + 1)) # add 1 to account for leading +
self.assertTrue(s[0].startswith('+'))
self.assertTrue(s[0].endswith('v'))
all_args = itertools.chain.from_iterable(splits)
for num in range(N_USERS):
# Check that no users are missing
self.assertIn('user%s' % num, all_args)
# TODO: test type coersion if channel or mode targets are ints

118
test/test_irc_parsers.py Normal file
View File

@ -0,0 +1,118 @@
"""
Runs IRC parser tests from ircdocs/parser-tests.
This test suite runs static code only.
"""
from pathlib import Path
import unittest
import yaml
PARSER_DATA_PATH = Path(__file__).parent.resolve() / 'parser-tests' / 'tests'
print(PARSER_DATA_PATH)
from pylinkirc import utils
from pylinkirc.protocols.ircs2s_common import IRCCommonProtocol
class MessageParserTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
with open(PARSER_DATA_PATH / 'msg-split.yaml') as f:
cls.MESSAGE_SPLIT_TEST_DATA = yaml.safe_load(f)
with open(PARSER_DATA_PATH / 'userhost-split.yaml') as f:
cls.USER_HOST_SPLIT_TEST_DATA = yaml.safe_load(f)
with open(PARSER_DATA_PATH / 'mask-match.yaml') as f:
cls.MASK_MATCH_TEST_DATA = yaml.safe_load(f)
with open(PARSER_DATA_PATH / 'validate-hostname.yaml') as f:
cls.VALIDATE_HOSTNAME_TEST_DATA = yaml.safe_load(f)
def testMessageSplit(self):
for testdata in self.MESSAGE_SPLIT_TEST_DATA['tests']:
inp = testdata['input']
atoms = testdata['atoms']
with self.subTest():
expected = []
has_source = False
if 'source' in atoms:
has_source = True
expected.append(atoms['source'])
if 'verb' in atoms:
expected.append(atoms['verb'])
if 'params' in atoms:
expected.extend(atoms['params'])
if 'tags' in atoms:
# Remove message tags before parsing
_, inp = inp.split(" ", 1)
if has_source:
parts = IRCCommonProtocol.parse_prefixed_args(inp)
else:
parts = IRCCommonProtocol.parse_args(inp)
self.assertEqual(expected, parts, "Parse test failed for string: %r" % inp)
@unittest.skip("Not quite working yet")
def testMessageTags(self):
for testdata in self.MESSAGE_SPLIT_TEST_DATA['tests']:
inp = testdata['input']
atoms = testdata['atoms']
with self.subTest():
if 'tags' in atoms:
self.assertEqual(atoms['tags'], IRCCommonProtocol.parse_message_tags(inp.split(" ")),
"Parse test failed for message tags: %r" % inp)
def testUserHostSplit(self):
for test in self.USER_HOST_SPLIT_TEST_DATA['tests']:
inp = test['source']
atoms = test['atoms']
with self.subTest():
if 'nick' not in atoms or 'user' not in atoms or 'host' not in atoms:
# Trying to parse a hostmask with missing atoms is an error in split_hostmask()
with self.assertRaises(ValueError):
utils.split_hostmask(inp)
else:
expected = [atoms['nick'], atoms['user'], atoms['host']]
self.assertEqual(expected, utils.split_hostmask(inp))
def testHostMatch(self):
for test in self.MASK_MATCH_TEST_DATA['tests']:
mask = test['mask']
# N.B.: utils.match_text() does Unicode case-insensitive match by default,
# which might not be the right thing to do on IRC.
# But irc.to_lower() isn't a static function so we're not testing it here...
for match in test['matches']:
with self.subTest():
self.assertTrue(utils.match_text(mask, match))
for fail in test['fails']:
with self.subTest():
self.assertFalse(utils.match_text(mask, fail))
def testValidateHostname(self):
for test in self.VALIDATE_HOSTNAME_TEST_DATA['tests']:
with self.subTest():
self.assertEqual(test['valid'], IRCCommonProtocol.is_server_name(test['host']),
"Failed test for %r; should be %s" % (test['host'], test['valid']))
# N.B. skipping msg-join tests because PyLink doesn't think about messages that way
### Custom test cases
def testMessageSplitSpaces(self):
# Test that tokenization ignores empty fields, but doesn't strip away other types of whitespace
f = IRCCommonProtocol.parse_prefixed_args
self.assertEqual(f(":foo PRIVMSG #test :message"), ["foo", "PRIVMSG", "#test", "message"])
self.assertEqual(f(":123LOLWUT NICK cursed\u3000nickname"), ["123LOLWUT", "NICK", "cursed\u3000nickname"])
self.assertEqual(f(":123LOLWUT MODE ## +ov \x1f checking"),
["123LOLWUT", "MODE", "##", "+ov", "\x1f", "checking"])
self.assertEqual(f(":123LOLWUT MODE ## +ov \u3000 checking"),
["123LOLWUT", "MODE", "##", "+ov", "\u3000", "checking"])
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,38 @@
import unittest
import unittest.mock
from pylinkirc.protocols import clientbot
from pylinkirc.classes import User
import protocol_test_fixture as ptf
class ClientbotProtocolTest(ptf.BaseProtocolTest):
proto_class = clientbot.ClientbotWrapperProtocol
def setUp(self):
super().setUp()
self.p.pseudoclient = self._make_user('PyLink', uid='ClientbotInternal@0')
def test_get_UID(self):
u_internal = self._make_user('you', uid='100')
check = lambda inp, expected: self.assertEqual(self.p._get_UID(inp), expected)
# External clients are returned by the matcher
with unittest.mock.patch.object(self.proto_class, 'is_internal_client', return_value=False) as m:
check('you', '100') # nick to UID
check('YOu', '100')
check('100', '100') # already a UID
check('Test', 'Test') # non-existent
# Internal clients are ignored
with unittest.mock.patch.object(self.proto_class, 'is_internal_client', return_value=True) as m:
check('you', 'you')
check('YOu', 'YOu')
check('100', '100') # already a UID
check('Test', 'Test') # non-existent
# In the future we will have protocol specific test cases here
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,13 @@
import unittest
from pylinkirc.protocols import inspircd
import protocol_test_fixture as ptf
class InspIRCdProtocolTest(ptf.BaseProtocolTest):
proto_class = inspircd.InspIRCdProtocol
# In the future we will have protocol specific test cases here
if __name__ == '__main__':
unittest.main()

86
test/test_protocol_p10.py Normal file
View File

@ -0,0 +1,86 @@
"""
Tests for protocols/p10
"""
import unittest
from pylinkirc.protocols import p10
class P10UIDGeneratorTest(unittest.TestCase):
def setUp(self):
self.uidgen = p10.P10UIDGenerator('HI')
def test_initial_UID(self):
expected = [
"HIAAA",
"HIAAB",
"HIAAC",
"HIAAD",
"HIAAE",
"HIAAF"
]
self.uidgen.counter = 0
actual = [self.uidgen.next_uid() for i in range(6)]
self.assertEqual(expected, actual)
def test_rollover_first_lowercase(self):
expected = [
"HIAAY",
"HIAAZ",
"HIAAa",
"HIAAb",
"HIAAc",
"HIAAd",
]
self.uidgen.counter = 24
actual = [self.uidgen.next_uid() for i in range(6)]
self.assertEqual(expected, actual)
def test_rollover_first_num(self):
expected = [
"HIAAz",
"HIAA0",
"HIAA1",
"HIAA2",
"HIAA3",
"HIAA4",
]
self.uidgen.counter = 26*2-1
actual = [self.uidgen.next_uid() for i in range(6)]
self.assertEqual(expected, actual)
def test_rollover_second(self):
expected = [
"HIAA8",
"HIAA9",
"HIAA[",
"HIAA]",
"HIABA",
"HIABB",
"HIABC",
"HIABD",
]
self.uidgen.counter = 26*2+10-2
actual = [self.uidgen.next_uid() for i in range(8)]
self.assertEqual(expected, actual)
def test_rollover_third(self):
expected = [
"HIE]9",
"HIE][",
"HIE]]",
"HIFAA",
"HIFAB",
"HIFAC",
]
self.uidgen.counter = 5*64**2 - 3
actual = [self.uidgen.next_uid() for i in range(6)]
self.assertEqual(expected, actual)
def test_overflow(self):
self.uidgen.counter = 64**3-1
self.assertTrue(self.uidgen.next_uid())
self.assertRaises(RuntimeError, self.uidgen.next_uid)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,70 @@
"""
Tests for protocols/ts6_common
"""
import unittest
from pylinkirc.protocols import ts6_common
class TS6UIDGeneratorTest(unittest.TestCase):
def setUp(self):
self.uidgen = ts6_common.TS6UIDGenerator('123')
def test_initial_UID(self):
expected = [
"123AAAAAA",
"123AAAAAB",
"123AAAAAC",
"123AAAAAD",
"123AAAAAE",
"123AAAAAF",
]
self.uidgen.counter = 0
actual = [self.uidgen.next_uid() for i in range(6)]
self.assertEqual(expected, actual)
def test_rollover_first_num(self):
expected = [
"123AAAAAY",
"123AAAAAZ",
"123AAAAA0",
"123AAAAA1",
"123AAAAA2",
"123AAAAA3",
]
self.uidgen.counter = 24
actual = [self.uidgen.next_uid() for i in range(6)]
self.assertEqual(expected, actual)
def test_rollover_second(self):
expected = [
"123AAAAA8",
"123AAAAA9",
"123AAAABA",
"123AAAABB",
"123AAAABC",
"123AAAABD",
]
self.uidgen.counter = 36 - 2
actual = [self.uidgen.next_uid() for i in range(6)]
self.assertEqual(expected, actual)
def test_rollover_third(self):
expected = [
"123AAAE98",
"123AAAE99",
"123AAAFAA",
"123AAAFAB",
"123AAAFAC",
]
self.uidgen.counter = 5*36**2 - 2
actual = [self.uidgen.next_uid() for i in range(5)]
self.assertEqual(expected, actual)
def test_overflow(self):
self.uidgen.counter = 36**6-1
self.assertTrue(self.uidgen.next_uid())
self.assertRaises(RuntimeError, self.uidgen.next_uid)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,13 @@
import unittest
from pylinkirc.protocols import unreal
import protocol_test_fixture as ptf
class UnrealProtocolTest(ptf.BaseProtocolTest):
proto_class = unreal.UnrealProtocol
# In the future we will have protocol specific test cases here
if __name__ == '__main__':
unittest.main()

View File

@ -3,8 +3,10 @@ Test cases for utils.py
""" """
import unittest import unittest
from pylinkirc import utils from pylinkirc import utils
class UtilsTestCase(unittest.TestCase): class UtilsTestCase(unittest.TestCase):
def test_strip_irc_formatting(self): def test_strip_irc_formatting(self):
@ -187,6 +189,87 @@ class UtilsTestCase(unittest.TestCase):
utils.parse_duration("4s3d") utils.parse_duration("4s3d")
utils.parse_duration("1m5w") utils.parse_duration("1m5w")
def test_match_text(self):
f = utils.match_text # glob, target
# Base cases
self.assertTrue(f("", ""))
self.assertFalse(f("test", ""))
self.assertFalse(f("", "abcdef"))
self.assertFalse(f("", "*")) # specified the wrong way
self.assertFalse(f("", "?"))
self.assertTrue(f("foo", "foo"))
self.assertFalse(f("foo", "bar"))
self.assertFalse(f("foo", "food"))
# Test use of *
self.assertTrue(f("*", "b"))
self.assertTrue(f("*", "abc"))
self.assertTrue(f("*", ""))
self.assertTrue(f("*!*@*", "nick!user@host"))
self.assertTrue(f("*@*", "rick!user@lost"))
self.assertTrue(f("ni*!*@*st", "nick!user@roast"))
self.assertFalse(f("nick!*abcdef*@*st*", "nick!user@roast"))
self.assertTrue(f("*!*@*.overdrive.pw", "abc!def@abc.users.overdrive.pw"))
# Test use of ?
self.assertTrue(f("?", "b"))
self.assertFalse(f("?", "abc"))
self.assertTrue(f("Guest?????!???irc@users.overdrive.pw", "Guest12567!webirc@users.overdrive.pw"))
self.assertFalse(f("Guest????!webirc@users.overdrive.pw", "Guest23457!webirc@users.overdrive.pw"))
def test_match_text_complex(self):
f = utils.match_text # glob, target
# Test combination of * and ?
for glob in {"*?", "?*"}:
self.assertTrue(f(glob, "a"))
self.assertTrue(f(glob, "ab"))
self.assertFalse(f(glob, ""))
self.assertTrue(f("ba*??*ll", "basketball"))
self.assertFalse(f("ba*??*ll", "ball"))
self.assertFalse(f("ba*??*ll", "basketballs"))
self.assertTrue(f("**", "fooBarBaz"))
self.assertTrue(f("*?*?*?*", "cat"))
self.assertTrue(f("*??****?*", "cat"))
self.assertFalse(f("*??****?*?****", "MAP"))
def test_match_text_casemangle(self):
f = utils.match_text # glob, target, manglefunc
# We are case insensitive by default
self.assertTrue(f("Test", "TEST"))
self.assertTrue(f("ALPHA*", "alphabet"))
# But we can override this preference
self.assertFalse(f("Test", "TEST", None))
self.assertFalse(f("*for*", "BEForE", None))
self.assertTrue(f("*corn*", "unicorns", None))
# Or specify some other filter func
self.assertTrue(f('005', '5', lambda s: s.zfill(3)))
self.assertTrue(f('*0*', '14', lambda s: s.zfill(6)))
self.assertFalse(f('*9*', '14', lambda s: s.zfill(13)))
self.assertTrue(f('*chin*', 'machine', str.upper))
def test_merge_iterables(self):
f = utils.merge_iterables
self.assertEqual(f([], []), [])
self.assertEqual(f({}, {}), {})
self.assertEqual(f(set(), set()), set())
self.assertEqual(f([1,2], [4,5,6]), [1,2,4,5,6])
self.assertEqual(f({'a': 'b'}, {'c': 'd', 'e': 'f'}),
{'a': 'b', 'c': 'd', 'e': 'f'})
self.assertEqual(f({0,1,2}, {1,3,5}),
{0,1,2,3,5})
with self.assertRaises(ValueError):
f([1,2,3], {'a': 'b'}) # mismatched type
with self.assertRaises(ValueError):
f([], set()) # mismatched type
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -5,19 +5,29 @@ This module contains various utility functions related to IRC and/or the PyLink
framework. framework.
""" """
import string
import re
import importlib
import os
import collections
import argparse import argparse
import collections
import functools
import importlib
import ipaddress import ipaddress
import os
from .log import log import re
from . import world, conf, structures import string
# Load the protocol and plugin packages. # Load the protocol and plugin packages.
from pylinkirc import protocols, plugins from pylinkirc import plugins, protocols
from . import conf, structures, world
from .log import log
__all__ = ['PLUGIN_PREFIX', 'PROTOCOL_PREFIX', 'NORMALIZEWHITESPACE_RE',
'NotAuthorizedError', 'InvalidArgumentsError', 'ProtocolError',
'add_cmd', 'add_hook', 'expand_path', 'split_hostmask',
'ServiceBot', 'register_service', 'unregister_service',
'wrap_arguments', 'IRCParser', 'strip_irc_formatting',
'remove_range', 'get_hostname_type', 'parse_duration', 'match_text',
'merge_iterables']
PLUGIN_PREFIX = plugins.__name__ + '.' PLUGIN_PREFIX = plugins.__name__ + '.'
PROTOCOL_PREFIX = protocols.__name__ + '.' PROTOCOL_PREFIX = protocols.__name__ + '.'
@ -92,6 +102,8 @@ def split_hostmask(mask):
""" """
nick, identhost = mask.split('!', 1) nick, identhost = mask.split('!', 1)
ident, host = identhost.split('@', 1) ident, host = identhost.split('@', 1)
if not all({nick, ident, host}):
raise ValueError("Invalid user@host %r" % mask)
return [nick, ident, host] return [nick, ident, host]
splitHostmask = split_hostmask splitHostmask = split_hostmask
@ -150,7 +162,7 @@ class ServiceBot():
else: else:
raise NotImplementedError("Network specific plugins not supported yet.") raise NotImplementedError("Network specific plugins not supported yet.")
def join(self, irc, channels, ignore_empty=True): def join(self, irc, channels, ignore_empty=None):
""" """
Joins the given service bot to the given channel(s). "channels" can be 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'). an iterable of channel names or the name of a single channel (type 'str').
@ -160,8 +172,8 @@ class ServiceBot():
marked persistent). This option is automatically *disabled* on networks marked persistent). This option is automatically *disabled* on networks
where we cannot monitor channels that we're not in (e.g. Clientbot). where we cannot monitor channels that we're not in (e.g. Clientbot).
Before PyLink 2.0-alpha3, this function implicitly marks channels i Before PyLink 2.0-alpha3, this function implicitly marked channels it
receives to be persistent - this is no longer the case! receives to be persistent. This behaviour is no longer the case.
""" """
uid = self.uids.get(irc.name) uid = self.uids.get(irc.name)
if uid is None: if uid is None:
@ -173,6 +185,10 @@ class ServiceBot():
if irc.has_cap('visible-state-only'): if irc.has_cap('visible-state-only'):
# Disable dynamic channel joining on networks where we can't monitor channels for joins. # Disable dynamic channel joining on networks where we can't monitor channels for joins.
ignore_empty = False ignore_empty = False
elif ignore_empty is None:
ignore_empty = not (irc.serverdata.get('join_empty_channels',
conf.conf['pylink'].get('join_empty_channels',
False)))
# Specify modes to join the services bot with. # Specify modes to join the services bot with.
joinmodes = irc.get_service_option(self.name, 'joinmodes', default='') joinmodes = irc.get_service_option(self.name, 'joinmodes', default='')
@ -392,7 +408,7 @@ class ServiceBot():
chanlist = namespace.setdefault(irc.name, structures.IRCCaseInsensitiveSet(irc)) chanlist = namespace.setdefault(irc.name, structures.IRCCaseInsensitiveSet(irc))
chanlist.add(channel) chanlist.add(channel)
if try_join: if try_join and irc.has_cap('can-manage-bot-channels'):
self.join(irc, [channel]) self.join(irc, [channel])
def remove_persistent_channel(self, irc, namespace, channel, try_part=True, part_reason=''): def remove_persistent_channel(self, irc, namespace, channel, try_part=True, part_reason=''):
@ -401,7 +417,7 @@ class ServiceBot():
""" """
chanlist = self.dynamic_channels[namespace][irc.name].remove(channel) chanlist = self.dynamic_channels[namespace][irc.name].remove(channel)
if try_part and irc.connected.is_set(): if try_part and irc.connected.is_set() and irc.has_cap('can-manage-bot-channels'):
self.part(irc, [channel], reason=part_reason) self.part(irc, [channel], reason=part_reason)
def get_persistent_channels(self, irc, namespace=None): def get_persistent_channels(self, irc, namespace=None):
@ -817,3 +833,48 @@ def parse_duration(text):
raise ValueError("Failed to parse duration string %r" % text) raise ValueError("Failed to parse duration string %r" % text)
return result return result
@functools.lru_cache(maxsize=1024)
def _glob2re(glob):
"""Converts an IRC-style glob to a regular expression."""
patt = ['^']
for char in glob:
if char == '*' and patt[-1] != '*': # Collapse ** into *
patt.append('.*')
elif char == '?':
patt.append('.')
else:
patt.append(re.escape(char))
patt.append('$')
return ''.join(patt)
def match_text(glob, text, filterfunc=str.lower):
"""
Returns whether glob matches text. If filterfunc is specified, run filterfunc on glob and text
before preforming matches.
"""
if filterfunc:
glob = filterfunc(glob)
text = filterfunc(text)
return re.match(_glob2re(glob), text)
def merge_iterables(A, B):
"""
Merges the values in two iterables. A and B must be of the same type, and one of the following:
- list: items are combined as A + B
- set: items are combined as A | B
- dict: items are combined as {**A, **B}
"""
if type(A) != type(B):
raise ValueError("inputs must be the same type")
if isinstance(A, list):
return A + B
elif isinstance(A, set):
return A | B
elif isinstance(A, dict):
return {**A, **B}

View File

@ -2,9 +2,13 @@
world.py: Stores global variables for PyLink, including lists of active IRC objects and plugins. world.py: Stores global variables for PyLink, including lists of active IRC objects and plugins.
""" """
from collections import defaultdict, deque
import threading import threading
import time import time
from collections import defaultdict, deque
__all__ = ['testing', 'hooks', 'networkobjects', 'plugins', 'services',
'exttarget_handlers', 'started', 'start_ts', 'shutting_down',
'source', 'fallback_hostname', 'daemon']
# This indicates whether we're running in tests mode. What it actually does # This indicates whether we're running in tests mode. What it actually does
# though is control whether IRC connections should be threaded or not. # though is control whether IRC connections should be threaded or not.