3
0
mirror of https://github.com/jlu5/PyLink.git synced 2025-08-02 10:57:22 +02:00

Compare commits

...

2044 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
James Lu
ed50202cf2 Release PyLink 2.0-rc1 2018-07-18 17:46:06 -07:00
James Lu
9273dd459b README: removing PPA support due to lack of demand
[skip ci]
2018-07-14 21:29:52 -07:00
James Lu
9a5e67412e README: minor rewording, fix markdown syntax
[skip ci]
2018-07-14 21:17:11 -07:00
James Lu
f1ce8351b9 Quick refresh of exttarget docs
- Mention that the $pylinkirc: prefix is implied as of 2.0
- Link to the exttargets reference in example-conf
- docs/exttargets: wording tweaks for the lead section
2018-07-14 21:09:16 -07:00
James Lu
d844ff5186 example-conf: remove references to my IRC nick & the old repository address 2018-07-14 21:09:16 -07:00
James Lu
44aa9af235 Get rid of example-permissions.yml, it's been a long-standing source of confusion 2018-07-14 21:09:16 -07:00
James Lu
6b65ab5f88
README: remove reference to deprecated relay_no_ips option
[skip ci]
2018-07-14 20:53:28 -07:00
James Lu
2246aea13c relay-quickstart: clarify the problem of duplicate network links some more
[skip ci]
2018-07-14 15:11:15 -07:00
James Lu
584b7e3712 RELNOTES: first pass at summarizing all of 2.0's changes (compared to 1.3.x)
[skip ci]
2018-07-14 15:09:11 -07:00
James Lu
bf1f8210bd relay-quickstart: also list ChanFix in incompatible services
[skip ci]
2018-07-14 15:08:55 -07:00
James Lu
a7196d7b79 example-conf: remove ALPHA tag from antispam 2018-07-11 22:56:47 -07:00
James Lu
16ac91a718 Merge the long-awaited 2.0 branch into master
Merge branch 'devel'

Conflicts:
	RELNOTES.md
	VERSION
	classes.py
	conf.py
	coremods/control.py
	coremods/corecommands.py
	coremods/service_support.py
	docs/advanced-relay-config.md
	docs/faq.md
	example-conf.yml
	launcher.py
	plugins/global.py
	plugins/relay.py
	plugins/relay_clientbot.py
	protocols/p10.py
	utils.py
2018-07-11 22:45:52 -07:00
James Lu
d356b53425 docs/t/hooks-reference,pmodule-spec: bump doc version to 2.0.0
[skip ci]
2018-07-11 22:40:29 -07:00
James Lu
0199daec76 RELNOTES: first draft of a 2.0-rc1 changelog
[skip ci]
2018-07-11 22:40:20 -07:00
James Lu
a1783ed2be bots: "bots.joinclient" should be "bots.join" (matching the command name) 2018-07-11 22:16:05 -07:00
James Lu
b5884d4cb3 docs/permissions-reference: s/Allows access to /Grants access to /g
This wording is really just clearer.

[skip ci]
2018-07-11 21:59:52 -07:00
James Lu
0eb0c49cb1 docs/permissions-reference: resort most sections alphabetically
[skip ci]
2018-07-11 21:59:10 -07:00
James Lu
04b17c30e3 docs/permissions-reference: resort Relay permissions by default assignment
[skip ci]
2018-07-11 21:56:39 -07:00
James Lu
f9611ef6bc relay: grant CHANDESC permissions to opers if allow_free_oper_links is true 2018-07-11 21:56:29 -07:00
James Lu
d8c1511b28 docs/permissions-reference: refresh for 2.0
Closes #501.

[skip ci]
2018-07-11 21:38:47 -07:00
James Lu
a5b77c18dd docs/t/writing-plugins: document hook priorities with some examples
Closes #595.

[skip ci]
2018-07-11 21:23:03 -07:00
James Lu
3208782225 docs: readd pylink-opers.md as a redirect to relay-quickstart.md
[skip ci]
2018-07-11 18:59:16 -07:00
James Lu
5b321f9f6f relay-quickstart: explicitly write PyLink 2.0 instead of "2.0"
[skip ci]
2018-07-10 19:00:51 -07:00
James Lu
d3fc95953e relay-quickstart: fix some typos / unclear wording
[skip ci]
2018-07-10 18:59:31 -07:00
James Lu
d310abeec3 faq, relay-quickstart: consistently capitalize Relay as a proper noun
[skip ci]
2018-07-10 18:55:33 -07:00
James Lu
66125530ef faq: discuss how relay handles kills, modes, and server bans (G/K/ZLINE)
Closes #619.

[skip ci]
2018-07-10 18:53:11 -07:00
James Lu
1b26c17d81 docs/channel-modes: add missing entry for auditorium (inspircd +u)
[skip ci]
2018-07-10 18:53:11 -07:00
James Lu
aacb65ab9e relay-quickstart: rewrap, fix typo (at the latest -> at a minimum)
[skip ci]
2018-07-10 18:53:11 -07:00
James Lu
71a24b8b9f relay: remove noctcp, regdeaf, stripcolor from whitelisted umodes
Filter-type umodes don't work properly with relay yet.
2018-07-10 18:09:24 -07:00
James Lu
5ffc629bce plugins/example: update word wrap note
As of PyLink 2.0, long paragraphs are automatically word-wrapped by irc.reply().
2018-07-10 18:04:25 -07:00
James Lu
bba235bba2 Update GitHub repository address 2018-07-08 12:54:10 -07:00
James Lu
c1f37c2236 relay: don't allow servers to forward KILLs
Closes #621.
2018-07-08 12:49:30 -07:00
James Lu
86b93ea969 docs: rewrite the Relay Quick Start Guide (#619)
- Rename pylink-opers.md -> relay-quickstart.md to better reflect its contents.
- Add a section regarding services compatibility and which features _not_ to use with Relay
- Document LINKACL whitelists, CHANDESC, CLAIM/LINKACL options introduced in 2.0
- Document reworked KILL handling in 2.0 and why K/G/ZLINE support hasn't been implemented
- Explain Relay concepts in more depth for people not familiar with Janus
2018-07-08 12:30:57 -07:00
James Lu
5a9b870e00 services-api: less awkward wording
[skip ci]
2018-07-02 00:59:29 -07:00
James Lu
a520496f81 writing-plugins: fix markdown syntax errors
[ci skip]
2018-07-02 00:57:41 -07:00
James Lu
86bfda7281 services-api: various wording and typo fixes
[ci skip]
2018-07-02 00:57:36 -07:00
James Lu
a74fe9bf08 writing-plugins: refer to the services API guide for utils.add_cmd features 2018-07-02 00:47:30 -07:00
James Lu
b2e85fa385 services-api: mention command alias support in 2.0-alpha1+ 2018-07-02 00:45:11 -07:00
James Lu
75b0ae6054 example-conf: detail explicitly how "spawn_services: false" affects a plugin's behavior 2018-07-02 00:35:55 -07:00
James Lu
0c55569c1f utils: clarify ServiceBot.(join|part) docstrings 2018-07-02 00:35:40 -07:00
James Lu
0ccaac595b services-api: refresh + expand for 2.0 and the persistent channel rework
Closes #603.

Other changes:
- Move the "Removing services" section to earlier in the doc - it is important!
- Consistently use 'strings' instead of "strings" in example code
- Mention briefly how "spawn_service:" false affects plugins
- Mention the "dynamic" channel handling added in 2.0-alpha3
2018-07-02 00:35:31 -07:00
James Lu
348dc7348c services-api: use snake case function names for (un)register_service()
[skip ci]
2018-07-01 23:36:14 -07:00
James Lu
d015e1e41a PyLink 2.0-beta1 2018-06-27 11:18:43 -07:00
James Lu
8362e5f234 RELNOTES: fix typo, elaborate a bit more on ipshare pools
[skip ci]
2018-06-26 23:37:22 -07:00
James Lu
f90ec284a4 pmodule-spec: document protocol capabilities
Closes #436.
2018-06-26 22:47:39 -07:00
James Lu
bdd568f75b pmodule-spec: consistently refer to protocol module attrs as self.<whatever> instead of irc.<whatever>
[skip ci]
2018-06-26 22:23:35 -07:00
James Lu
a548ae0714 pmodule-spec: reflow, mention self.connected in Special variables
[skip ci]
2018-06-26 22:20:28 -07:00
James Lu
763ffcf903 pmodule-spec: various wording tweaks
[skip ci]
2018-06-26 22:14:38 -07:00
James Lu
c48846727e pmodule-spec: fix link to protocol-modules.svg
[skip ci]
2018-06-26 22:02:09 -07:00
James Lu
420f523dfd writing-plugins: briefly mention log, world, irc.connected, and useful builtin modules
Closes #522.
2018-06-26 21:58:17 -07:00
James Lu
e340f6e9a2 writing-plugins.md: rewrite for clarity for conciseness
Also document the introduction of hook handler return values and irc.error()

[skip ci]
2018-06-26 21:36:17 -07:00
James Lu
e037b927f8 adv-relay-conf: fix reference to wrong relay version
$mode_prefix in particular was only added in 2.x.

[skip ci]
2018-06-26 14:44:56 -07:00
James Lu
086a5f4496 example-conf, relay, utils: replace 2.0-alpha4 references with 2.0-beta1 2018-06-26 14:44:07 -07:00
James Lu
bbf1b34b12 RELNOTES: more proofreading and reordering
[skip ci]
2018-06-20 16:29:07 -07:00
James Lu
a543de9d73 RELNOTES: refine for flow and add some more issue links
[skip ci]
2018-06-20 16:26:17 -07:00
James Lu
5d11774442 RELNOTES: initial draft of a 2.0-beta1 changelog
[skip ci]
2018-06-20 16:17:57 -07:00
James Lu
c8b8762c12 utils: add parse_duration(), which takes in a duration string and returns the equiv. amt of seconds (#504) 2018-06-15 19:40:05 -07:00
James Lu
5e1cb232b0 IRCNetwork: also detect address types from supplied bindhosts 2018-06-15 18:43:00 -07:00
James Lu
26361c4cc9 IRCNetwork: warn when using plaintext links to non-local addresses 2018-06-15 18:30:21 -07:00
James Lu
e5f817fc95 IRCNetwork: suppress "You can enhance...security...[with] ssl_fingerprint" notices when TLS cert validation is enabled 2018-06-15 15:57:45 -07:00
James Lu
ab9df93898 IRCNetwork: more consistent log format when remote certfp is verified 2018-06-15 15:52:04 -07:00
James Lu
b26d75a6a8 IRCNetwork: handle the case when certificate fingerprint checking is on but the remote provides no cert 2018-06-15 15:50:32 -07:00
James Lu
fefd5a1f6b IRCNetwork: raise ssl.CertificateError instead of an in house exception if certfp is mismatched
Also, fix the expected and real fingerprints being logged in the wrong order.
2018-06-15 15:48:09 -07:00
James Lu
68837aa927 example-conf: enable TLS/SSL by default in all the example server blocks 2018-06-15 15:43:20 -07:00
James Lu
17cd7af22d example-conf: reorder sample server options
Move TLS/SSL options higher up; shift pingfreq and autoconnect options further down
2018-06-15 15:41:20 -07:00
James Lu
17f0b09eb2 example-conf: suggest turning on TLS/SSL in the hostname-as-IP example 2018-06-15 15:36:59 -07:00
James Lu
8fa53f60cb example-conf: copy some more autoconnect examples to the clientbot block 2018-06-15 15:32:35 -07:00
James Lu
76c0db15c4 core: merge TLS validation code into IRCNetwork (#592)
Certificate verification is now enabled for all Clientbot networks, but not yet for S2S links (self-signed certs are common here and direct IP links even more so)
2018-06-15 15:29:15 -07:00
James Lu
e38cd0ada2 get_hostname_type: return 0 for hostnames instead of False for consistency 2018-06-15 14:02:30 -07:00
James Lu
4524aebbac clientbot: initial pass of TLS cert validation (#592)
This works OK, but we should make the validation options built-in instead of clientbot-specific.
2018-06-15 02:47:12 -07:00
James Lu
d3125d9a8f core: automatically detect between IPv4 / IPv6 addresses on connect
Closes #212.
2018-06-15 02:43:33 -07:00
James Lu
5ea33baa8e utils: add get_hostname_type() to autodetect address types (#212) 2018-06-15 02:39:20 -07:00
James Lu
040b009fcb clientbot: ignore RPL_ENDOFBANLIST (368) responses for removed channels 2018-06-15 02:19:56 -07:00
James Lu
c3bb0f7aca relay: rework kill->kick forwarding to send from the sender network's subservers
This makes the kick message a lot neater.

Before:
* net1.relay has kicked GL/net1 from #test ((net2.relay) KILL FWD from GL/net2: test)

After:
* net2.relay has kicked GL/net1 from #test (KILL FWD from GL/net2: test)
2018-06-14 14:48:54 -07:00
James Lu
a98dd36810 example-conf: mention kill -> kick forwarding briefly
[skip ci]
2018-06-14 14:44:03 -07:00
James Lu
56c035a1f5 IRCNetwork: fix broken ping timeout handling
Check for ping outs in the ping scheduler instead of the listener... If the connection is dead, the listener won't ever be called.
2018-06-14 01:18:33 -07:00
James Lu
b2421f5e15 IRCNetwork: simplify connection error handling 2018-06-14 01:18:33 -07:00
James Lu
579b5ce93f IRCNetwork: split SSL connection setup into separate functions
* _make_ssl_context(): returns the SSLContext to use on he network (with options set)
* _setup_ssl(): sets up TLS by loading certfile / keyfile and calling wrap_socket()
* _verify_ssl(): implements certificate fingerprint verification, raising TLSVerificationError (a new subclass of ConnectionError) if this fails

This is a prerequisite for #592.
2018-06-14 01:17:39 -07:00
James Lu
8386edc6d5 conf: rename ConfigValidationError -> ConfigurationError & inherit from RuntimeError 2018-06-13 22:46:58 -07:00
James Lu
76b58c4432 relay: log chandesc changes to INFO 2018-06-12 02:36:37 -07:00
James Lu
77fd9475b6 relay: show channel descriptions before "created by" info 2018-06-12 02:33:29 -07:00
James Lu
8c42825612 relay: allow disabling free link access for all opers 2018-06-12 00:26:24 -07:00
James Lu
5617224780 example-conf: roughly sort "relay:" block options by usefulness
[skip ci]
2018-06-12 00:13:54 -07:00
James Lu
deff6d077d match_host: remove deprecation notice for non-host globs
This is too flaky with commands taking arbitrary user inputs (e.g. opercmds/checkban)
2018-06-12 00:02:18 -07:00
James Lu
1b68bfadc6 coremods, plugins, protocols: drop now redundant allowAuthed=False in is_oper() calls 2018-06-11 23:56:44 -07:00
James Lu
2ca9de2ea8 PyLinkNCWUtils: make the allowAuthed, allowOper options to is_oper no-ops 2018-06-11 23:55:19 -07:00
James Lu
18f108c328 PyLinkNCWUtils: remove check_authenticated() (#422) 2018-06-11 23:54:48 -07:00
James Lu
ed5d46e28a utils: remove deprecated is* functions 2018-06-11 23:48:12 -07:00
James Lu
a30921eeb8 classes: remove irc.conf (#422) 2018-06-11 23:44:59 -07:00
James Lu
3bea214cb0 classes: remove irc.botdata (#422) 2018-06-11 23:43:57 -07:00
James Lu
2e3317ce07 relay: explicitly mention forwarding in relayed kill messages (#520) 2018-06-11 19:26:52 -07:00
James Lu
7d56b30582 opercmds: skip verbose formatting of kill reasons for internal targets (#520) 2018-06-11 19:26:51 -07:00
James Lu
5ecbc2750e exec: fix textwrap error caused by passing the wrong type to reply() 2018-06-11 19:26:51 -07:00
James Lu
1a97a32ef5 ircs2s_common: return just the kill reason as text in kill parsing (#520) 2018-06-11 19:26:42 -07:00
James Lu
93fef9b923 relay: use match_text() to check forcetag_nicks globs 2018-06-11 18:51:30 -07:00
James Lu
73d0e153cf relay: support relaying kills (#520)
Instead of always bouncing, kills to a relay client are now handled as follows:

1) If the target and source networks are both in any killshare pool, relay the kill entirely
2) Otherwise, iterate over all channels the kill target is in:
    3) If the killer has claim access in a channel, forward the KILL as a kick
    4) Otherwise, bounce the kill (so far, silently)

TODO: kill messages are currently very cluttered, we should make our parser deliver more concise strings...
* GL|unreal has quit (Killed (chary.relay (KILL from GL/chary: Killed (GL (test)))))
2018-06-11 18:34:52 -07:00
James Lu
9466813ba1 relay: switch to a flexible, pool-based configuration scheme for IP sharing
This deprecates the "relay::show_ips" and network-specific "relay_no_ips" options, replacing it with the "relay::ip_share_pools" list.
2018-06-11 17:29:29 -07:00
James Lu
5f9904126a log: drop client-in-channel requirement for channel logging 2018-06-11 17:26:04 -07:00
James Lu
17ffd1f640 automode: log mass-removals to INFO as well 2018-06-10 14:36:40 -07:00
James Lu
372e7fb405 automode: send delacc confirmations from the right client
Also bold some parts of the output for easier viewing.
2018-06-10 14:31:56 -07:00
James Lu
8608c72b16 automode: allow removing entries by entry numbers
Closes #506.

This isn't the most efficient implementation because Automode entry lists are actually unordered...
So far we're relying on consistent sorting of entries between LISTACC and DELACC, and indrectly removing entries by comparing the entry list with remove_range's output.
2018-06-09 23:12:37 -07:00
James Lu
c919c523dc utils: add remove_range()
"""
    Removes a range string of (one-indexed) items from the list.
    Range strings are indices or ranges of them joined together with a ",":
    e.g. "5", "2", "2-10", "1,3,5-8"

    See test/test_utils.py for more complete examples.
    """
2018-06-09 17:03:40 -07:00
James Lu
f8e3cfa346 antispam: strip IRC formatting by default before processing
Closes #615.
2018-06-09 16:22:14 -07:00
James Lu
ebf7443d97 antispam: add a "block" verb, and make textfilter use it by default
Closes #616.
2018-06-09 16:22:14 -07:00
James Lu
de62b2e77a utils: add strip_irc_formatting() 2018-06-09 16:22:14 -07:00
James Lu
f9d21c2b10 UserMapping: fix wrong args to __copy__ 2018-06-09 10:44:36 -07:00
James Lu
0ae7eb2563 relay_clientbot: allow overriding clientbot styles by network
Closes #455.
2018-06-08 19:15:47 -07:00
James Lu
b1248524a9 relay: raise an error when trying to delink a leaf channel from another leaf network
Previously this would (confusingly) delink the channel from the network the command was called on instead of the intended target.
2018-06-08 18:45:45 -07:00
James Lu
31a0d36990 clientbot: ignore missing args in 324 / RPL_CHANNELMODEIS
Fixes #537.
2018-06-08 18:45:20 -07:00
James Lu
180da83b4e global: reply with a confirmation
We can also use this space to show the number of channels and networks announced to.
2018-06-08 18:28:41 -07:00
James Lu
f82ddb5336 global: allow configuring channels to exempt from announcements
Closes #453.
2018-06-08 18:25:23 -07:00
James Lu
0edbeb7fad global: do not allow sending empty messages 2018-06-08 18:25:23 -07:00
James Lu
f4de604b7d classes: split match_host() into match_host() and match_text() 2018-06-08 18:25:23 -07:00
James Lu
6085b21e48 antispam: normalize logging format in handle_masshighlight 2018-06-08 17:54:32 -07:00
James Lu
10416013e8 example-conf: mention how kick, ban, etc. won't work for non-channel specific events 2018-06-08 17:54:03 -07:00
James Lu
18bc1942e5 antispam: implement text filters with optional PM spam checks
Also refactor the _punish code to account for events without a channel attached.

Closes #359.
2018-06-08 17:49:26 -07:00
James Lu
b0188dab92 get_friendly_name: strip STATUSMSG prefixes before checking is_channel 2018-06-08 17:26:25 -07:00
James Lu
7b744655ee Merge branch 'devel' into wip/antispam-textfilters 2018-06-08 15:57:40 -07:00
James Lu
8cc838e5ca relay: allow "relay.link.force_ts" as an alternate permission to 'link --force' 2018-06-08 15:56:42 -07:00
James Lu
6f3813d3a4 UserMapping: add in missing reference to the parent irc instance 2018-06-08 15:54:06 -07:00
James Lu
06d57a5b28 relay: rename 'link --force' to 'link --force-ts' to better reflect its purpose
Also mention explicitly that this option does not bypass LINKACL and other channel restrictions (e.g. the Clientbot one)
2018-06-07 13:48:38 -07:00
James Lu
88bd9b2791 relay: oops, the op check in 'link' should be specific to clientbot
(cherry picked from commit d4bf407c5d891edad4ab7cd2ca4f6dc71a9f353c)
2018-06-07 13:46:01 -07:00
James Lu
d4bf407c5d relay: oops, the op check in 'link' should be specific to clientbot 2018-06-07 13:44:37 -07:00
James Lu
b202954be4 relay: check permissions before clientbot op status to prevent arbitrary join triggering
This mirrors the fix in 1.x: commit 141e941fcddaffc93906b0b5e7cb632f21dde464
2018-06-07 13:39:16 -07:00
James Lu
fee64ece04 relay: fix clientbot op requirement not being checked if the sender is in the target channel
Also, show a slightly different error when using the command with 'remote', since the clientbot client gets overridden to be the message sender.

This mirrors the fix in 1.x: commit 9578fd5ac306866f3535ee1cf9c5a7e241fc4511
2018-06-07 13:38:52 -07:00
James Lu
141e941fcd relay: check permissions before clientbot op status to prevent arbitrary join triggers 2018-06-07 13:20:30 -07:00
James Lu
9578fd5ac3 relay: fix clientbot op requirement not being checked if the sender is in the target channel 2018-06-07 13:19:53 -07:00
James Lu
1e0b0dbc70 relay: part the PyLink service bot after relaying other parts on 'delink'
This prevents these messages from being overriden by the "Clientbot was force parted" one if the IRCd responds too quickly.
2018-06-07 13:07:51 -07:00
James Lu
e76c48f24c Initial config skeleton for antispam text filters (#359) 2018-06-02 01:05:00 -07:00
James Lu
5b94a10c67 example-conf: rewrap antispam section, typo fix
[skip ci]
2018-06-02 00:57:26 -07:00
James Lu
b120e2d701 antispam: allow individual punishments to fail gracefully when not supported 2018-06-02 00:31:14 -07:00
James Lu
186b73c72d example-conf: more wording tweaks 2018-06-02 00:15:26 -07:00
James Lu
df6562dcff example-conf: add antispam to the available plugins list 2018-06-02 00:09:52 -07:00
James Lu
cb774ac3da example-conf: document antispam::exempt_level 2018-06-02 00:07:20 -07:00
James Lu
e65d84960a antispam: make punishments method-specific 2018-06-02 00:06:21 -07:00
James Lu
8cc4527ff7 example-conf: initial documentation for the antispam plugin 2018-06-01 23:50:50 -07:00
James Lu
83aa3d262c antispam: allow masshighlight blocking to be disabled 2018-06-01 23:48:22 -07:00
James Lu
62fcdf880c relay: support setting freeform channel descriptions for LINKED
Closes #576.
2018-06-01 23:03:31 -07:00
James Lu
32acc27967 relay: tweak the "wrong network" error message for claim, modedelta 2018-06-01 22:48:13 -07:00
James Lu
c8b2a676fd relay: rename CHANNEL_DELINKED_PARTMSG -> CHANNEL_DELINKED_MSG 2018-05-30 12:45:20 -07:00
James Lu
08a7b5c837 relay: remove our persistent channels on unload 2018-05-30 12:43:59 -07:00
James Lu
5ac283f018 ServiceBot: introduce clear_persistent_channels() to clear all persistent channels in a namespace 2018-05-30 12:43:51 -07:00
James Lu
1ab5d614c0 protocols: convert user TS to an int when receiving new users (#594) 2018-05-26 16:26:14 -07:00
James Lu
e3a935d0b7 classes: make User, Channel TS a property for type-safety (#594) 2018-05-26 02:30:25 -07:00
James Lu
f20fa5e995 Add User.get_fields(), and rework plugins to fix $nick broken expansions
User.nick is no longer a writable attribute since a085aed92435c98713173e3e379202caa4fcddd0, so it won't show up in __dict__ now.

get_fields() replaces the User.__dict__ hack various plugins used and also provides some new expansion variables:
- $sid and $server expand to the server ID and name respectively
- $modes and $channels are now preformatted strings
- $netname expands to the network name

$manipulatable and $_irc were removed since their values aren't quite meaningful as strings
2018-05-26 02:12:38 -07:00
James Lu
108d4b86d9 login: fix login for legacy accounts (#590) 2018-05-26 01:27:12 -07:00
James Lu
2df3dc280c commands.showuser: show home server and TS as "N/A" if they are spoofed values 2018-05-26 01:14:32 -07:00
James Lu
b72420a8aa Revert "NetworkCore: return the server in get_server if the arg was a server"
The complete implementation of is_privileged_service() in ec3a94c4ca61f2b9b32dab9a18d590d206cc4484 explicitly checks the entity ID type, so this strange hack is not needed.

This reverts commit 9113b34b46ba6aa2b381b3c7e9822b4f411caaec.
2018-05-26 01:14:32 -07:00
James Lu
ec3a94c4ca Move U:line checking into core as PyLinkNCWUtils.is_privileged_server()
Closes #604.
2018-05-26 00:14:04 -07:00
James Lu
9e936f1612 Rewrite login handling (Closes #590)
* Move identify command and login helpers into coremods.login
   - corecommands._login -> login._irc_try_login
* Add login._get_account() function to consistently fetch login block info
* Rename functions in coremods.login to snake case:
   - checkLogin -> check_login
   - verifyHash -> verify_hash
* Replace explicit returns in login checks with raising utils.NotAuthorizedError()
2018-05-25 23:50:55 -07:00
James Lu
73261a31bd opercmds: forbid killing the main PyLink client 2018-05-21 02:37:53 -07:00
James Lu
b9f782868c inspircd: remove users from the state immediately when sending a kill
This matches Anope and Atheme's behaviours.

Closes #607.
2018-05-21 00:33:13 -07:00
James Lu
a66a9b6336 core: demote KeyError logging in _remove_client to DEBUG
This is needed for #607.
2018-05-21 00:28:16 -07:00
James Lu
c9c937e7a7 relay: block networks not on the claim list from merging in modes when relinking
This can cause channels to be inadvertently set modes that it shouldn't be on a relink (e.g. modes set by services DEFCON), since relay ignores modes from defined u-lines instead of bouncing them.
2018-05-21 00:09:57 -07:00
James Lu
1fb2a90580 relay: log rejected links due to LINKACL to WARNING
Closes #609.
2018-05-20 22:17:23 -07:00
James Lu
20bbf531e6 wrap_message: fall back to bufsize=510 on protocols declaring S2S_BUFSIZE to be 0 (unlimited) 2018-05-18 19:09:06 -07:00
James Lu
559b262db8 core: break attempts to read from a socket if no data is available 2018-05-18 19:08:37 -07:00
James Lu
f87e646f35 core: use the most recent UID if we have an unresolved nick collision / desync
This is the one most likely to be correct, assuming IRCds deals with nick collisions properly on their own.
2018-05-18 18:17:04 -07:00
James Lu
73322bd9ba wrap_message: expand UID targets into nicks before processing (#153) 2018-05-11 14:47:27 -07:00
James Lu
244c4fe0eb classes: return valid channels as-is in get_friendly_name()
This makes it safe to use when processing message targets. (ref #153)
2018-05-11 14:47:18 -07:00
James Lu
0ac5d424d8 core: implement text wrapping in irc.msg()
Closes #153.
2018-05-11 14:38:21 -07:00
James Lu
5d098f57d7 example-conf: relay::tag_nicks = false is no longer experimental
We've been using this in production with 2.0 for quite some time.

[skip ci]
2018-05-11 14:01:27 -07:00
James Lu
13c315c9a2 example-conf: fix a typo (enable_default_claim -> relay_enable_default_claim) 2018-05-11 13:28:57 -07:00
James Lu
28862281fe example-conf: rework some relay config descriptions for consistent style 2018-05-11 13:26:37 -07:00
James Lu
741e2c8ece relay: allow claim to be disabled by default on new channels
Closes #581.
2018-05-11 13:26:13 -07:00
James Lu
fc275cfdca relay: remove service bots joined persistently when the home network disconnects 2018-05-11 13:21:16 -07:00
James Lu
aa4cedd945 relay: allow default LINKACL mode to be configured as an option (#394) 2018-05-11 13:09:54 -07:00
James Lu
fb6aa88d83 Merge branch 'relay-linkacl-whitelist' into devel
Closes #394.
2018-05-11 12:58:45 -07:00
James Lu
77a6d69f29 RELNOTES: more changelog clarifications and fixes for 2.0-alpha3
[skip ci]
2018-05-10 17:42:53 -07:00
James Lu
16f630560e PyLink 2.0-alpha3 2018-05-10 17:34:52 -07:00
James Lu
613e6412a2 networks.remote: properly error if the target service is not available on the target network
Closes #554.
2018-05-10 16:06:16 -07:00
James Lu
30c1980b59 relay: consistently use bold instead of repr in LINKACL output 2018-05-09 23:33:00 -07:00
James Lu
0ae4aea133 relay: add a whitelist mode for LINKACL (#394) 2018-05-09 23:29:56 -07:00
James Lu
f1b3d8d0ad README: typo fix
[skip ci]
2018-05-09 22:47:16 -07:00
James Lu
d1ac33a1af utils: remove references to deprecated irc.proto 2018-05-09 22:44:36 -07:00
James Lu
64a98120bf relay: remove references to deprecated irc.proto 2018-05-09 22:44:17 -07:00
James Lu
3120fa5396 clientbot: stop sending duplicate JOIN hooks
Also set _clientbot_initial_who_received on 315/ENDOFWHO, instead of on the first /who response we get.

Really fixes #551.
2018-05-09 22:31:19 -07:00
James Lu
451db74f0c clientbot: don't send duplicate away statuses 2018-05-09 22:23:14 -07:00
James Lu
f54382534c kick and kill should raise NotImplementedError when not supported by a protocol
Closes #605.
2018-05-09 22:19:03 -07:00
James Lu
b50ae89acc relay: check service bot status before remote user presence
add_persistent_channels() is usable regardless of whether the service bot is ready, so we do not need to break if the remote copy of the service bot doesn't exist.

Closes #606.
2018-05-09 21:44:04 -07:00
James Lu
f3c2149d7a relay: fix variable confusion when managing service bots
We should be checking for service bot presence locally, and applying changes on the *remote* network.
2018-05-09 21:34:17 -07:00
James Lu
0c19b3719e automode: clarify comments to main() slightly 2018-05-09 21:29:20 -07:00
James Lu
c44aa64503 services_support: check service bot state, not is_internal_client before dynamically parting channels
If a channel still has e.g. relay users when the last local user leaves the channel, the services bots should stay put.
2018-05-09 20:47:51 -07:00
James Lu
d46c494351 faq: move service bot issues under a separate heading
[skip ci]

(cherry picked from commit eb078056e17a71d50e6c024460b6c239a61ed4ce)
2018-05-08 13:37:22 -07:00
James Lu
b5133aebbb faq: more minor edits
[skip ci]

(cherry picked from commit 030facdb7591f93f8cee42453a9190492bbf9dc5)
2018-05-08 13:37:22 -07:00
James Lu
940ff357c9 faq: add links to YAML guides, complete with a mini-rant on YAML misconceptions
[skip ci]

(cherry picked from commit 074019b77aff00318b890123aca3c27e05bc2686)
2018-05-08 13:37:22 -07:00
James Lu
a425015c13 faq: link to the new Disabling Colors/Control Codes section in adv-relay-config
(cherry picked from commit 0ce80f0edec5ed5637f482c402b2e4ab6fa59a23)
2018-05-08 13:37:22 -07:00
James Lu
19c9d4031d Add a donate badge
[skip ci]

(cherry picked from commit d0568a6ad73700e1236f2023990c744b07e02b10)
2018-05-08 13:37:22 -07:00
James Lu
83dd37c4e7 Drop pypandoc stuff and use Markdown descriptions on PyPI
(cherry picked from commit 8a09f321da723e8616976e4354b6e179e867976c)
2018-05-08 13:37:22 -07:00
James Lu
f06a8f09b4 docs/adv-relay-config: resync with master
Remaining changes: restore documentation for
  - $mode_prefix in Custom Clientbot Styles
  - The relay_endburst_delay option on InspIRCd networks

[skip ci]
2018-05-08 13:36:20 -07:00
James Lu
83a7f14b5a PyLink 1.3.0 2018-05-08 13:16:46 -07:00
James Lu
eb078056e1 faq: move service bot issues under a separate heading
[skip ci]
2018-05-06 12:20:49 -07:00
James Lu
030facdb75 faq: more minor edits
[skip ci]
2018-05-06 12:19:08 -07:00
James Lu
074019b77a faq: add links to YAML guides, complete with a mini-rant on YAML misconceptions
[skip ci]
2018-05-06 12:12:17 -07:00
James Lu
0ce80f0ede faq: link to the new Disabling Colors/Control Codes section in adv-relay-config 2018-05-06 11:44:18 -07:00
James Lu
09d2f99855 RELNOTES: refresh with more 2.0-alpha3 changes
[skip ci]
2018-05-06 11:39:48 -07:00
James Lu
2f6c8d2938 Revert "relay: shortcut get_remote_user some more; only grab spawn lock if the user doesn't exist"
This seems to have caused sporadic duplicate user spawns once more (#602)

This reverts commit 0bc24c94b265a68b9830cde42de1ff0c29b223af.
2018-05-05 23:17:24 -07:00
James Lu
c71e9a6410 Merge branch 'services-v3' into devel
- Revamp persistent channel registration to be plugin specific, effectively working around relay-services conflicts (closes #265)
- New abstraction in ServiceBot: add/remove_persistent_channel() to manage persistent channels independently of explicit joins and parts
- Introduce ServiceBot.part(), which sends a part "request" that succeeds only if a channel is not marked persistent by any plugin
- Replace ServiceBot.join() calls with the new registration mechanism, which queues joins instead of dropping them if the service client is not yet ready (closes #596)
2018-05-05 23:17:10 -07:00
James Lu
5a0cb9a4ff automode: add/remove persistent channels on set/del/clearacc 2018-05-05 21:51:27 -07:00
James Lu
8aa67b93fa automode: also rejoin DB channels on reload 2018-05-05 21:10:08 -07:00
James Lu
bf4863eb6d relay, ServiceBot: remove dead code 2018-05-05 13:20:55 -07:00
James Lu
72c2fa38e9 relay: consistently use "Channel delinked." as part message for service bots too 2018-05-05 13:19:57 -07:00
James Lu
c7da7f0025 ServiceBot: allow sending service parts with reasons 2018-05-05 13:19:43 -07:00
James Lu
61d7bf18d3 relay: also attempt to part the PyLink service bot on delink 2018-05-05 13:15:17 -07:00
James Lu
2532042506 services_support: raise endburst handler priority
Other plugins may implement endburst handlers assuming that their service bots have already been created.
2018-05-05 13:07:28 -07:00
James Lu
01c22aac21 Revert "service_support: consistently rejoin all channels on kick and kill"
This is no longer needed with plugin-specific persistent channels.

This reverts commit 3c9dac7e6b2bfd20586a4fea9db40172e537344a.
2018-05-05 13:07:07 -07:00
James Lu
92be421fad relay: attempt to remove persistent channels on delink as well
Also, wrap remove_persistent_channel calls with a try/except when they may fail.
2018-05-05 12:57:17 -07:00
James Lu
8994811f54 relay: further fixes for persistent channels
Also, merge the 'relay_local' and 'relay_remote' namespaces into one.
2018-05-05 12:52:00 -07:00
James Lu
e9fe15bd7d [WIP] Further revise the persistent channels implementation
- Make dynamic_channels per plugin as well as per network to work around relay-service conflicts (#265)
- Introduce ServiceBot.(add|remove)_persistent_channel() to add/remove persistent channels and optionally join/part them
- Introduce ServiceBot.part(), which checks remaining persistent channels list and parts a channel only if it is still not marked persistent
- Refactor automode to autojoin channels on ENDBURST instead of plugin load
- Refactor relay to manage persistent channels on join/part/(de)init, both locally (namespace 'relay_local') and remotely (namespace 'relay_remote')
2018-05-04 22:52:26 -07:00
James Lu
b46ab844fe classes: really fix KeyError crashes on duplicate QUIT 2018-04-30 11:11:14 -07:00
James Lu
7476c6cf05 ServiceBot: log access denials to warning - closes #593
(backported from commit 655221491cdce25ae292ebfc36721f2ef781b6aa)
2018-04-25 23:19:29 -07:00
James Lu
8eba402a33 clientbot: drop pre-WHO join bursting with userhost-in-names, it's too unreliable
Closes #602.
Closes #551.
2018-04-24 12:39:29 -07:00
James Lu
d0568a6ad7
Add a donate badge
[skip ci]
2018-04-21 13:28:34 -07:00
James Lu
8a09f321da Drop pypandoc stuff and use Markdown descriptions on PyPI 2018-04-21 11:08:39 -07:00
James Lu
e96081aa6e PyLinkNetworkCore: make deletion from self.users non-fatal 2018-04-20 19:39:35 -07:00
James Lu
af744123e6 automode: join channels regardless of whether they're empty 2018-04-20 19:35:06 -07:00
James Lu
0ead868546 service_support: skip dynamic join/part hooks on bot-only servers 2018-04-20 19:35:06 -07:00
James Lu
4bfcd52731 RELNOTES: fix formatting of old versions' changelogs
[skip ci]

(cherry picked from commit ce82c231fef41c24a4488f6e7714e23f4bfa0bea)
2018-04-17 12:03:42 -07:00
James Lu
9967b7d9fe adv-relay-config: fix up in-page link [skip ci] 2018-04-17 11:52:09 -07:00
James Lu
041b6aecb6 adv-relay-config: more revisions (closes #597) [skip ci]
commit c82dcb95a6fa1584104a258c9772845b5549b35e
Author: James Lu <james@overdrivenetworks.com>
Date:   Tue Apr 17 11:49:44 2018 -0700

    adv-relay-config: more cleanup and stylistic tweaks

commit 515974a54a3a5970b4b78569e395ba69b7ee2ebc
Author: James Lu <james@overdrivenetworks.com>
Date:   Tue Apr 17 11:45:02 2018 -0700

    adv-relay-config: revert sorting events

    I'd rather have this match the code 1 to 1.

commit c63a298bd44b341831c2492d667becac67b42b42
Author: Ken Spencer <me@iotaspencer.me>
Date:   Tue Apr 17 02:15:31 2018 -0400

    adv-relay-config: change a bit of formatting (#597)

commit aa9577e8300d30713249e054905e6975897e1965
Author: Ken Spencer <me@iotaspencer.me>
Date:   Tue Apr 17 02:09:31 2018 -0400

    adv-relay-config: add more about coloring, sort events
2018-04-17 11:50:28 -07:00
James Lu
89a7f59262 adv-relay-config: various wording adjustments 2018-04-16 22:09:01 -07:00
James Lu
236d800b47 adv-relay-config: move the supported events slightly lower (#597)
[skip ci]
2018-04-16 22:01:49 -07:00
Ken Spencer
601259c771 adv-relay-config: fix up and change table to markdown (#600) 2018-04-16 21:56:20 -07:00
Ken Spencer
5623c06b8f adv-relay-config: fix colons so they match example-conf (#599)
First part of #597.
2018-04-16 20:33:10 -07:00
Ken Spencer
78d1d20856 adv-relay-config: fix colons so they match example-conf (#598)
First part of #597.
2018-04-16 20:27:00 -07:00
James Lu
281ac7aa31 antispam: add a workaround for clientbot support (#359) 2018-04-14 11:40:19 -07:00
James Lu
47052a3bba antispam: use kick+ban as default punishment 2018-04-13 22:18:41 -07:00
James Lu
3e07239db4 antispam: don't kill users if they quit after previous punishments 2018-04-13 22:18:03 -07:00
James Lu
dd8f9411b6 antispam: filter protection-triggering messages from reaching other plugins (#359) 2018-04-13 22:15:24 -07:00
James Lu
3825b93dee Initial pass of a mass-highlight blocking plugin (#359) 2018-04-13 22:08:37 -07:00
James Lu
4d9fbc55ba Merge branch 'wip/dynamic-services' into devel 2018-04-13 22:08:28 -07:00
James Lu
3c9dac7e6b service_support: consistently rejoin all channels on kick and kill
This is the cheap fix to part one of #265: "When a services client is killed, it won't join any relay leaf channels"
2018-04-13 20:34:26 -07:00
James Lu
4cdc19ac78 relay: fix is_internal_client call 2018-04-13 20:31:56 -07:00
James Lu
2f12a5b710 Initial work on dynamic service bot joining / parting (#265) 2018-04-12 12:45:33 -07:00
James Lu
2cb4a06e64 commands: report times in showuser, showchan in UTC
XXX: we could perhaps use abstraction to this since we're repetitively chaining commands
XXX: 'ts' is not type-safe yet, some protocol modules are storing it as a string?!
2018-04-12 10:58:09 -07:00
James Lu
25d24e9bb2 Use non-blocking sockets again, since Linux select() doesn't guarantee that recv() won't block 2018-04-12 10:58:09 -07:00
James Lu
bbc7a981ba example-conf: tweak some more defaults for clarity
(cherry picked from commit 66ec3d9755943bab7fe17a177c0873ecec4b32ad)

Conflicts:
	example-conf.yml
2018-04-10 22:31:24 -07:00
James Lu
c82b12d9d6 example-conf: clarify nick/ident setting on clientbot
(cherry picked from commit 170d793939bf64931f474e4977225101870ea7f0)
2018-04-10 22:29:43 -07:00
James Lu
c7159b9cad core: move clearing channels on kick/part to coremods/handlers
This is for consistency with the rest of the state cleanup code.
2018-04-07 22:44:00 -07:00
James Lu
16faac83eb core: delete empty permanent channels when -P is set on them 2018-04-07 22:38:19 -07:00
James Lu
6bcf7d325f PyLinkNetworkCore: add get_service_option()
Closes #574.
2018-04-07 22:20:35 -07:00
James Lu
138a52611e classes: oops, actually tell the queue thread to abort before we wait for it to stop 2018-04-07 22:12:17 -07:00
James Lu
80cbd7a257 classes: make disconnections more synchronized
- Make ping timer abort instantly if the network is dead
- Shut down the read and write parts of the socket separately, and only close the socket once both parts are done.
2018-04-07 21:56:10 -07:00
James Lu
84dbca4bda classes: thread socket connects once more since they block 2018-04-07 21:56:06 -07:00
James Lu
655221491c ServiceBot: log access denials to warning - closes #593 2018-04-07 20:22:55 -07:00
James Lu
0202d88124 PyLink 1.3-beta1 2018-04-07 16:40:24 -07:00
James Lu
66ec3d9755 example-conf: tweak some more defaults for clarity 2018-04-07 15:44:44 -07:00
James Lu
170d793939 example-conf: clarify nick/ident setting on clientbot 2018-04-07 15:37:42 -07:00
Austin Ellis
3266e1a430 plugins/automode: fix SETACC example (#507)
Small fix to plugins/automode SETACC example given in help output.

(cherry picked from commit fa0dd100e57a5ceb7eba0cf0daea3e1f65eb6398)
2018-04-07 15:16:10 -07:00
James Lu
c33f7437ef Rename servers::<netname>::server_suffix -> servers::<netname>::relay_server_suffix
(cherry picked from commit f75b1eb3566c02800919d861dc2e9f136e87b324)

Conflicts:
	example-conf.yml
	plugins/relay.py
2018-04-04 12:44:14 -07:00
James Lu
5339ddcf08 example-conf: reword description for servers::<netname>::relay_forcetag_nicks
(cherry picked from commit ba4e0aed851d6ee0c3dba56584d9b67e7447b978)
2018-04-04 12:40:28 -07:00
James Lu
95f806dc46 relay: allow defining server-specific nicks to always tags
Closes #564.

(cherry picked from commit 60c05af9ed30c0f4407cc8b2d72bdc2cc808a507)

Conflicts:
	example-conf.yml
2018-04-04 12:40:01 -07:00
Mitchell Cooper
3e16469b25 allow realname to be specified the same way
(cherry picked from commit a1dfa14d201a350dec38a0aa98cf86694cdcd4f0)

Conflicts:
	coremods/service_support.py
	example-conf.yml
2018-04-04 12:35:48 -07:00
Mitchell Cooper
95dbacdba6 allow host to be specified in service bot block or per-network
(cherry picked from commit 33630e8f9dc5152425622d885ff31a6201eef2c0)
2018-04-04 12:32:58 -07:00
Mitchell Cooper
8200d92d23 relay: add server-specific server_suffix (closes #462) (#484)
(cherry picked from commit c92bb1e33b8787d4b2f34be8e56900d94fe6f497)

Conflicts:
	example-conf.yml
2018-04-04 12:30:02 -07:00
James Lu
f75b1eb356 Rename servers::<netname>::server_suffix -> servers::<netname>::relay_server_suffix 2018-04-03 17:43:02 -07:00
James Lu
7586989763 classes: really quash duplicate disconnect calls as much as possible 2018-03-31 11:53:50 -07:00
James Lu
d6952f0361 handlers: fix names of state cleanup function
Where did "stats cleanup" even come from?!
2018-03-31 00:09:30 -07:00
James Lu
ac4296b56b README: the Ubuntu nightlies PPA is no longer supported for trusty / 14.04
[skip ci]
2018-03-31 00:00:30 -07:00
James Lu
57d7a70933 launcher: mention that -c is explicitly kept as a no-op for PyLink <= 1.2.x
(cherry picked from commit c0a061eff3c5e82ec9ed43aff1c8609d075d9d34)

Conflicts:
	launcher.py
2018-03-30 23:52:19 -07:00
James Lu
c0a061eff3 launcher: mention that -c is explicitly kept as a no-op for PyLink <= 1.2.x 2018-03-30 23:44:47 -07:00
James Lu
b14f52b547 unreal: bump protocol version to 4017 (no changes needed)
(cherry picked from commit a2783d74c56cdd007070df7ef44ca18cb4a0ddb4)
2018-03-30 23:41:27 -07:00
James Lu
481561f972 control: don't spew "Stopping plugins" notices if none are loaded
(cherry picked from commit d818c170726cf343678a0e98b8fac1e527df4a58)
2018-03-30 23:27:38 -07:00
James Lu
7dec2d15db launcher: daemonize and write PID file only after reading control options (-s/-R/-r)
(cherry picked from commit b9a66244f04e12509271feadf5d30d7bce2e81c9)
2018-03-30 23:24:41 -07:00
James Lu
b9a66244f0 launcher: daemonize and write PID file only after reading control options (-s/-R/-r) 2018-03-30 23:23:10 -07:00
James Lu
2ddb0ee18f launcher: add experimental daemonization support
Closes #187.

(cherry picked from commit 0b0da2cfe63819bf012bf875d1399455f8647b94)
2018-03-30 23:07:30 -07:00
James Lu
7d5d5a385e control: handle SIGUSR1 as well as SIGHUP as rehash
This is used by the backported launcher for rehash.
2018-03-30 23:06:58 -07:00
James Lu
b5735702f7 Backport the launcher as of commit 8321485315c9e83c1a45a053ec51ce81cd7977cc to 1.3
This adds support for stale PID file checking (#182), as well as the --rehash/--stop/--restart options
2018-03-30 23:06:01 -07:00
James Lu
79ff9f23fb setup: update PyPI classifiers
- Declare Python 3.6 support
- Change development status to Production/Stable

(cherry picked from commit dbc9d1690d46d01a56b0333624bcca3956423af1)
2018-03-30 22:44:31 -07:00
James Lu
58e6527719 Irc: don't abort on BlockingIOError, ssl.SSLWantReadError, ssl.SSLWantWriteError
This effectively merges in the following 2.0 commits:
- "IRCNetwork: do not break on socket BlockingIOError" 6a90e99de4
- "IRCNetwork: also catch ssl.SSLWantReadError and ssl.SSLWantWriteError" ccc9f8e5c8
- "IRCNetwork: bump SOCKET_REPOLL_WAIT to 1 sec" 92460716d1

This fixes one part of #463.
2018-03-30 12:24:01 -07:00
James Lu
c54bb557dd relay: only look up nick once in normalize_nick
(cherry picked from commit ec8f74444906347dda3329c929f2c7315a1da507)

Conflicts:
	plugins/relay.py
2018-03-30 12:14:48 -07:00
James Lu
78b515144f Bump VERSION to 1.3-dev
[skip ci]
2018-03-30 12:09:01 -07:00
James Lu
9d21a5269f relay: speed up shutdowns by not manually splitting off every relay server
The connection will soon be gone anyways, so this is fairly pointless.
2018-03-30 12:06:45 -07:00
James Lu
c978e1c52f relay: fix a typo: CLIENTBOT_WHITELISTED_UMODES -> CLIENTBOT_WHITELISTED_CMODES 2018-03-30 12:01:38 -07:00
James Lu
310ad345a3 SECURITY: normalize account names before checking network / oper filters
(cherry picked from commit a6c1beaad0f715a28495edab8b4ae0f53a8968a7)
2018-03-30 11:59:29 -07:00
James Lu
22efe1384c _login: tweak error message: opered up => opered
(cherry picked from commit 557f1578bcd5b43b9be75b41ee887d659c49d3a9)

Conflicts:
	coremods/corecommands.py
2018-03-30 11:58:50 -07:00
James Lu
a6c1beaad0 SECURITY: normalize account names before checking network / oper filters 2018-03-30 11:55:29 -07:00
James Lu
557f1578bc _login: tweak error message: opered up => opered 2018-03-30 11:55:29 -07:00
James Lu
79143a1e40 Allow limiting login blocks to opers & certain hosts
Closes #502.

(backported from commit f439267129f88b8bd6f52f6b80483cf1d7853762)

This also fixes the previous commit "Allow specifying login blocks that are local to certain networks" for 1.3.
2018-03-30 11:35:45 -07:00
James Lu
c5970ba26d Allow specifying login blocks that are local to certain networks
(cherry picked from commit 8059f3f7fcd856e245732f2129b5813b31309039)
2018-03-30 11:35:45 -07:00
James Lu
1fbe3c6891 relay: sync whitelisted mode lists with 2.0-alpha3 2018-03-30 11:21:15 -07:00
James Lu
209fa9722c inspircd: move definitions for cmodes +J and +E to kicknorejoin_insp and repeat_insp
These use InspIRCd specific arguments which don't map cleanly to other IRCds. #559

(cherry picked from commit ec3b230eab29083a231b5c5623bbe070dea88e97)
2018-03-30 11:20:41 -07:00
James Lu
597685d77c relay_clientbot: fix @#channel messages not being treated as channel-specific
This is a lighter version of the more comprehensive fix in 2.0[1], which
depends on reworked STATUSMSG handling in relay.

[1]: https://github.com/GLolol/PyLink/commit/57334183
2018-03-30 11:14:56 -07:00
James Lu
a24d4d5b9c example-conf: sort clientbot ex. options so that cb-specific ones are at the end
(backported from commit 0c2927fb1eab5ab318800c6cd91eeff52c66ad0b)
2018-03-30 11:00:53 -07:00
James Lu
59d52920f3 example-conf: revise the example servers: section
- Remove "8P#" as a sidrange default - it does not leave adequate room for larger networks
- Move the TS6 example to near the end; it has less demand than Unreal and P10
- Consistently use "must" to describe P10 cloaking options, where any misconfiguration will lead to serious desyncs
- Bumped example autoconnect times from 5 to 10 seconds
- Add the 'netname' option to server blocks that previously didn't include it
- Comment out the 'channels' setting on each server example by default
- Various wording clarifications

(backported from commit 390b7a327a44e05409097050104bdaa19553ecb1)
2018-03-30 11:00:53 -07:00
James Lu
77eb9b4060 example-conf: revise notes for P10, TS6 servers
Backport commit 3d61bfd1148c74d030acf505ee2eeb7594b09d42 from 2.0.
2018-03-30 11:00:53 -07:00
James Lu
6d08e0b953 docs: various fixes pointed out by @MrBenC
- Clarify the project's goals of being an IRC services *framework*
- Briefly mention in the FAQ that the relay plugin is needed for...well, relay!

[skip ci]

(cherry picked from commit 484822e5d793ebb0d565a4b96cb2005dc630dc72)
2018-03-30 11:00:53 -07:00
James Lu
f274088ea0 classes: more checks on _aborted to (hopefully) prevent duplicate disconnects triggered by _send 2018-03-30 10:47:34 -07:00
James Lu
93d590fdea UserMapping: check for lower_nick rather than isinstance(userobj, User)
This makes classes reload-safe again.
2018-03-30 10:46:49 -07:00
Jordy Zomer
7a1dcbd460 Use yaml.safe_load instead of yaml.load to prevent executing arbitrary code (#589) 2018-03-29 14:12:32 -07:00
Jordy Zomer
be8e4be49d Use yaml.safe_load instead of yaml.load to prevent executing arbitrary code (#589) 2018-03-29 14:10:33 -07:00
James Lu
335fb352ec classes: ignore errors on duplicate selector.unregister()
This should really be fixed more thoroughly: there are still some remaining issues after the port to select, with disconnect / reconnect being triggered twice.
2018-03-29 14:04:58 -07:00
James Lu
29cda543e4 launcher: use a clearer description for --trace 2018-03-24 01:07:24 -07:00
James Lu
22b2410913 RELNOTES: mention new --trace arguments for 2.0-alpha3 2018-03-24 01:05:47 -07:00
James Lu
22f781189d Refresh 2.0-alpha3 changelog with engine-rework batch 2 changes so far 2018-03-24 01:02:55 -07:00
James Lu
84187a07ec Refresh 2.0-alpha3 changelog (up to 815535d76b0b2c150a250eea9d7372901e1586af) 2018-03-24 01:02:14 -07:00
James Lu
0bc24c94b2 relay: shortcut get_remote_user some more; only grab spawn lock if the user doesn't exist 2018-03-24 00:31:15 -07:00
James Lu
bbb36cd956 relay: reuse get_relay_server_sid output when bursting groups of users 2018-03-24 00:25:05 -07:00
James Lu
a085aed924 Rework irc.users and User() to transparently create a store of nicks -> UIDs
- This turns IRCNetwork.users into a new UserMapping class, which stores User objects by UID (str) and provides a 'bynick' dict storing case-normalized nicks to lists of UIDs.
- Turn User.nick into a property, where the setter implicitly updates the 'bynick' index and computes a case-normalized version of the nick (User.lower_nick)
2018-03-24 00:12:19 -07:00
James Lu
815535d76b IRCNetwork: fix autoconnect not applying when socket.connect() fails 2018-03-23 20:21:49 -07:00
James Lu
814dd9a3c5 IRCNetwork: only register a socket with selectdriver after connecting 2018-03-23 20:16:59 -07:00
James Lu
ec8f744449 relay: only look up nick once in normalize_nick 2018-03-22 22:45:40 -07:00
James Lu
bb1334696c classes: cache more of to_lower() 2018-03-22 22:03:08 -07:00
James Lu
c11a476257 Revert "selectdriver: actually, force a disconnect when _run_irc() fails"
This reverts commit f10f5bee52c395ef93e5032a90cf41d78531590b.
2018-03-22 21:21:33 -07:00
James Lu
b522967760 Fix duplicate calls to _run_autoconnect 2018-03-22 17:42:28 -07:00
James Lu
f10f5bee52 selectdriver: actually, force a disconnect when _run_irc() fails 2018-03-21 21:07:44 -07:00
James Lu
989259af97 selectdriver: don't crash if _run_irc hits an error 2018-03-21 20:14:27 -07:00
James Lu
5172841378 launcher: add support for tracing (most of) PyLink's execution 2018-03-17 16:13:35 -07:00
James Lu
91b86ce0e4 Remove structures.DeprecatedAttributesObject, it's vastly inefficient for what it accomplishes 2018-03-17 15:49:48 -07:00
James Lu
fb6c3bf6d5 selectdriver: don't try to deregister dead sockets 2018-03-17 15:33:39 -07:00
James Lu
ab70d7c8fb selectdriver: stop delivering events when _aborted is set 2018-03-17 15:27:35 -07:00
James Lu
8100a4cea6 IRCNetwork: run _run_autoconnect in a thread so it doesn't block whatever calls disconnect() 2018-03-17 15:26:36 -07:00
James Lu
30bcd8ca79 control: remove check for _connection_thread (removed in select rework) 2018-03-17 12:18:34 -07:00
James Lu
0151f77f7b Don't clear the read buffer with every _run_irc call 2018-03-17 12:18:16 -07:00
James Lu
10d2fb93ed classes: fix syntaxerror from merge 2018-03-17 11:06:02 -07:00
James Lu
0033612fa3 Merge branch 'devel' into engine-rework
Conflicts:
	classes.py
2018-03-17 11:03:58 -07:00
James Lu
f7ab2564fe Rework inbound connection handling to use select
Closes #588.
2018-03-17 11:01:32 -07:00
James Lu
180bfa9917 relay: don't spam ulines with "notice failed" errors 2018-03-17 10:27:56 -07:00
James Lu
5bffe67416 relay: bandaid patch for freezes on startup when there are a ton of networks
The side effect of this patch is that it makes large bursts *really* CPU intensive. A proper fix for this will hopefully be introduced in the future.
2018-03-11 21:26:37 -07:00
James Lu
b7b49769e0 relay: silently abort if a network splits while we try to spawn a server or client 2018-03-11 21:25:42 -07:00
James Lu
5733418380 relay_cb: bandaid fix to prevent STATUSMSG messages from being interpreted as non-channel specific 2018-03-10 19:54:33 -08:00
James Lu
b6bac994c6 servermaps: show the uplink server name for Clientbot links 2018-03-07 22:28:34 -08:00
James Lu
92460716d1 IRCNetwork: bump SOCKET_REPOLL_WAIT to 1 sec 2018-03-07 18:32:20 -08:00
James Lu
ccc9f8e5c8 IRCNetwork: also catch ssl.SSLWantReadError and ssl.SSLWantWriteError 2018-03-07 18:31:43 -08:00
James Lu
8f9b56e9d9 IRCNetwork: abort when _send() fails to avoid deadlocks 2018-03-07 18:30:14 -08:00
James Lu
c49147f232 stats: route permission error replies to notice and not privmsg
This prevents "unknown command" flood loops with stats services which poll these on link.
2018-03-04 12:11:50 -08:00
James Lu
57f77c676d relay: don't show the network name when routing kicks through a server
This is redundant, as relay has always spawned subservers for quite some time now.
2018-03-02 21:42:25 -08:00
James Lu
9f39e484da Merge branch 'mode-rework' into devel 2018-03-02 21:34:09 -08:00
James Lu
0ca185fada classes: fix some prefixmodes list vs. state confusion 2018-03-02 21:07:47 -08:00
James Lu
5a00454a8d _parse_modes: apply modes to a temporary mode list as we parse them
Fixes #573.

Old, broken behaviour:
irc.parse_modes('#test', '+b-bb *!*@new.ban *!*@nonexistent.ban *!*@new.ban')
=> [('+b', '*!*@new.ban')]

Fixed:
irc.parse_modes('#test', '+b-bb *!*@new.ban *!*@nonexistent.ban *!*@new.ban')
=> [('+b', '*!*@new.ban'), ('-b', '*!*@new.ban')]
2018-03-02 20:57:16 -08:00
James Lu
f12318b5dc classes: add an is_channel argument to _parse_modes
This is required for the following commit.
2018-03-02 20:56:59 -08:00
James Lu
1413aa6042 _apply_modes: don't add prefix modes to mode sets even when prefixmodes=None 2018-03-02 20:47:41 -08:00
James Lu
054680c806 classes: split apply_modes into two functions 2018-03-02 20:43:25 -08:00
James Lu
9cca695d14 classes: split parse_modes into core and wrapper functions 2018-03-02 20:43:25 -08:00
James Lu
d172831805 conf: rename methods to snake case (#523) 2018-03-02 20:23:48 -08:00
James Lu
03103bea14 conf: use a more informative description for validate() 2018-03-02 20:23:47 -08:00
James Lu
87fdb1dde1 opercmds: migrate from utils.isServerName to irc.is_server_name 2018-03-02 20:23:47 -08:00
James Lu
9e212fc0a4 protocols: migrate utils.wrapArguments, splitHostmask use to camel case (#523) 2018-03-02 20:23:47 -08:00
James Lu
3e656cd943 utils: mark reset_module_dirs, load_plugin, get_protocol_module as private 2018-03-02 20:23:47 -08:00
James Lu
1cdf16f5c9 various: migrate utils.loadPlugin, getProtocolModule, resetModuleDirs calls to snake-case (#523) 2018-03-02 20:23:47 -08:00
James Lu
bea2ea8ebd plugins, coremods: migrate (un)registerService calls to snake case (#523) 2018-03-02 20:23:47 -08:00
James Lu
9e3f412f0b log: rename methods to snake case (#523) 2018-03-02 20:23:47 -08:00
James Lu
ad5a11bf34 control: skip networks that fail to initialize on rehash
This follows the launcher fix in 8321485315c9e83c1a45a053ec51ce81cd7977cc.
2018-03-02 20:23:47 -08:00
James Lu
a2783d74c5 unreal: bump protocol version to 4017 (no changes needed) 2018-03-02 20:23:47 -08:00
James Lu
1c3f71ac1b servermaps: assign servermaps.localmap to all opers 2018-03-02 16:18:55 -05:00
James Lu
d5d94f86e8 servermaps: split into two perms (servermaps.map and servermaps.localmap) 2018-03-02 16:14:04 -05:00
James Lu
6a90e99de4 IRCNetwork: do not break on socket BlockingIOError
On non-blocking sockets, recv() raises BlockingIOError instead of blocking when there's no data to be read.
The correct behaviour is to wait and try again instead of breaking the connection.
2018-03-01 12:52:41 -05:00
James Lu
e8e26daf05 ctcp: fix wrong logging format for unknown CTCPs 2018-03-01 02:48:04 -05:00
James Lu
5d25b3c105 ctcp: remove extraneous keyword argument
private is not defined in irc.msg()
2018-02-25 14:29:19 -05:00
James Lu
3c0809dce0 ctcp: don't use irc.reply in hook functions
This is undefined behaviour because nothing in this stack actually updates the 'last caller' variables irc.reply() use.
2018-02-24 14:19:57 -05:00
James Lu
50f8cde694 classes: make _to_lower_core and _expandPUID type-safe 2018-02-24 14:19:57 -05:00
James Lu
2aca4e39c1 RELNOTES: fill in release dates for 1.2.0, 1.2.1, 2.0-alpha1
[skip ci]
2018-02-24 01:48:26 -08:00
James Lu
a2e793755f RELNOTES: first batch of 2.0-alpha3 changes 2018-02-24 01:44:04 -08:00
James Lu
5c9639b4a9 opercmds: alias 'trace' to checkban 2018-02-21 00:12:08 -08:00
James Lu
390b7a327a example-conf: revise the example servers: section
- Remove "8P#" as a sidrange default - it does not leave adequate room for larger networks
- Move the TS6 example to near the end; it has less demand than Unreal and P10
- Consistently use "must" to describe P10 cloaking options, where any misconfiguration will lead to serious desyncs
- Bumped example autoconnect times from 5 to 10 seconds
- Add the 'netname' option to server blocks that previously didn't include it
- Comment out the 'channels' setting on each server example by default
- Various wording clarifications
2018-02-20 23:42:13 -08:00
James Lu
3d61bfd114 example-conf: revise notes for P10, TS6 servers 2018-02-20 23:26:06 -08:00
James Lu
0c2927fb1e example-conf: sort clientbot ex. options so that cb-specific ones are at the end 2018-02-20 23:23:04 -08:00
James Lu
804791b8af clientbot: support expansions ($nick, etc) in autoperform 2018-02-20 23:19:32 -08:00
James Lu
0b0da2cfe6 launcher: add experimental daemonization support
Closes #187.
2018-02-19 21:05:15 -08:00
James Lu
9cdb224c02 Replace use of conf.conf['bot'] with conf.conf['pylink'] 2018-02-18 23:26:39 -08:00
James Lu
c40250330d ctcp: log the service bot receiving CTCP messages 2018-02-18 23:13:44 -08:00
James Lu
e68db3689d Rewrite the CTCP plugin
- Extend CTCP replies to all service bots - closes #468.
- Use a generic hook handler instead of wrapping around the commands handler (#407).

This code takes advantage of the hook suppression feature introduced in 2e66b9bde61d796a821cfc2d30f94ac6baa26e40 (#547).
2018-02-18 23:11:36 -08:00
James Lu
2e66b9bde6 classes: allow hook functions to block further execution by returning False
Closes #547.
2018-02-18 22:42:39 -08:00
James Lu
4a01948647 relay: oops, multiple STATUSMSG prefixes should pick the lowest, not highest (#570)
This behaviour really isn't consistent across IRCds though...
- Unreal, Hybrid, and charybdis mangle messages at the server side to use the lowest prefix
- InspIRCd throws a "No such nick/channel" error
- Nefarious silently drops messages with multiple prefixes?
2018-02-18 22:29:16 -08:00
James Lu
37be73d39c clientbot: add STATUSMSG support (#570) 2018-02-18 22:15:19 -08:00
James Lu
81bd1e8474 relay: add basic support for STATUSMSG (#570)
So far, this code only knows about changing prefixes while keeping mode characters as-is.
A complete but longer solution would be to actually go through irc.cmodes, but I don't
think doing so is necessary given how little STATUSMSG is actually used in production.
2018-02-18 22:03:12 -08:00
James Lu
1405b01597 ServiceBot: clean up some function descriptions 2018-02-18 19:40:46 -08:00
James Lu
484822e5d7 docs: various fixes pointed out by @MrBenC
- Clarify the project's goals of being an IRC services *framework*
- Briefly mention in the FAQ that the relay plugin is needed for...well, relay!

[skip ci]
2018-02-17 00:50:12 -08:00
James Lu
7114c6942f README: the shared{} requirement for KLINEs likely applies to chatircd too 2018-02-12 11:18:48 -08:00
James Lu
8321485315 launcher: prevent protocol module init errors from aborting execution
This fixes various issues including:
- Networks going missing (the server list is read in a non-deterministic order)
- world.started never being set, causing relay to never work!
2018-02-10 16:17:18 -08:00
James Lu
3f7e2328fe relay: make endburst delay configurable
Also, raise the default to 10 seconds.
2018-02-10 15:53:49 -08:00
James Lu
ea84497359 protocols: remove the endburst_delay option from spawn_server
Interestingly, this was never documented in the protocol module spec...
2018-02-10 15:44:09 -08:00
James Lu
a425f873b5 relay, inspircd: move endburst delay code to a private API
This is a very specific hack that shouldn't be extended across the protocol module spec. So far, all other protocol modules ignore the endburst_delay option anyways.
2018-02-10 15:34:07 -08:00
James Lu
ccbd79a95c relay: fix KeyError when a local client is kicked from a claimed channel
Fixes #572.
2018-02-10 15:12:40 -08:00
James Lu
18c1a277f5 clientbot: remove extraneous use of to_lower() in handle_part 2018-01-31 19:35:24 -08:00
James Lu
74848853ac clientbot: fix KeyError caused by lower() in spawn_server
This affected connections to afternet for example.
2018-01-31 19:33:05 -08:00
James Lu
9f6e4306cd automode: fix handling of channels with multiple #'s in them
(cherry-picked from commit 09c8b03705e9513f3381f6a1c43d44e7dc1cc214)
2018-01-29 08:21:37 -08:00
James Lu
84f6190478 inspircd: only read METADATA modules changes from the uplink
Closes #567.
2018-01-25 09:52:29 -08:00
James Lu
28a62f629a automode: replace assert usage with proper exceptions 2018-01-22 08:17:20 -08:00
James Lu
09c8b03705 automode: fix handling of channels with multiple #'s in them 2018-01-22 08:15:04 -08:00
James Lu
5fd216c720 commands: fix 'showchan' displaying status prefixes in reverse 2018-01-22 08:10:55 -08:00
James Lu
d608661a33 permissions-reference: document perms for 'raw' plugin 2018-01-21 13:52:29 -08:00
James Lu
8000d51453 Split the 'raw' command into a new plugin
Closes #565.
2018-01-21 13:50:37 -08:00
James Lu
e446e0e27b control: continue handling SIGUSR1 as rehash for compat with older 2.0 versions 2018-01-21 13:36:29 -08:00
James Lu
44be5910e0 Revert "control: move rehash signal to SIGUSR1, and shutdown on SIGHUP (terminal close)"
This wasn't an incredibly popular decision because it broke a simple 'pylink &'

This reverts commit 883f9199ecf63ef91df25a5dc47d8f7d353d7af0.

Conflicts:
	coremods/control.py
2018-01-21 13:31:15 -08:00
James Lu
6bb2198710 inspircd: move _modsupport.clear() to a _post_disconnect override
Speculative fix for #567.
This may be caused by a race condition between post_connect and handle_capab, since the remote server can send its server data before we start sending ours.
2018-01-21 13:27:59 -08:00
James Lu
67dea6f748 classes: add docstrings to _pre_connect, _pre_disconnect, _post_disconnect 2018-01-21 13:20:42 -08:00
James Lu
58b717a2a0 docs/t: warn in main articles that specifications in master may be outdated
[skip ci]
2018-01-13 18:29:57 -08:00
James Lu
31c96bd1ed hooks-reference: bump to 2.0-alpha2
- Replace `IrcChannel`, `IrcUser`, and `IrcServer` with their new class names (`classes.Channel`, `classes.User`, and `classes.Server`)
- Replace `irc.fullVersion()` with `irc.version()`
- Various minor wording tweaks.
2018-01-13 18:23:12 -08:00
James Lu
06ee01b7a7 hooks-reference: don't use the wrong terminology in example channel names 2018-01-13 18:23:12 -08:00
James Lu
7a51220309 relay: match P10 WALL* commands as notices 2018-01-08 20:56:06 -08:00
James Lu
ec9063b9e8 Revert "relay: differentiate between PRIVMSG vs. NOTICE via a blacklist"
This reverts commit d81a9cd5c3f51174755d64434ddf6c5f2b0d7988.
2018-01-08 20:56:06 -08:00
James Lu
bcb0fecfa8 PyLink 2.0-alpha2 2018-01-06 17:30:50 -08:00
James Lu
ce3b1152b2 modelists: fix page title for extbans table
[skip ci]
2018-01-05 18:46:08 -08:00
James Lu
7a32e7d8a2 global: ignore empty "global:" configuration blocks
(cherry picked from commit 15a231a3711b96bbad2b2cda7339bcb838ea95df)
2018-01-03 08:01:27 -08:00
James Lu
d5d140f4b0 p10: fix hashed cloaks check reading from the wrong config variable
(cherry picked from commit 043a147b41d5d23633bfad6ec261d1f058e8c276)

Conflicts:
	protocols/p10.py
2018-01-02 20:38:12 -08:00
James Lu
f8abdd1244 RELNOTES: update for previous commit 2017-12-31 12:09:52 -08:00
James Lu
043a147b41 p10: fix hashed cloaks check reading from the wrong config variable 2017-12-31 12:09:36 -08:00
James Lu
3d661c9713 RELNOTES: update 2.0-alpha2 changes so far 2017-12-30 01:34:33 -08:00
James Lu
ba4e0aed85 example-conf: reword description for servers::<netname>::relay_forcetag_nicks 2017-12-30 01:33:14 -08:00
James Lu
60c05af9ed relay: allow defining server-specific nicks to always tags
Closes #564.
2017-12-30 01:22:24 -08:00
James Lu
56fa626605 inspircd: use clear() instead of replacing the _modsupport set
Maybe this will fix issues with _modsupport not being completely filled?
2017-12-27 11:48:00 -08:00
James Lu
7c0d279f61 inspircd: raise NotImplementedError instead of only warning when a CHG* module is missing 2017-12-22 12:41:48 -08:00
James Lu
92427201f1 inspircd: track module (un)loading
Closes #555.
2017-12-22 12:38:48 -08:00
James Lu
c62580d228 bots: don't allow 'spawnclient' on protocols where it is stubbed 2017-12-22 12:28:27 -08:00
James Lu
958bb351ca clientbot: log warnings if the bot cannot join a channel
Closes #533.

This adds handlers for the following numerics:
* ERR_TOOMANYCHANNELS (405)
* ERR_CHANNELISFULL (471)
* ERR_INVITEONLYCHAN (473)
* ERR_BANNEDFROMCHAN (474)
* ERR_BADCHANNELKEY (475)
* ERR_BADCHANMASK (476)
* ERR_NEEDREGGEDNICK (477)
* ERR_BADCHANNAME (479)
* ERR_SECUREONLYCHAN / ERR_SSLONLYCHAN (489)
* ERR_DELAYREJOIN (495)
* ERR_OPERONLY (520)
2017-12-22 12:28:27 -08:00
James Lu
7afe193259 bots: fix KeyError when attempting to join a channel not in the index 2017-12-22 12:28:27 -08:00
James Lu
444d8c53bb RELNOTES: fix wacky line spacing
[skip ci]
2017-12-22 01:05:40 -08:00
James Lu
5b2fdc94e7 RELNOTES: add a list of changes since 2.0-alpha1 so far 2017-12-22 01:03:19 -08:00
James Lu
dbc9d1690d setup: update PyPI classifiers
- Declare Python 3.6 support
- Change development status to Production/Stable
2017-12-22 00:07:49 -08:00
James Lu
5f9365a521 relay, inspircd: add support for blockhighlight +V 2017-12-21 21:18:20 -08:00
James Lu
145a4677f6 ircs2s_common: add handling for nick@servername messages 2017-12-21 02:57:10 -08:00
James Lu
8dbbe65a1c ircs2s_common: remove useless statusmsg splitting code
We used to do this in order to lowercase the channel part of ~#channel messages correctly, but that is no longer needed as of 9702030bf5437a9749f5435c87d9c7a2757eaadc.
2017-12-21 02:11:28 -08:00
James Lu
d01b9aaa23 relay: improve fallback KNOCK notices
- Show the network that the /knock originated from (it is *not* obvious when we have a user with no relay client)
- Specifically hint that users with no relay client cannot be invited directly because there is no client to actually /invite!
- Prefer sending the notice to %#channel when halfops are available, as they usually have the power to /invite
2017-12-21 01:57:43 -08:00
James Lu
f64976b1ed p10: implement outgoing knock() as a wrapper over NOTICE 2017-12-21 01:53:41 -08:00
James Lu
2df608307d relay: block sending STATUSMSG messages to IRCds not supporting them 2017-12-21 01:41:01 -08:00
James Lu
16b491fdab ts6_common, p10: declare protocol cap has-statusmsg 2017-12-21 01:34:41 -08:00
James Lu
d81a9cd5c3 relay: differentiate between PRIVMSG vs. NOTICE via a blacklist
This is so that P10 WALL* commands are forwarded correctly as notices instead of privmsgs.
2017-12-21 01:27:34 -08:00
James Lu
20e730ba2b p10: add inbound handlers for WALLCHOPS/WALLHOPS/WALLVOICES
This essentially finishes off STATUSMSG support on P10.
2017-12-21 01:27:19 -08:00
James Lu
2cc1195ff9 p10: refactor message() to send @%+#channel messages correctly 2017-12-21 01:20:08 -08:00
James Lu
63f3cdaea8 relay: add knock forwarding support 2017-12-21 00:19:09 -08:00
James Lu
454539185e Move knock handling to ts6_common
This adds support for (reasonless) KNOCK on TS6.
2017-12-21 00:04:55 -08:00
James Lu
2245af1dba Revert "ts6: handle ChatIRCd ENCAP USERMODE"
This reverts commit 785fc8d2d23adeb05061d7b01945a9d58d1ccef4.

This feature isn't actually used yet and is relatively non-standard. Aside from that, the biggest issue so far is that ENCAP USERMODE doesn't give servers a way to explicitly acknowledge or reject(ignore) the mode change, which can lead to desyncs.
2017-12-18 17:20:37 -08:00
James Lu
ab91acb2f7 relay_clientbot: rename isRelayClient => is_relay_client() 2017-12-18 13:23:53 -08:00
James Lu
954f4f9886 relay: capitalize constants related to modes 2017-12-18 13:23:16 -08:00
James Lu
0104462782 relay: whitelist cmodes kicknorejoin, kicknorejoin_insp, repeat, repeat_insp
Closes #559.
2017-12-18 13:17:05 -08:00
James Lu
ec3b230eab inspircd: move definitions for cmodes +J and +E to kicknorejoin_insp and repeat_insp
These use InspIRCd specific arguments which don't map cleanly to other IRCds. #559
2017-12-18 13:10:48 -08:00
James Lu
6c65d5523e IRCNetwork: potentially fix queue thread shutdowns (#558)
Replace unreliable appendleft() usage with replacing the first element (or adding None if the queue is empty).
2017-12-17 01:01:21 -08:00
James Lu
9a5072824d relay: stop lowercasing modedelta args, as that can break things like unreal +f 2017-12-14 13:40:47 -08:00
James Lu
f908e407d4 relay: also ignore clientbot networks in modedelta
TODO: abstract all these checks out!
2017-12-14 13:26:43 -08:00
James Lu
923795719f relay: drop all list and prefix modes in modedelta 2017-12-14 13:07:00 -08:00
James Lu
57a2132d5d relay: always enforce modedelta modes, and prevent them from being unset 2017-12-14 12:46:25 -08:00
James Lu
b2270ca3eb relay: move claim enforcement routines outside the mode/kick loops
This prevents claim responses from being sent multiple times.
2017-12-14 12:15:19 -08:00
James Lu
59c12ff354 relay: merge in modedelta branch, port to latest 2.x relay
Merge remote-tracking branch 'origin/wip/relay-modedelta' into devel

Conflicts:
	plugins/relay.py
2017-12-14 11:56:41 -08:00
James Lu
8490bee634 handlers: pick the highest prefix when displaying status in WHOIS
This fixes a regression from aa44bc15a37ef690eb1cc47d5f8d315116b919a0
2017-12-12 12:46:43 -08:00
James Lu
9dfa0a478e classes: fix inverted order in get_prefix_modes() description
No, no, no. Nobody sorts modes that way!
2017-12-07 12:07:08 -08:00
James Lu
d54bf0d06c relay: also log the extban prefix strings being readded 2017-12-07 11:38:27 -08:00
James Lu
a2cb4daa46 relay: mangle the mode name when forwarding extban->cmode so that +b syntax filters don't trigger
This should allow bidirectional forwarding between UnrealIRCd +b ~T:block:<glob> and InspIRCd +g <glob> to work.
(#557)
2017-12-07 11:26:16 -08:00
James Lu
8fcb5f9df0 relay: ignore static extbans when looking up dynamic extban prefixes
This fixes #560, which was caused by relay confusing ban_all_registered ($a) with ban_account ($a:), since $a:account also starts with $a.
2017-12-07 11:05:39 -08:00
James Lu
6adeada598 relay: add more complete debug logging in extban handlers 2017-12-07 11:04:26 -08:00
James Lu
a0c57d0a5a unreal: register ~T extbans as filter and filter_censor (#557)
This doesn't work in relay yet because of #560 (as well as a restriction where acting extbans must match n!u@h...)
2017-12-07 00:36:38 -08:00
James Lu
bd2cd90957 modelists/channel-modes: add inspircd +D definition
[skip ci]
2017-12-05 12:04:35 -08:00
James Lu
d30eca77e9 relay: add more channel modes to the whitelist
* blockcaps: inspircd +B, elemental-ircd +G
* exemptchanops: inspircd +X
* filter: inspircd +g
* hidequits: nefarious +Q, snircd +u
* history: inspircd +H
* largebanlist: ts6 +L
* noamsg: snircd/nefarious +T
2017-12-05 12:01:57 -08:00
James Lu
80ef2ca788 hybrid: remove slash-in-hosts as it is not supported 2017-12-03 18:56:43 -08:00
James Lu
2fc5d32e3f NetworkCore: don't clear state on disconnect
This is already reset on connect, so doing it here too is a bit pointless.
2017-12-03 18:56:39 -08:00
James Lu
bebdf2e4ff IRCNetwork: avoid sending multiple disconnect hooks for one disconnection 2017-12-03 17:46:45 -08:00
James Lu
8b62d6d458 modelists: resort data by ircd names defined in protocol modules 2017-11-21 07:39:45 -08:00
James Lu
52f40ad7a2 networks.remote: don't clobber command switches for other commands
This also moves the --service argument to before the network name to prevent ambiguity with argparse.REMAINDER.

Closes #538.
2017-11-14 18:14:23 -08:00
James Lu
f969197436 Convert protocol-modules graphic to a .svg 2017-11-12 12:07:08 -08:00
James Lu
bff53c6e69 Remove .codeclimate.yml, practically unused
This reverts commit 3869c069919bbc928e54dd6e748d62c5167f2617.
2017-11-12 12:03:25 -08:00
James Lu
03e02dda51 relay: replace garbage locking code with proper filtering in relay_joins
I'm not even going to start on how much time I spent working on this...

Closes #548, #529
2017-11-12 11:58:36 -08:00
James Lu
d4cbf1d2af services_support: fix rejoin-on-kill to the main service bot
Clear the irc.pseudoclient state on kill as the respawning code will check for it first and reuse UIDs even if they don't exist.
2017-11-12 10:54:30 -08:00
James Lu
847854aac3 Merge branch 'master' into devel
Conflicts:
	README.md
2017-11-07 19:19:52 -08:00
James Lu
48ea58c1fb bots: remove use of deprecated utils.isNick() 2017-11-07 19:19:11 -08:00
James Lu
c35c8cd4aa
FAQ updates [skip ci]
- add answers for "services bots not spawning" and "inconsistent config spacing causing errors"
- reword the connection troubleshooting section slightly
- mention using 'showchan' to help determine the cause of missing user issues
2017-11-07 09:59:37 -08:00
James Lu
15a231a371 global: ignore empty "global:" configuration blocks 2017-11-06 12:07:33 -08:00
James Lu
b6af6dddc5 relay: fix RuntimeError on handle_part for clientbot networks 2017-11-05 01:18:42 -08:00
James Lu
509c2e52c4 clientbot: fix KeyError when there are still queued outgoing messages to a channel we just left 2017-11-05 01:17:59 -08:00
James Lu
62cef5c3f5
classes: clarify comments on mode-related functions 2017-11-03 23:40:11 -07:00
James Lu
b366aa8d61 Merge branch 'wip/ts6-updates' into devel 2017-10-27 06:50:37 -07:00
James Lu
544e078512 clientbot: treat 0 as an empty account name (for WHOX)
This fixes incorrect "X is logged in (on somenet) as 0" messages in WHOIS.
2017-10-25 16:01:10 -07:00
James Lu
c974ee9b44 hybrid: various cleanup 2017-10-22 01:19:38 -07:00
James Lu
c636e064e7 ts6: remove SAVE from required capabs
We don't actually send SAVE out to any IRCd, so we don't need to demand that it's supported.

Closes #545.
2017-10-22 01:08:30 -07:00
James Lu
5e7469b56f Remove protocols/ratbox, superseded by ts6
Closes #543.
2017-10-22 01:06:55 -07:00
James Lu
975d835c92 ts6: add support for ratbox, send EUID only when supported (#543) 2017-10-22 01:00:12 -07:00
James Lu
1a24bc19af ts6: rename self.caps to self._caps 2017-10-22 00:44:30 -07:00
James Lu
3d3300e542 ts6: merge in CHGHOST checks and umode definitions from protocols/ratbox 2017-10-22 00:41:15 -07:00
James Lu
c2dbb74f5a ts6: clean up mode definitions and target_ircd code 2017-10-22 00:29:00 -07:00
James Lu
d0dff2c5ae Move permission enumeration to runtime, fix default perms not applying at startup
Closes #542.
2017-10-22 00:08:16 -07:00
James Lu
26b8292564 relay_clientbot: handle errors if the relay: or clientbot_styles: blocks are empty 2017-10-21 13:19:31 -07:00
James Lu
9ffe2edc74 README: mention fix for #526 (unreal hostname desyncs)
[skip ci]
2017-10-18 22:36:22 -07:00
James Lu
472b73cf65 classes.Channel: clarify the intended behaviour of sort_prefixes() and get_prefix_modes() 2017-10-15 02:16:18 -07:00
James Lu
a63e2557be unreal: fix wrong hook name for legacy user introduction
(cherry picked from commit 4935ef521e047b02339315b8d55849cd74192444)
2017-10-15 02:00:02 -07:00
James Lu
4935ef521e unreal: fix wrong hook name for legacy user introduction 2017-10-15 01:55:53 -07:00
James Lu
e8958962dd unreal: fix authentication-in-progress check in handle_server 2017-10-15 01:54:39 -07:00
James Lu
1470e7691f relay_clientbot: add support for showing prefix modes
This adds a new expansion $mode_prefix, and adds it to the default formats for MESSAGE and ACTION.
Closes #540
2017-10-15 01:42:07 -07:00
James Lu
aa44bc15a3 classes: fix backwards sorting in Channel.sort_prefixes()
Also remove various workaround code added to address this.
2017-10-15 01:29:42 -07:00
James Lu
b8df1a1b61 Merge remote-tracking branch 'origin/master' into devel 2017-10-11 18:56:21 -07:00
James Lu
e8b7116888 example-conf: clarify terminology regarding server IDs
P10 calls them server numerics, for example. Also clarify what the sid and sidrange options are used for (i.e. why they need to be set).

[skip ci]
2017-10-11 17:59:16 -07:00
James Lu
a9916a74f2 Revert "NetworkCoreWUtils: strip off leading and trailing quotes from parse_modes"
This was a band-aid fix not ready to be committed - we should work on this in IRCParser and opercmds instead.

This reverts commit aeaee491f352fc76399a0210915b83886327391a.
2017-10-10 22:33:24 -07:00
James Lu
fdaee37b7b example-conf: fix a typo
[skip ci]
2017-10-10 22:31:50 -07:00
James Lu
aeaee491f3 NetworkCoreWUtils: strip off leading and trailing quotes from parse_modes 2017-10-10 22:25:06 -07:00
James Lu
762ec3a0eb structures: fix _keymangle when key isn't a string
I really need to add unit tests for these...
2017-10-10 22:14:33 -07:00
James Lu
66c762b63f Merge remote-tracking branch 'origin/clientbot-fixes' into devel 2017-10-10 20:53:12 -07:00
James Lu
08917f8aae PyLink 2.0-alpha1 2017-10-07 22:54:24 -07:00
James Lu
ce82c231fe RELNOTES: fix formatting of old versions' changelogs
[skip ci]
2017-10-07 22:51:34 -07:00
James Lu
fe4fb9c84c stats: use a simpler /stats c format 2017-10-07 22:27:43 -07:00
James Lu
25ec88c566 clientbot: don't send empty MODE hooks when enumerating empty ban lists 2017-10-07 21:51:38 -07:00
James Lu
eca40a3d7c coremods/handlers: implement cleanup code for visible-state-only servers
Closes #536.
Closes #517.
2017-10-07 21:49:17 -07:00
James Lu
de5ab051aa clientbot: rename cap. clear-channels-on-leave => visible-state-only (#517) 2017-10-07 21:48:48 -07:00
James Lu
740b399ec2 clientbot: block attempts from virtual clients to change to an existing nick (#535) 2017-10-07 20:50:09 -07:00
James Lu
d7766d54d5 clientbot: check for nick collisions with virtual clients on NICK
Closes #535.
2017-10-07 20:07:26 -07:00
James Lu
84ff797b5f clientbot: rewrite _get_UID nick collision handling to be less confusing 2017-10-07 20:03:25 -07:00
James Lu
85ac0bb80a docs/modelists: add a GitHack link to extbans.html
[skip ci]
2017-10-05 19:37:26 -07:00
James Lu
0ad2bc2f7b docs/modelists: use a different color to represent modes implemented as extbans and vice versa
[skip ci]
2017-10-05 19:32:57 -07:00
James Lu
61fe97b646 docs/modelists: drop :data suffix from extbans lists
[skip ci]
2017-10-05 19:28:43 -07:00
James Lu
48aab1cf16 docs/services-api: revise
- Rewrite lead section to be more concise
- Move to snake case method names
- Consistently use the terms "services" and "service bots"

[skip ci]
2017-10-05 19:21:52 -07:00
James Lu
39b1e28061 Remove plugins/example_service, it is out of date and broken with 2.x
Closes #532.
2017-10-05 19:08:28 -07:00
James Lu
f79168ce5f docs/t: remove future articles that won't be around for a while
[skip ci]
2017-10-05 19:05:36 -07:00
James Lu
5574c746b9 docs: update table of contents
Mode lists are in a separate folder now.

[skip ci]
2017-10-05 19:04:45 -07:00
James Lu
82a6ceb99e docs: Refresh release-process.md
[skip ci]
2017-10-05 18:20:59 -07:00
James Lu
f0d1c1bb89 pmodule-spec: mention in detail how users are tracked
Closes #478.
2017-10-05 18:18:09 -07:00
James Lu
8fff9ea641 pmodule-spec: mention post_connect definition for IRCNetwork derivatives (#478) 2017-10-05 18:18:09 -07:00
James Lu
d8768bcb73 Revise docs/automode.md
- The default bot name has been "Automode" instead of "ModeBot" for a while now; reflect that change here
- General edits for flow

[skip ci]
2017-10-04 23:28:27 -07:00
James Lu
d09c1be688 README: update IRC link 2017-10-01 00:39:28 -07:00
James Lu
3022274f6b Merge branch 'master' into devel
Conflicts:
	README.md
	VERSION
2017-09-24 12:26:06 -07:00
James Lu
d2a3bb8d28 actually no, forget having 3 tiers of support for IRCds
[skip ci]
2017-09-24 12:23:25 -07:00
James Lu
3da61f0f2a Revert "Demote ratbox to tier 3 [skip ci]"
This reverts commit d0fbfcd2d8bf13fec0d8d7a9770386747d0f6a73.
2017-09-24 12:22:37 -07:00
James Lu
67d5766cde README: remove expiringdict install note
1.1.4 has since been released, fixing installation via pip
[skip ci]
2017-09-24 10:31:07 -07:00
James Lu
d0fbfcd2d8 Demote ratbox to tier 3 [skip ci]
I don't know of any network actually using PyLink with this IRCd.
2017-09-24 00:04:36 -07:00
James Lu
91e7d4f47a README: bump juno version [skip ci] 2017-09-23 23:53:17 -07:00
James Lu
4d6f80f58e README: update IRCd notes [skip ci]
- Add notes for beware-ircd
- Update notes for InspIRCd
2017-09-23 23:50:25 -07:00
James Lu
e25f6fd470 ircs2s_common: expand PUIDs in squit()
This fixes SQUIT not working correctly on ngIRCd.
2017-09-23 23:15:29 -07:00
James Lu
f74b34e99b ts6: fix wrong prefix char for +a on ChatIRCd
This fixes users with +a disappearing when bursted to ChatIRCd.
2017-09-23 22:43:27 -07:00
James Lu
8443de4701 servermaps: display hopcount in 'map' for local servers 2017-09-23 22:36:49 -07:00
James Lu
ad32ce20da protocols: send outgoing hop counts for servers and users
Closes #527.
2017-09-23 22:36:31 -07:00
James Lu
663bfe462c classes: track hopcount in Server 2017-09-23 22:35:55 -07:00
James Lu
76a0eb78e3 clientbot: fix possible TypeError in squit() 2017-09-23 21:54:42 -07:00
James Lu
a2a32ed32f clientbot: wrap outgoing modes to prevent cutoff 2017-09-23 21:54:02 -07:00
James Lu
6e89dbed24 clientbot: implement ban list enumeration on JOIN
Closes #530.
2017-09-23 21:26:13 -07:00
James Lu
b52082ed05 relay: various cleanup
- Raise desync-related state checks to warning
- Rename get_remote_sid to get_relay_server_sid - it's less ambiguous
- Clarify and add some missing function docstrings
2017-09-23 20:43:21 -07:00
James Lu
6cbb6617ef relay: don't forward simple bans as text to clientbot links if modesync is on
Closes #528.
2017-09-23 14:39:26 -07:00
James Lu
a60e6e7f22 relay: add missing comment to last commit 2017-09-23 14:09:28 -07:00
James Lu
b667bed1e6 relay: only allow one thread to run initialize_channel at a time
Closes #529.
2017-09-23 13:58:15 -07:00
James Lu
113bfcba9d PyLinkNetworkCore: copy world.hooks before iterating
This fixes a race condition where the order of PRIVMSG handlers could be changed as 'load <plugin>' runs, causing the load command to be processed multiple times.
2017-09-23 13:39:43 -07:00
James Lu
2535aa145f corecommands: fix unloading plugins that define hooks
This fixes a regression from 5e92aefcd49a7a4944d9b6c308cc7ae482a216b6.
2017-09-23 13:39:13 -07:00
James Lu
42d62fe28a p10: pass IPv6 IPs on to supported servers
Closes #254.
2017-09-23 13:20:58 -07:00
James Lu
ae02a9ba4f README: reword branch notes to be more neutral
[ci skip]
2017-09-20 20:54:18 -07:00
James Lu
746c6783da PyLink 1.2.1 2017-09-19 21:15:48 -07:00
James Lu
0e45fbdf55 Revert "faq: add a note regarding #497 (bans and modes blocking clientbot from relaying)"
This reverts commit 7ae22dc848a3dc4988daa67fc1cd095dc5511c49.
2017-09-19 21:15:48 -07:00
James Lu
594e8ad771 p10: fix wrong hook name for user introduction
(cherry picked from commit 6dec4bd96fde8b6c0d7285680768dfd427180622)
2017-09-19 21:06:14 -07:00
James Lu
6dec4bd96f p10: fix wrong hook name for user introduction 2017-09-17 13:59:42 -07:00
James Lu
0c50091d11 Merge branch 'master' into devel 2017-09-08 19:12:17 -07:00
James Lu
49136d5abd core: raise better errors on common logging block syntax mistakes
These are commonly reported and include:
- Commenting out the contents of logging:channels without commenting out the "channels:" heading, causing that block to become None.
- Commenting out headers like "filerotation:", causing its body to become pairs in logging:files or something similar.
- Leaving logging:channels:<netname> empty: this causes it to become None, so using get() on it fails.
2017-09-08 19:07:03 -07:00
James Lu
499fe319aa permissions-api: fix markdown syntax [skip ci] 2017-09-08 19:02:51 -07:00
James Lu
4cf7b36b7b permissions-api: clarify how globs are processed
[skip ci]
2017-09-08 19:02:03 -07:00
James Lu
b9a4010acc Merge branch 'master' into devel
Conflicts:
	plugins/relay.py
2017-09-05 21:22:16 -07:00
James Lu
93704d85bf unreal: fix TypeError when a set is given for modes in mode() 2017-09-05 21:21:16 -07:00
James Lu
ae06484aea exttargets: tweak docstring syntax for better readability 2017-09-05 19:19:44 -07:00
James Lu
d3892a85be Add $service exttarget for matching service bots 2017-09-05 19:19:01 -07:00
James Lu
149fdde92f README: drop Debian repo instructions
There seems to be little user interest in these builds, and they take too much maintenance effort (3 different dists) to be worthwhile.
2017-09-05 19:11:37 -07:00
James Lu
f3e82cc15b Concisify match_host CIDR logging again
We're replacing the glob, not the target's host.

(cherry picked from commit 7aa836efa68962c5202a8e6346785e2ea60d2003)
2017-09-05 19:03:18 -07:00
James Lu
d1f8358159 Fix wrong logging for CIDR ranges in match_host
(cherry picked from commit 761d3ef500db4991c2546df89eda35ba67c243ea)
2017-09-05 19:03:18 -07:00
James Lu
90884924a8 relay: remove a useless logging line
(cherry picked from commit 3b091f9e204ef17e6f4a63245f8f251fe982f877)
2017-09-05 19:03:17 -07:00
James Lu
f27b179211 Merge branch 'master' into devel
Conflicts:
	plugins/automode.py
	protocols/clientbot.py
2017-09-05 18:57:24 -07:00
James Lu
7188081511 networks: throw a proper error in 'remote' if the remote network isn't connected 2017-09-05 18:55:19 -07:00
James Lu
8420587318 clientbot: warn when an outgoing message is blocked
Closes #497.

(backported from commit 5112fcd7d105388b324e2fce6b18d022f84b8ab8)
2017-09-05 18:53:33 -07:00
James Lu
f4c51cde00 automode: don't send empty mode lines if no users match the ACL 2017-09-05 18:36:37 -07:00
James Lu
0a72519155 exttargets.md: bump "as of" PyLink version
[skip ci]
2017-09-05 06:30:23 -07:00
James Lu
5112fcd7d1 clientbot: warn when an outgoing message is blocked
Closes #497.
2017-09-02 21:46:35 -07:00
James Lu
0136ac9e41 relay: fix potential irc.channels KeyErrors in get_prefix_modes and handle_join 2017-09-02 21:35:21 -07:00
James Lu
afd4558531 relay: bind handle_messages at a higher priority than fantasy
This fixes #123, where responses for fantasy commands are relayed before the original message if the Fantasy plugin is loaded before Relay.
2017-09-02 21:17:54 -07:00
James Lu
5e92aefcd4 Implement priorities in utils.add_hook()
This changes world.hooks to store lists of tuples indicating (priority, hook_func).
2017-09-02 21:17:50 -07:00
James Lu
0ea35dab18 Merge branch 'master' into devel
Conflicts:
	docs/technical/channel-modes.csv
	docs/technical/user-modes.csv
	protocols/hybrid.py
2017-08-31 15:18:38 -07:00
James Lu
0eb605219b conf: use splitext() to fetch the config name
This fixes incorrect behaviour when there are multiple .'s in the config filename (e.g. '1.2.3.yml' got truncated to confname='1')
2017-08-31 15:14:47 -07:00
James Lu
1f270c985f hybrid: drop EX and IE from required capabilities
This fixes compatibility with hybrid trunk, commit ircd-hybrid/ircd-hybrid@981c61e36c

(cherry picked from commit 30dc4a2b27d6b6e6d68b0d154fbafd737207596e)

Conflicts:
	protocols/hybrid.py
2017-08-31 15:07:04 -07:00
James Lu
a03214514c hybrid, ts6, ratbox: fix +p mode definitions properly
+p is noknock and rfc1459-style private on all 3 IRCds, though the latter bit is undocumented in /help cmodes (maybe it's assumed?)
+p is also "paranoia" on hybrid, which adds more restrictions to halfops and /invite

(cherry picked from commit c107f0062ffb25a54d52946a26725940865318bb)
2017-08-31 15:06:25 -07:00
James Lu
32130a7988 channel/user-modes.csv: add RFC1459 & ngIRCd columns
(cherry picked from commit 0a56ab662bbb01ecc23f4de8dae955333a8b8eb4)
2017-08-31 15:06:25 -07:00
James Lu
dd5a0c4892 ts6: fallback realhost to host, not None
(cherry picked from commit dfa90378dfd056ade41c18ba213cd8408fc237b6)
2017-08-31 14:57:28 -07:00
James Lu
74ae6fd7c0 p10: add support for nefarious2 extbans
Closes #524.
2017-08-31 14:52:03 -07:00
James Lu
7fcefa41af example-conf: remove wrong advice regarding P10 extended_accounts
Clearly I misread readme.features, oops.

(cherry picked from commit 7f112e3c661ff19c1317df84228eb9ab7489ed41)
2017-08-31 14:35:32 -07:00
James Lu
7f112e3c66 example-conf: remove wrong advice regarding P10 extended_accounts
Clearly I misread readme.features, oops.
2017-08-31 14:33:23 -07:00
James Lu
5c14a9c8c2 relay: add iterate_all_present to condense network iteration code
Closes #471.
2017-08-31 14:19:30 -07:00
James Lu
2d2b524a63 ngircd: disable slash-in-nicks
This breaks user mode changes, as it is one of the only commands to undergo nick validation when received from a remote server.
2017-08-31 14:09:04 -07:00
James Lu
5c981c83b1 core: abort autoconnect and socket connectons when the daemon is shutting down 2017-08-31 13:40:11 -07:00
James Lu
82a7b914b6 Move control.tried_shutdown to world.shutting_down 2017-08-31 13:36:50 -07:00
James Lu
19bd3ec0b2 Merge branch 'wip/rework-endburst' into devel 2017-08-31 13:27:54 -07:00
James Lu
9e7af9ac3d ServiceBot: migrate to irc.is_channel 2017-08-31 13:19:34 -07:00
James Lu
ac89f45683 ngircd: rework NJOIN code
- Fix "Internal NJOIN error"s caused by joining users already in the channel again
- Fix NJOIN being sent from the wrong internal server
- Condense two iterations over the user list into one
2017-08-31 13:17:28 -07:00
James Lu
450718cce6 relay: don't block on client spawning
Just fail instantly if the remote isn't ready.
2017-08-31 12:27:52 -07:00
James Lu
e02393c22b Merge branch 'devel' into wip/rework-endburst 2017-08-31 12:20:58 -07:00
James Lu
85a7dd3dff example, relay_cb: migrate to irc.is_channel 2017-08-30 21:22:19 -07:00
James Lu
36d6581bba automode, bots: migrate to irc.is_channel 2017-08-30 21:20:19 -07:00
James Lu
979d5a48f1 relay: migrate away from deprecated utils.isHostmask 2017-08-30 21:09:19 -07:00
James Lu
9380336948 relay: fall back to the current time on TS-less servers if the remote channel doesn't exist 2017-08-30 21:08:54 -07:00
James Lu
136e5fbee7 conf: fix getDatabaseName calling the wrong variable name
(cherry picked from commit 21b8b51cba98f0f8e8208748758f4d00347374d4)
2017-08-30 20:13:51 -07:00
James Lu
94e05a6233 services_support: fix clientbot service spawning when irc.pseudoclient exists but isn't in the user index 2017-08-30 19:50:25 -07:00
James Lu
bc48709595 PyLinkNetworkCore: fix extraneous warnings in get_service_bot 2017-08-30 19:48:46 -07:00
James Lu
8170e777e8 protocols: move setting irc.connected to endburst 2017-08-30 19:39:57 -07:00
James Lu
cad55097f1 core: reuse existing service client UIDs for all service bots
This prevents nick collision wars caused by spawn_service when an ENDBURST hook for the uplink is received multiple times.
2017-08-30 19:29:46 -07:00
James Lu
21b8b51cba conf: fix getDatabaseName calling the wrong variable name 2017-08-30 19:21:43 -07:00
James Lu
9a84dbde71 protocols: consistently track ENDBURST on sub-servers too 2017-08-30 19:18:39 -07:00
James Lu
87639ddeb2 classes: add a has_eob attribute to each server object 2017-08-30 19:16:54 -07:00
James Lu
5084cc2c69 pmodule-spec: rewording, fix headings 2017-08-30 01:29:09 -07:00
James Lu
46e9975bd5 Continue rewriting the protocol module spec (#478) 2017-08-30 01:26:35 -07:00
James Lu
4a363aee50 Move _expandPUID into PyLinkNetworkCoreWithUtils 2017-08-30 00:56:18 -07:00
James Lu
5b941daf4d Begin rewriting the protocol spec for PyLink 2.0 (#478) 2017-08-30 00:34:42 -07:00
James Lu
3922d44173 utils: rename remaining functions to snake case (#476) 2017-08-28 20:28:10 -07:00
James Lu
43b6566aa8 Move getDatabaseName from utils to conf (#476) 2017-08-28 20:27:39 -07:00
James Lu
ff8587736f fantasy, relay: migrate to irc.is_* 2017-08-28 20:14:14 -07:00
James Lu
d79f1766b6 classes, coremods: migrate to irc.is_* 2017-08-28 20:13:25 -07:00
James Lu
c4a3baca7d protocols: switch to self.is* 2017-08-28 20:07:36 -07:00
James Lu
e39b4e9c69 Move utils.is* methods into PyLinkNetworkCoreWithUtils (#476) 2017-08-28 20:07:31 -07:00
James Lu
2a7594e56e Move PUIDGenerator->classes, IncrementalUIDGenerator->ircs2s_common (#476) 2017-08-28 19:42:24 -07:00
James Lu
b1159400f1 Move DeprecatedAttributesObject, CamelCaseToSnakeCase to structures (#476) 2017-08-28 19:36:55 -07:00
James Lu
0907f05296 Condense (deep)copy definitions into structures.CopyWrapper 2017-08-25 17:05:53 -07:00
James Lu
8c0f19422f core: Add irc argument to User and Server classes
Also, add a __deepcopy__ override to channel because we cannot clone IRCNetwork objects (locks cannot be pickled).
2017-08-25 13:53:45 -07:00
James Lu
96a202acce core: make User.channels use IRCCaseInsensitiveSet
Closes #515. This is an API breaking change!
2017-08-25 13:26:34 -07:00
James Lu
2700e42ebf structures: rework classes & implement (IRC)CaseInsensitiveSet 2017-08-25 13:21:02 -07:00
James Lu
1cdb5fc025 hybrid, ratbox: remove extban definitions 2017-08-25 12:24:09 -07:00
James Lu
ba649fb8b4 utils, exttargets: add checks for channel presence 2017-08-25 02:31:26 -07:00
James Lu
1031aaa320 relay: add checks for channel presence 2017-08-25 02:29:10 -07:00
James Lu
cdb575236e inspircd: remove a useless and misformatted debug line 2017-08-25 02:13:15 -07:00
James Lu
80766e051e core: define two (joined) versions of the channels index
Closes #509.

PyLinkNetworkCore.channels is split into the following:
- irc._channels which implicitly creates channels on access (mostly used in protocol modules)
- irc.channels which does not (recommended for use by plugins)
2017-08-25 02:12:08 -07:00
James Lu
f34198647e structures: support a 'data' keyword argument in (IRC)CaseInsensitiveDict 2017-08-25 02:07:54 -07:00
James Lu
a02fa45d96 clientbot: use existing fallback hostname abstraction instead of hardcoding it separately 2017-08-24 01:12:45 -07:00
James Lu
7230aaa7df Add extbans docs (#498) 2017-08-24 01:07:50 -07:00
James Lu
b214a8f4c0 unreal: fix wrong case in opertype extban 2017-08-24 00:55:55 -07:00
James Lu
1408622694 ts6: add definitions for inverted extbans as well 2017-08-23 23:23:28 -07:00
James Lu
579bfecdb4 relay: improve logging related to extban handling 2017-08-23 23:23:02 -07:00
James Lu
11d63e19cd relay: allow adding back multiple extban prefixes (#498)
This fixes inbound relaying of modes such as +q $a:test, which are eventually converted into double extbans on InspIRCd and UnrealIRCd.
2017-08-23 22:29:14 -07:00
James Lu
c6ca89b48a Fill in the rest of the extbans list for inspircd & unreal (#498) 2017-08-23 21:47:43 -07:00
James Lu
de1a6379df Add (experimental) support for matching extbans (#498) 2017-08-23 21:18:57 -07:00
James Lu
903f86a342 clientbot: add extended-join support (#290) 2017-08-23 19:28:10 -07:00
James Lu
abdc67e0c1 clientbot: track numeric 900 (RPL_LOGGEDIN) and set our own account name
This is mostly for completeness.
2017-08-23 19:19:53 -07:00
James Lu
c9f10796ee clientbot: move services account setting bits into a shared function 2017-08-23 19:16:20 -07:00
James Lu
c2fc9080cc clientbot: don't send JOIN hooks for the bot itself when receiving JOIN
Closes #519.
2017-08-23 19:11:15 -07:00
James Lu
96c89b13b1 opercmds: send KILL hook payloads from the calling user
This is so that Relay's kill warnings, etc. can be actually be routed back to the sender.
2017-08-22 23:55:53 -07:00
James Lu
bd6272abf6 clientbot: add a dummy internal host for the clientbot bot... 2017-08-21 23:28:10 -07:00
James Lu
060a947798 clientbot: whoops, fix wrong arg count for WHOX services acc handling 2017-08-21 23:27:47 -07:00
James Lu
29bb4c3dfd relay_clientbot: bump default startup delay to 20 seconds 2017-08-21 23:16:38 -07:00
James Lu
f5f30c118a structures: add collections.abc import (py3.4 support) 2017-08-21 23:15:38 -07:00
James Lu
83183b366a exec: import all of pylinkirc for convenience 2017-08-21 23:12:42 -07:00
James Lu
62e4e66321 example-conf: dcument the altnicks option on clientbot 2017-08-21 23:12:22 -07:00
James Lu
0bb4a35c6f Support pre-auth irc.pseudoclient enumeration & configurable altnicks
Closes #516. Closes #288.
2017-08-21 23:05:56 -07:00
James Lu
04cfa9c93e ServiceBot: add altnick fetching capabilities to get_nick (#288) 2017-08-21 22:22:01 -07:00
James Lu
d28006ae62 Move ProtocolError to utils, and link the copy in classes to it 2017-08-21 22:21:09 -07:00
James Lu
89699051d5 Services API rework
- Move nick/ident/host/gecos fetching from services_support into functions
- Remove the unused 'ident' argument from ServiceBot
- Rename the 'nick' argument in ServiceBot to 'default_nick'
- Define default nicks for the PyLink, Automode, and Games services
2017-08-21 21:51:45 -07:00
James Lu
3e150d8514 Break up classes.ChannelState into structures.(IRC)CaseInsensitiveDict 2017-08-21 21:50:44 -07:00
James Lu
6981869c06 clientbot: split up join() and handle_join() to prevent duplicate JOIN/MODE/WHO on connect 2017-08-20 19:24:46 -07:00
James Lu
81f3112d01 clientbot: use WHOX where available to fetch account names on join 2017-08-20 18:49:07 -07:00
James Lu
55f50eb251 clientbot: don't send /NAMES on join, it isn't needed 2017-08-20 17:29:03 -07:00
James Lu
85f283c5f8 clientbot: add support for IRCv3.2 account-tag (#290) 2017-08-20 16:52:23 -07:00
James Lu
48c6765411 clientbot: add support for IRCv3.1 account-notify (#290) 2017-08-20 16:12:25 -07:00
James Lu
0bbe5d3a1a clientbot: add support for IRCv3 CHGHOST (#290) 2017-08-19 23:55:41 -07:00
James Lu
36e8ddd97e Remove log/ from source folder, it's not necessary 2017-08-19 23:23:41 -07:00
James Lu
766450afd3 clientbot: add experimental userhost-in-names support (#290) 2017-08-19 23:23:19 -07:00
James Lu
471733bfd0 clientbot: track /who received status by user, not by channel 2017-08-19 22:29:08 -07:00
James Lu
72145e09b8 clientbot: implement away-notify support (#290) 2017-08-19 22:04:24 -07:00
James Lu
ffc734d8e2 relay: only send RAW_MODES payloads to networks without can-spawn-clients 2017-08-18 15:51:14 -07:00
James Lu
15be760b19 relay: send RELAY_RAW_MODE payloads to the *remote* channel name 2017-08-18 13:30:17 -07:00
James Lu
8563556850 relay: fix relaying between channels not matching their lowercase (DB) name
This rewrites get_relay() to be case insensitive by taking the IRC object instead of a string name.
This fixes a regression introduced by 32249ac (case-insensitive channel state)
2017-08-18 12:42:47 -07:00
James Lu
70fbbb206e Merge branch 'master' into devel
Conflicts:
	VERSION
2017-08-16 23:51:31 -07:00
James Lu
d273941dc8 launcher: add a -R/--rehash option 2017-08-16 12:28:40 -07:00
James Lu
5526dcedca launcher: add --restart/-r and --stop/-s options
Closes #244.
2017-08-16 12:22:43 -07:00
James Lu
d818c17072 control: don't spew "Stopping plugins" notices if none are loaded 2017-08-16 12:17:19 -07:00
James Lu
3b6c1e56c4 launcher: move reset_permissions() call into the permissions module 2017-08-16 11:21:06 -07:00
James Lu
d03924ae82 launcher: add checks for stale PID files via psutil on Unix
This hasn't been tested on other systems, so it is disabled there.

Closes #512.
2017-08-16 11:12:20 -07:00
James Lu
361a3de9dd faq: stop advertising PyLink as cross platform
Windows support has not been tested for a long time, and there doesn't seem to be much developer/user interest in this.
2017-08-16 10:51:17 -07:00
James Lu
7aa836efa6 Concisify match_host CIDR logging again
We're replacing the glob, not the target's host.
2017-08-16 00:03:57 -07:00
James Lu
761d3ef500 Fix wrong logging for CIDR ranges in match_host 2017-08-15 21:26:18 -07:00
James Lu
394057c7a1 PyLink 1.2.0 2017-08-14 17:34:20 -07:00
James Lu
d679859d7d relay: explain/comment the code for extban handling 2017-08-12 17:12:56 -07:00
James Lu
e180f9fa56 docs/modelists: add in chatircd +qah 2017-08-12 13:58:57 -07:00
James Lu
d2466dd33c relay: support relaying channel mode changes as text
Closes #389.

This adds a new internal hook RELAY_RAW_MODE, which is called once on every relayed network but with the mode list from the source network.
2017-08-11 19:22:14 -07:00
James Lu
92dae6db5e docs/t: link to html versions of mode lists 2017-08-11 15:19:46 -07:00
James Lu
4c0d765a2f docs: revamp mode list docs
- HTML version now available via a .csv -> .html generator script
- Addded a column for ChatIRCd
2017-08-11 15:14:55 -07:00
James Lu
30dc4a2b27 hybrid: drop EX and IE from required capabilities
This fixes compatibility with hybrid trunk, commit ircd-hybrid/ircd-hybrid@981c61e36c
2017-08-11 15:09:53 -07:00
James Lu
c107f0062f hybrid, ts6, ratbox: fix +p mode definitions properly
+p is noknock and rfc1459-style private on all 3 IRCds, though the latter bit is undocumented in /help cmodes (maybe it's assumed?)
+p is also "paranoia" on hybrid, which adds more restrictions to halfops and /invite
2017-08-11 13:45:40 -07:00
James Lu
e17837cbb1 relay: add nickflood (inspircd +F) to whitelisted cmodes 2017-08-11 13:32:04 -07:00
James Lu
5250e41a94 relay: reformat whitelisted mode lists 2017-08-11 13:29:45 -07:00
James Lu
4a6f94f8fc relay: add workarounds for u-lined servers
- Allow ulines to join with modes (this status gets forwarded as part of relay_joins()), but *ignore* this status when checking for claim
    - For U-lined clients to set modes properly and kick, they be on the claim list
- Allow ulines to set modes on ulined clients including itself. These changes are ignored: not forwarded, not bounced, but just left there.
- Ignore uline attempts to set non-list modes - this is mostly for compatibility with Anope's DEFCON, as it would otherwise lead to a mode flood.

This requires commit 9113b34b46ba6aa2b381b3c7e9822b4f411caaec, i.e. the get_server() update.
2017-08-11 13:15:03 -07:00
James Lu
9113b34b46 NetworkCore: return the server in get_server if the arg was a server 2017-08-11 13:13:50 -07:00
James Lu
046fe0c385 protocols: add headers to modules where missing 2017-08-11 12:27:58 -07:00
James Lu
8df7b5319e Move handle_realhost/handle_login to ts6 2017-08-11 12:21:41 -07:00
James Lu
dfa90378df ts6: fallback realhost to host, not None 2017-08-11 12:19:23 -07:00
James Lu
785fc8d2d2 ts6: handle ChatIRCd ENCAP USERMODE
Per https://github.com/ChatLounge/ChatIRCd/blob/master/doc/technical/ChatIRCd-extra.txt
2017-08-11 12:15:05 -07:00
James Lu
4ec1727888 unreal: rewrite/condense usermode change handling to be more consistent
Consistently call _check_oper_status_change() and _check_cloak_change() through a _handle_umode() wrapper. Also, handle the real portion of the mode change given in SVS2MODE.
2017-08-11 12:03:56 -07:00
James Lu
e13f2fdbb0 unreal: remove obsolete note regaring server-sourced notices 2017-08-10 19:53:16 -07:00
James Lu
6a5ca6b508 Merge bits of handle_server into ts6_common 2017-08-10 19:50:32 -07:00
James Lu
d0f9a2465d Move handle_sid to ts6_common 2017-08-10 19:42:04 -07:00
James Lu
614b8b87da inspircd: rewrite handle_ping to handle one-arg PING 2017-08-09 16:55:02 -07:00
James Lu
5381e85d3c IRCS2SProtocol: fix wrong if: statement regarding user presence 2017-08-09 16:06:37 -07:00
James Lu
00f70a9432 opercmds: remove extra newline in 'massbanre' help 2017-08-08 21:28:27 -07:00
James Lu
9044d17863 Merge pull request #507 from IRC4Fun/devel
automode: use 'setacc' instead of 'set' as the command name in the 'setacc' example
2017-08-08 20:19:16 -07:00
Austin Ellis
fa0dd100e5 plugins/automode: fix SETACC example
Small fix to plugins/automode SETACC example given in help output.
2017-08-08 22:12:23 -05:00
James Lu
17ba9be238 exec.threadinfo: use case-insensitive sort 2017-08-08 00:30:47 -07:00
James Lu
486a225156 inspircd: mark endburstf() threads as daemon=True
There is no reason whatsoever for this to even potentially block shutdown.
2017-08-08 00:27:05 -07:00
James Lu
64f05bd28b inspircd: set a name on endburstf() threads 2017-08-08 00:26:32 -07:00
James Lu
8b771f6d28 exec: add 'threadinfo' command 2017-08-08 00:22:57 -07:00
James Lu
8558a4e56d stats: treat stats characters case-insensitively 2017-08-08 00:00:12 -07:00
James Lu
7d26ce4ab5 stats: log to INFO attempts to call unknown /stats 2017-08-07 23:58:30 -07:00
James Lu
29fc73193f stats: removed unused code 2017-08-07 23:55:53 -07:00
James Lu
be41f57795 PyLink 2.0-dev, now with /stats support!
Closes #131.
2017-08-07 23:52:16 -07:00
James Lu
9702030bf5 protocols: continue removing to_lower() calls on channels (#372) 2017-08-07 21:54:33 -07:00
James Lu
29458c8e47 commands, opercmds: remove explicit to_lower usage
Closes #500 - I'm going to skip relay and automode because DataStore doesn't do case normalization yet.
2017-08-07 21:47:31 -07:00
James Lu
f439267129 Allow limiting login blocks to opers & certain hosts
Closes #502.
2017-08-07 21:44:15 -07:00
James Lu
716bb6da5e opercmds: exempt service bots from masskill/massban 2017-08-07 17:24:44 -07:00
James Lu
90eee9f5cb opercmds: skip opers by default in masskill/massban 2017-08-07 17:22:56 -07:00
James Lu
a080fd253f opercmds: log massban and masskill results to INFO 2017-08-07 17:15:23 -07:00
James Lu
14bebd98e7 control: fix import loop with pylinkirc.classes 2017-08-07 17:06:56 -07:00
James Lu
1597c78089 opercmds: fix wrong counting in 'masskill' if a user gets kicked from multiple channels 2017-08-07 16:55:00 -07:00
James Lu
7266f09879 opercmds: add masskillre variant
Closes #499.
2017-08-07 16:50:15 -07:00
James Lu
8987b91845 opercmds: fix --force-kb check on masskill 2017-08-07 16:50:04 -07:00
James Lu
dd1444dcd9 corecommands: block 'identify' from being used by command proxies such as networks.remote
This would have pretty bad unintentional consequences...
2017-08-07 16:31:28 -07:00
James Lu
e0e929492e match_host: implicitly convert string masks such as "user1" to "$pylinkacc:user1"
This keeps it in line with other services packages and is way nicer to read.
2017-08-07 16:31:17 -07:00
James Lu
8059f3f7fc Allow specifying login blocks that are local to certain networks 2017-08-07 16:16:17 -07:00
James Lu
21a39de0b4 corecommands: alias 'identify' to 'login' and 'id' 2017-08-07 16:15:54 -07:00
James Lu
187ca11946 commands: show an error if 'echo' is called without a text argument. 2017-08-07 00:48:28 -07:00
James Lu
f3acb3c21d networks: support more specific 'remote' permissions by target network, service, and command
Closes #457.
2017-08-07 00:46:53 -07:00
James Lu
6e0145c3b7 networks: use nargs='+' instead of manual verification in 'networks' 2017-08-07 00:38:59 -07:00
James Lu
075b746f32 opercmds: add a note to 'remote' in 'masskill' 2017-08-07 00:37:09 -07:00
James Lu
d10da72545 opercmds: reword docs/errors and add a separate permission for --force-kb 2017-08-07 00:34:03 -07:00
James Lu
f0a82859a0 opercmds: apply claim checks to massban as well 2017-08-07 00:33:01 -07:00
James Lu
8b4e73e0ed opercmds: show failed kills in 'masskill' due to claim 2017-08-07 00:18:56 -07:00
James Lu
6dc41ca15a opercmds: join reason fields in massban/masskill properly 2017-08-07 00:18:11 -07:00
James Lu
b1b2394836 ircs2s_common: ignore PART for channels that the user wasn't on
These extra parts caused issues with relay when receiving P10 KICK acknowledgements as they are treated as if the user was already in the channel.
2017-08-07 00:05:44 -07:00
James Lu
aca78b52cf opercmds: add masskill command (#499) 2017-08-06 23:49:09 -07:00
James Lu
5a00a632d6 Remove core-structure doc, outdated and not really useful 2017-08-06 23:13:20 -07:00
James Lu
a070ec5c32 PyLinkNetworkCore: add stubs for disconnect() and connect() 2017-08-06 21:49:52 -07:00
James Lu
d3f635901b classes: reorganize methods in NetworkCore 2017-08-06 21:49:41 -07:00
James Lu
ea70d34b28 docs/exttargets: refresh for 2.0 2017-08-06 21:42:09 -07:00
James Lu
2ff0007e56 exttargets: add $realname target 2017-08-06 21:41:44 -07:00
James Lu
9e97dd0b75 opercmds: rename hook payloads to be more consistent 2017-08-06 20:03:20 -07:00
James Lu
a72f710a69 Add regex variants to checkban and massban 2017-08-06 20:02:20 -07:00
James Lu
d12f12ae22 Add a 'massban' command
Closes #174.
2017-08-06 19:21:55 -07:00
James Lu
99790bfae2 p10: remove direct usage of to_lower() for channels
Ref #372
2017-08-06 18:11:34 -07:00
James Lu
c8a9163f57 ChannelState: add __repr__ 2017-08-06 18:09:37 -07:00
James Lu
07fa53d128 protocols: remove direct usage of to_lower() for channels
Not needed as of 32249ace3e579d8dc938a359cb5c7c4019abc31b (ref #372)
2017-08-06 18:05:14 -07:00
James Lu
c9c0e0a85b ChannelState: add copy and __contains__ methods 2017-08-06 18:04:46 -07:00
James Lu
cb36e29b92 opercmds: migrate to irc.match_all() 2017-08-06 17:56:27 -07:00
James Lu
cbb3c88e11 NetworkCoreWithUtils: add new match_all() method 2017-08-06 17:55:43 -07:00
James Lu
32249ace3e Store channels case-insensitively in a new classes.ChannelState
Closes #372.
2017-08-06 17:52:52 -07:00
James Lu
3c675bb163 inspircd, unreal: support the "block nick changes" extban
This is n: and ~n: on inspircd and unreal respectively.
2017-08-06 01:47:43 -07:00
James Lu
9ae851e1fc classes: rename NetworkCore.aborted to _aborted 2017-08-05 22:16:52 -07:00
James Lu
8d15d05711 IRCNetwork: use disconnect() to kill networks if parsing a command errors
This is more standard, as aborted is solely an internal value.
2017-08-05 22:14:44 -07:00
James Lu
f4da1fc94c Merge branch 'master' into devel
Conflicts:
	VERSION
	classes.py
	docs/faq.md
	example-conf.yml
	protocols/p10.py
	pylink
	world.py
2017-08-05 22:11:22 -07:00
James Lu
b456966dd3 relay: move ban-style and whitelist checks to earlier in get_supported_cmodes 2017-08-05 22:03:58 -07:00
James Lu
ffde2c6b32 relay: add in muteban relaying on InspIRCd and UnrealIRCd 2017-08-05 22:03:51 -07:00
James Lu
7db811f2dd protocols: declare quiet extban support where applicable 2017-08-05 21:52:34 -07:00
James Lu
b9a58670ef relay: cleanup code flow in handle_mode 2017-08-05 21:13:39 -07:00
James Lu
7ae22dc848 faq: add a note regarding #497 (bans and modes blocking clientbot from relaying)
This will be fixed more completely in the future. Thanks to @Ryan-Goldstein for reporting!
2017-08-03 10:44:47 -07:00
James Lu
a0a295f7d2 Set Irc.aborted earlier in the disconnect loop
This prevents plugins from getting really confused as we remove things.
2017-08-03 10:22:57 -07:00
James Lu
0d5afd266f Irc: stop extraneous queue threads when removing from world.networkobjects 2017-08-03 10:15:29 -07:00
James Lu
d734fc3280 servprotect: bump default conf up to 10 hits/10 seconds 2017-08-03 10:10:28 -07:00
James Lu
dcc171095f example-conf: spacing & comment tweaks
(cherry picked from commit a55f60c6dc7c02dcde42b43f751e8083593faeec)

Conflicts:
	example-conf.yml
2017-08-03 09:57:15 -07:00
James Lu
a639efa93e relay: allow overriding tag_nicks per network
Closes #494.

(cherry picked from commit 1d6b692e1490e538f5c8d9c16542ff45feab1f9f)

Conflicts:
	example-conf.yml
2017-08-03 09:55:29 -07:00
James Lu
e8efbb8e83 Merge branch 'master+example-conf-updates' 2017-08-03 09:54:02 -07:00
James Lu
819aab2248 example-conf: roughly sort the plugins' order by usefulness/popularity
(cherry picked from commit b24eefc87375ff81a0218309931cc15c1aa91902)
2017-08-03 09:51:00 -07:00
James Lu
13baef08c1 example-conf: roughly sort config options by usefulness
Also, remove the example for the deprecated pylink:prefixes option.

(cherry picked from commit 1eb274342166bc11d1daa4b9880e7a3990f32df5)

Conflicts:
	example-conf.yml
2017-08-03 09:50:52 -07:00
James Lu
7df19bae5f example-conf: tweak whether some options are commented out by default or not
(cherry picked from commit dbc1e80def928528e60969739f041f01779a250b)
2017-08-03 09:49:26 -07:00
James Lu
4379ef68ef Migrate coremods.permissions to snake case 2017-08-02 22:24:23 +08:00
James Lu
f15f27168a services_support: log the reason as well when a service bot is killed 2017-08-02 22:15:19 +08:00
James Lu
d42eb82b62 bots: remove obsolete "Admin-only" tags from command help 2017-08-02 22:10:04 +08:00
James Lu
76b8932eeb bots: remove duplicate nick_to_uid call 2017-08-02 22:09:48 +08:00
James Lu
529d1d84be opercmds: remove obsolete "Oper/admin only" tags from commands
These have been long superseded by the permissions API.
2017-08-02 22:06:08 +08:00
James Lu
a55f60c6dc example-conf: spacing & comment tweaks 2017-08-02 22:02:35 +08:00
James Lu
b947afabd0 example-conf: reorder some config options 2017-08-02 22:02:23 +08:00
James Lu
1d6b692e14 relay: allow overriding tag_nicks per network
Closes #494.
2017-08-02 21:57:48 +08:00
James Lu
def1c0bfd9 opercmds: use irc.reply() instead of irc.msg(source, ...) 2017-08-02 21:48:05 +08:00
James Lu
acceb4e714 opercmds: reword checkban docs 2017-08-02 21:45:20 +08:00
James Lu
bf495a0aae opercmds.checkban: make maxresults configurable, up to a cap of 200 2017-08-02 21:44:17 +08:00
James Lu
981e6c508f Move _check_nick_collision to NetworkCoreWithUtils
This is useful for networks that emulate IRC as well, to prevent clashes between real clients and virtual ones.
2017-07-31 20:58:02 +08:00
James Lu
47f3977554 Move NetworkCore.parse_protocol_command to IRCNetwork.parse_irc_command
Also add a stub for handle_events.
2017-07-31 11:09:08 +08:00
James Lu
b9a5de16de user-modes.csv: fix wrong names for sno_serverconnects, sno_stats 2017-07-31 00:57:51 +08:00
James Lu
0a56ab662b channel/user-modes.csv: add RFC1459 & ngIRCd columns 2017-07-31 00:56:10 +08:00
James Lu
eae1425975 NetworkCore: remove nonexistent cmode +r from mode type definitions 2017-07-31 00:43:19 +08:00
James Lu
9345f2549b Merge branch 'master+faq-updates'
Sync docs/faq.md with the 2.x (devel) branch.
2017-07-31 00:34:29 +08:00
James Lu
7b281e4b04 faq: tweak wording
(cherry picked from commit 17a4bbea87686d814eb2292d0142f8a8fdf591b4)
2017-07-31 00:33:34 +08:00
James Lu
381d96552b faq: fix formatting in the connection troubleshooting guide
(cherry picked from commit f1f8f91bd7b6097c962b047b5b644b0c6bb9632b)
2017-07-31 00:33:32 +08:00
James Lu
5d000c9930 faq: add a section regarding #463 (sporadic SSL-related errors)
(cherry picked from commit 99acd06e02f8543f8d9196fa63913f57fe42a9a8)
2017-07-31 00:33:31 +08:00
James Lu
8e67017c81 faq: be slightly less excited
(cherry picked from commit cc4890184cc00ebc736697cbe5609c7e14bf386c)
2017-07-31 00:33:31 +08:00
James Lu
c9734cc0af faq: add "Relay users are missing" section
(cherry picked from commit 0d9c53a4f401f74ad9fdc369ab0c065f53d6b948)
2017-07-31 00:33:30 +08:00
James Lu
b41234ad79 faq: add a troubleshooting guide for connection failures
(cherry picked from commit bc5474a43a73a40303b6ba3a62c02825aa2a33c9)
2017-07-31 00:33:29 +08:00
James Lu
f18f7de9f7 faq: sort by subtopic
(cherry picked from commit 42ce9c83b5e6827cd1140ef4ff482248b78ccc11)
2017-07-31 00:33:28 +08:00
James Lu
c16ca14fba faq: reword "advantages over Janus" section
(cherry picked from commit 9dcc94129256e3d7f037514397fba11fc001d1e1)
2017-07-31 00:33:27 +08:00
James Lu
54dab5f107 faq: remove obsolete note about clientbot support
The answer is fairly obvious now, and it's even mentioned in the readme.

(cherry picked from commit 01dc2505e2c2aff9ca0a30a22bca63eed220287f)
2017-07-31 00:33:27 +08:00
James Lu
bed40cc10a faq: add point about maxnicklen misconfigurations and relay
(cherry picked from commit c7f300357c12e6c1aac1cf7fd7e76a42aef5f447)
2017-07-31 00:33:26 +08:00
James Lu
17a4bbea87 faq: tweak wording 2017-07-31 00:29:11 +08:00
James Lu
f1f8f91bd7 ffaq: fix formatting in the connection troubleshooting guide 2017-07-31 00:27:48 +08:00
James Lu
99acd06e02 faq: add a section regarding #463 (sporadic SSL-related errors) 2017-07-31 00:21:16 +08:00
James Lu
cc4890184c faq: be slightly less excited 2017-07-31 00:19:26 +08:00
James Lu
0d9c53a4f4 faq: add "Relay users are missing" section 2017-07-31 00:06:19 +08:00
James Lu
bc5474a43a faq: add a troubleshooting guide for connection failures 2017-07-31 00:01:07 +08:00
James Lu
b24eefc873 example-conf: roughly sort the plugins' order by usefulness/popularity 2017-07-30 23:44:27 +08:00
James Lu
42ce9c83b5 faq: sort by subtopic 2017-07-30 23:42:23 +08:00
James Lu
9dcc941292 faq: reword "advantages over Janus" section 2017-07-30 23:40:26 +08:00
James Lu
01dc2505e2 faq: remove obsolete note about clientbot support
The answer is fairly obvious now, and it's even mentioned in the readme.
2017-07-30 07:57:05 -05:00
James Lu
c7f300357c faq: add point about maxnicklen misconfigurations and relay 2017-07-30 07:54:58 -05:00
James Lu
02bd2035e4 opercmds: refactor checkban to use IRCParser, and implement --channel
--channel is really just a wrapper around the $and and $channel exttargets, but it exists separately for convenience. Closes #491.
2017-07-30 01:12:02 +08:00
James Lu
9b271efc89 opercmds: use %r instead of '%s' in formatting 2017-07-30 00:37:27 +08:00
James Lu
3eb2c6bbf7 service_support: raise KILLs to service bots to INFO 2017-07-30 00:32:59 +08:00
Ken Spencer
79d66ad94d .gitignore: ignore .idea/ due to PyCharm (#492)
* PyCharm keeps state inside of .idea/ which may contain sensitive information
2017-07-24 23:21:13 +08:00
James Lu
f53969a4b2 launcher: reword -n description to mention that it skips PID file checking as well 2017-07-20 21:39:36 +08:00
James Lu
9bec181cec launcher: restore -c/--check-pid as a no-op option for compatiblity 2017-07-20 21:39:29 +08:00
James Lu
dac0d5b234 core: Properly track whether we should actually remove the PID file
Previously, PyLink spuriously removed PID files even if -n/--no-check-pid was set or if PID file checking caused PyLink to quit

(cherry picked from commit 91659ea992e5be9fc645f8805a09c2dffef4148e)
2017-07-20 21:33:17 +08:00
James Lu
b90da19dfa control: log the name of the PID file on shutdown
(cherry picked from commit d57e141fbce83ea5c91f292d5db805af8b515e68)
2017-07-20 21:31:54 +08:00
James Lu
498a32a525 launcher: reword "PID exists" error 2017-07-20 21:22:01 +08:00
James Lu
1630d176d0 launcher: re-enable PID file checking by default 2017-07-20 21:19:00 +08:00
James Lu
819ac4d406 Move world.log_queue to world._log_queue 2017-07-20 21:16:44 +08:00
James Lu
91659ea992 core: Properly track whether we should actually remove the PID file
Previously, PyLink spuriously removed PID files even if -n/--no-check-pid was set or if PID file checking caused PyLink to quit
2017-07-20 21:13:01 +08:00
James Lu
2113f834a3 Rework the launcher to always call the installed copy of PyLink
This should prevent cryptic errors caused by mismatched PyLink core/launcher versions.
2017-07-20 21:01:16 +08:00
James Lu
63f52ca8b0 ircs2s_common: handle 'netadmin' umodes as an oper type
Also, make the Network Service opertype take precedence over the others.
2017-07-20 20:03:32 +08:00
James Lu
d707c243f4 Refresh mailmap 2017-07-20 18:46:35 +08:00
James Lu
b4ef0d1b16 PyLink 1.2.0-rc1 2017-07-20 18:41:53 +08:00
James Lu
19d41d7298 ngircd: add GLINE support
Also add IRCd notes regarding AllowRemoteOper and modeless channels.
2017-07-17 09:12:07 -07:00
James Lu
75e286fdc7 README: remove experimental tag for ngIRCd 2017-07-17 08:43:04 -07:00
James Lu
66576daf3f README: declare ChatIRCd support 2017-07-17 08:42:46 -07:00
James Lu
22e11d7811 README: add note about shared{} blocks on ts6 ircds 2017-07-17 08:40:35 -07:00
James Lu
b1d5ca36fb hybrid: add KLINE support (#139) 2017-07-17 08:37:43 -07:00
James Lu
f56fae4bc9 unreal: implement GLINE support (#139) 2017-07-17 08:13:28 -07:00
James Lu
7e8ff51646 conf: when config loading fails, show an error /before/ quitting
This fixes a regression from 2b346e3c01c094922e3d96411370c8394f5e38d4.
2017-07-17 07:56:23 -07:00
James Lu
cc9025a080 p10: add GLINE support (#139) 2017-07-17 07:50:48 -07:00
James Lu
410ade3b60 ts6: implement KLINE support (#139) 2017-07-17 06:29:44 -07:00
James Lu
ab8a922809 inspircd: add GLINE support (#139) 2017-07-17 06:12:12 -07:00
James Lu
c7c29f35e5 core: make message cutoff optional, and disable for inspircd
Closes #490.
2017-07-16 21:01:55 -07:00
James Lu
1f377adfee p10: also acknowledge our own kicks with a PART
(adapted from commit 1438f9e566a8f1778a448fd3fb3d1fc9da90a26b)
2017-07-16 07:34:45 -07:00
Mitchell Cooper
d1b321310e opercmds: add chghost, chgident, chgname commands (#488)
Closes #481.
2017-07-15 07:41:16 +08:00
James Lu
5a5a98c4ef services_support: migrate to conf.conf['pylink'], make the realname option optional 2017-07-14 05:51:40 -07:00
James Lu
06d3de354e utils: migrate to conf.conf['pylink'] 2017-07-14 05:51:29 -07:00
James Lu
b88830ba25 classes: migrate to conf.conf['pylink'] 2017-07-14 05:50:19 -07:00
James Lu
a164924ce5 Merge branch 'conf-host' of https://github.com/cooper/PyLink into devel 2017-07-14 05:38:45 -07:00
James Lu
880d0975db Merge branch 'type-to-isinstance' of https://github.com/cooper/PyLink into devel 2017-07-14 05:22:37 -07:00
James Lu
716ffd389b classes: mark reply_lock, init_vars as private 2017-07-14 05:22:05 -07:00
James Lu
fa4d831e44 NetworkCore: remove outdated, now misleading constructor description 2017-07-12 23:16:03 -07:00
James Lu
2e7fed84c1 IRCNetwork: mark connection_thread, pingTimer, socket, and queue as private 2017-07-12 22:56:30 -07:00
James Lu
2ef7df01e7 IRCNetwork: don't try to close the socket if none was ever initialized 2017-07-12 22:50:16 -07:00
James Lu
3cc6ea1e88 corecommands: remove duplicate error logging when REHASH fails 2017-07-12 22:44:51 -07:00
James Lu
2b346e3c01 conf: use Python logging when avaiable when the config file fails to load 2017-07-12 22:44:51 -07:00
James Lu
db778debb8 Fix error logging for validate_server_conf (#472) 2017-07-12 22:44:48 -07:00
James Lu
fceb2efce4 conf: remove ancient migration note from PyLink 0.9.x 2017-07-12 22:24:59 -07:00
James Lu
69bafedcca IRCNetwork: use a less confusing error than "No data received" 2017-07-12 22:22:08 -07:00
James Lu
d57e141fbc control: log the name of the PID file on shutdown 2017-07-12 22:22:08 -07:00
James Lu
561319bc57 networks: explicitly log successful network disconnects 2017-07-12 22:22:08 -07:00
James Lu
61db5d616d corecommands: use irc.get_hostmask instead of formatting the caller's hostname manually 2017-07-12 22:22:08 -07:00
James Lu
0e3d733a72 NetworkCore: set self.aborted to True as early as possible when launching a disconnect
This should prevent spurious "No data received" errors from popping up when using the 'disconnect' command.
2017-07-12 22:22:08 -07:00
James Lu
f85bdb3d8b IRCNetwork: suppress logging connection errors when PyLink is shutting down 2017-07-12 22:22:08 -07:00
James Lu
bb2b57f9dd control: mark signal handlers as private 2017-07-12 22:22:08 -07:00
James Lu
74f68c2176 control: mark rehash and shutdown as public 2017-07-12 22:22:08 -07:00
James Lu
22e6992770 core: consistently call die() with irc as a keyword argument 2017-07-12 22:22:08 -07:00
James Lu
b8a254167c p10: rename the 'p10_ircd' option to 'ircd', for consistency with ts6 2017-07-12 22:22:05 -07:00
James Lu
3d27e4a347 ts6: add support for ChatIRCd
This depreates the "use_elemental_modes" setting on ts6 networks, and replaces it with an "ircd" option targetting charybdis, elemental-ircd, or chatircd
Closes #339.
2017-07-12 22:21:34 -07:00
James Lu
b081270aa1 ts6: tweak some misplaced mode definitions
Charybdis provides deaf as umode +D and ssl as umode +Z
2017-07-12 22:21:34 -07:00
Mitchell Cooper
a1dfa14d20 allow realname to be specified the same way 2017-07-12 18:53:02 -04:00
Mitchell Cooper
33630e8f9d allow host to be specified in service bot block or per-network 2017-07-12 18:38:26 -04:00
Mitchell Cooper
3e356180a0 use isinstance() for conf values as well 2017-07-12 17:38:31 -04:00
Mitchell Cooper
7ab0e8f105 use isinstance() instead of type() where appropriate #410 2017-07-12 17:29:34 -04:00
Mitchell Cooper
87fe7693b0 ircs2s_common: use conf.validate() instead of assertion in validate_server_conf() (#485)
Closes #472.
2017-07-12 14:17:41 -07:00
Mitchell Cooper
c92bb1e33b relay: add server-specific server_suffix (closes #462) (#484) 2017-07-12 14:10:36 -07:00
James Lu
1eb2743421 example-conf: roughly sort config options by usefulness
Also, remove the example for the deprecated pylink:prefixes option.
2017-07-12 08:37:08 -07:00
James Lu
dbc1e80def example-conf: tweak whether some options are commented out by default or not 2017-07-12 08:33:28 -07:00
James Lu
cb368439cf inspircd: bring back extended WHOIS replies via a force_whois_extensions option 2017-07-12 08:28:32 -07:00
James Lu
0e4737e59d handlers: strip away '(on $network)' portions of relay oper types if the source and target netname are the same 2017-07-12 07:57:07 -07:00
James Lu
654df0889d relay: rename checkSendKey -> _check_send_key 2017-07-12 07:51:34 -07:00
James Lu
9a2bff25ee control: migrate to new log_setup() name 2017-07-12 07:50:34 -07:00
James Lu
50e7b0ab13 Limit signon time in WHOIS replies to service bot targets
Faking signon time for relay clients, etc. is misleading...
2017-07-12 07:49:19 -07:00
James Lu
556c2efb0a Make showing startup time an option (whois_show_startup_time) 2017-07-12 07:44:18 -07:00
James Lu
adaa6021f9 handlers: use conf.conf['pylink'] 2017-07-12 07:35:51 -07:00
James Lu
6a0859f56a handlers: send PyLink's connect time via 317 (RPL_WHOISIDLE) 2017-07-12 07:32:50 -07:00
James Lu
7c5f1533a0 handlers: fix weirdly named variables 2017-07-12 07:32:40 -07:00
James Lu
3fb563bb43 inspircd: get rid of IDLE->WHOIS hack
This is unnecessary as sending 0 for both the idle time and signon time will tell the IRCd to not show anything.

...But we track our startup time, so we can actually pass that as a value (Anope does this as well)
2017-07-12 07:23:00 -07:00
James Lu
1438f9e566 p10: also acknowledge our own kicks with a PART 2017-07-11 02:36:00 -07:00
James Lu
fbaa12de71 p10: use is_halfop_plus when checking whether we need to override 2017-07-11 02:27:44 -07:00
James Lu
508253af7e relay: switch to Channel.is_op_plus() 2017-07-11 02:23:13 -07:00
James Lu
1a8dcdfa3a NetworkCore: use the new validate_server_conf name 2017-07-11 02:22:01 -07:00
James Lu
f203abdeb0 relay: move iteration over all networks into a wrapper function
First part of #471.
2017-07-10 23:09:00 -07:00
James Lu
a43076e815 relay: rename isRelayClient to is_relay_client 2017-07-10 22:38:25 -07:00
James Lu
2f87aa63e9 relay: remove double iteration when firing the PYLINK_RELAY_JOIN hook 2017-07-10 22:36:43 -07:00
James Lu
5ed4f8bf85 ServiceBot: break when trying to alias a command to itself 2017-07-10 22:18:01 -07:00
James Lu
63ce7ea407 ServiceBot: tweak display format for command aliases
Specifically: bold the command lists, add a space before, and be more specific with "alias for X".
2017-07-10 22:12:53 -07:00
James Lu
bd19468825 automode: rename internal functions to snake case 2017-07-10 22:10:55 -07:00
James Lu
4df101c40c automode: fix alias definitions 2017-07-10 22:10:47 -07:00
James Lu
79db7b2124 automode: fix $ircop exttarget name in setacc examples
(cherry picked from commit 24caf36230849709707c867e6a33c1870bc630aa)
2017-07-10 22:07:11 -07:00
James Lu
59a4ecdcb9 automode: rewrap help for SET
(cherry picked from commit 499e94e0a52c91f6bd4e143fd1d221990e4ee3da)
2017-07-10 22:07:09 -07:00
James Lu
24caf36230 automode: fix $ircop exttarget name in setacc examples 2017-07-10 22:06:41 -07:00
James Lu
499e94e0a5 automode: rewrap help for SET 2017-07-10 22:06:08 -07:00
James Lu
6437721ec9 Merge branch 'hide-aliases' into devel 2017-07-10 22:00:29 -07:00
James Lu
bf24bac9c9 ServiceBot: replace 'alias' option with 'aliases' & condense multiple add_cmds calls into one 2017-07-10 21:59:29 -07:00
Mitchell Cooper
f0379d79ef mention that command is an alias or has aliases available in help command 2017-07-09 21:40:08 -04:00
Mitchell Cooper
20abac7461 hide aliases from command list 2017-07-09 21:23:52 -04:00
Mitchell Cooper
100089f6b8 add alias argument where appropriate in plugins 2017-07-09 21:19:08 -04:00
Mitchell Cooper
2299204efa add alias argument to add_cmd() 2017-07-09 21:18:45 -04:00
James Lu
b7466327db protocols: move S2S_BUFSIZE definition into a class variable 2017-07-07 20:14:26 -07:00
James Lu
1172ca7387 IRCNetwork: use \r\n as separator instead of \n
CRLF is the standard way of doing this per RFC1459
2017-07-07 20:14:26 -07:00
James Lu
51fb269d0d IRCNetwork: don't attempt to send more than 510 bytes per message
Some IRCds like ngIRCd will SQUIT you if you try to do so, though most just ignore this kind of overflow.
2017-07-07 20:14:23 -07:00
James Lu
3a42c8e835 protocols: add _check_oper_status_change abstraction
This condenses a large chunk of the code checking for oper ups, and adds support for the servprotect/admin umodes in an IRCd-independent manner. Closes #451.
2017-07-07 20:04:21 -07:00
James Lu
3bcf0092e9 NetworkCoreWithUtils: wrap irc.to_lower in a lru_cache 2017-07-07 14:40:27 -07:00
James Lu
f7dfc38688 relay: only initialize channels that are relevant to the called network in initialize_all()
Previously, this would quite often hit channel names that simply don't exist on the target network.
2017-07-07 14:20:24 -07:00
James Lu
3b091f9e20 relay: remove a useless logging line 2017-07-07 13:43:30 -07:00
James Lu
5955d3f90f p10: expand nick!user@host only for topic setters that are clients 2017-07-07 13:37:16 -07:00
James Lu
c2f12460da unreal, p10: condense topic_burst and topic together
Closes #480.
2017-07-07 13:35:30 -07:00
James Lu
d2d76baad8 relay: more detailed logging when the spawn_* condition times out 2017-07-07 13:32:04 -07:00
James Lu
67a36b7ebe inspircd: rename _operUp to _oper_up 2017-07-07 13:27:45 -07:00
James Lu
5d5c861a93 protocols: rename check_nick_collision to _check_nick_collision 2017-07-07 03:18:40 -07:00
James Lu
c5d06b2d41 ngircd: run check_nick_collision in handle_nick, per #375 2017-07-07 03:16:56 -07:00
James Lu
bd79c71b85 unreal: fix TypeError in mode() from accessing set items by index 2017-07-07 03:16:17 -07:00
James Lu
4b69edcbd7 README: declare support for ngircd 24+ 2017-07-07 02:56:28 -07:00
James Lu
28d2f89311 ngircd: implement KILL 2017-07-07 02:52:57 -07:00
James Lu
67a414fa2a ngircd: set slash-in-hosts, slash-in-nicks, underscore-in-hosts capabilities 2017-07-07 02:46:14 -07:00
James Lu
57c86c6d25 ngircd: implement update_client() 2017-07-07 02:41:20 -07:00
James Lu
fa2c5d928a IRCS2SProtocol: fix extraneous umode based AWAY messages 2017-07-06 22:17:47 -07:00
James Lu
6636a19a2b ngircd: implement handler for METADATA 2017-07-06 22:15:56 -07:00
James Lu
f29c95152b ngircd: remove has-ts from protocol capabilities 2017-07-06 21:49:51 -07:00
James Lu
b0eb1656a5 ngircd: add a stub for KNOCK 2017-07-06 21:47:03 -07:00
James Lu
6d3d2b239d IRCS2SProtocol: ignore attempts to ping the uplink before the link is ready 2017-07-06 21:45:51 -07:00
James Lu
d149576b4e protocols: move invite() into IRCS2SProtocol 2017-07-06 21:43:53 -07:00
James Lu
085b4cacbe protocols: handle usermode-based away (i.e. ngircd +a) 2017-07-06 20:19:52 -07:00
James Lu
b2b50371ab ngircd: fix setting umodes 2017-07-06 20:19:34 -07:00
James Lu
73464e516f ngircd: fill in mode definitions
Source: https://github.com/ngircd/ngircd/blob/master/doc/Modes.txt
2017-07-06 20:12:29 -07:00
James Lu
3d0ccadb76 ngircd: sort handler functions alphabetically 2017-07-06 18:38:25 -07:00
James Lu
28313fd478 ngircd: send burst modes after NJOIN, if there are any 2017-07-06 18:29:34 -07:00
James Lu
961e8ae991 ngircd: add outgoing MODE command 2017-07-06 18:22:56 -07:00
James Lu
4cd1ed5a7b ngircd: add an outgoing sjoin() function using NJOIN 2017-07-06 18:08:46 -07:00
James Lu
45dad63d5b Move handle_mode into IRCS2SProtocol
TODO: clean up protocols/unreal to use more of this code as well
2017-07-06 17:10:03 -07:00
James Lu
faa5b729d9 docs: update protocol-modules graphic 2017-07-05 03:28:28 -07:00
James Lu
694b5018fc Move numeric() into IRCS2SProtocol 2017-07-05 02:36:34 -07:00
James Lu
56c8b90362 IRCS2SProtocol: handle both killpath-based and preformatted kill reasons
Also drop the override in protocols/inspircd, as it is no longer needed.
2017-07-05 02:26:40 -07:00
James Lu
1e5985b608 Merge remote-tracking branch 'origin/beta' into wip/ngircd
Conflicts:
	protocols/ircs2s_common.py
	protocols/ts6.py
2017-07-05 02:26:40 -07:00
James Lu
58558c89ae ngircd: ignore KILLs not meant for us
ngIRCd sends QUIT after a successful KILL, so trying to remove the target twice is erroneous and will cause a crash.

TODO: what happens if an external KILL is never responded to for whatever reason?
2017-07-05 02:17:15 -07:00
James Lu
1acd654e6e ts6: fix 'ts' value type in handle_invite 2017-07-05 02:10:24 -07:00
James Lu
69f3ae52ec ts6: fix wrong argument count when parsing INVITE ts
(cherry picked from commit 7cfc63d6edcd218da82d75e2032359b50cc9891a)
2017-07-05 02:09:55 -07:00
James Lu
d2d176b6f9 IRCS2SProtocol: fix UnboundLocalError in "message coming from wrong way" warning
This fixes a regression from 69cf21c04e7fed2e3c3eff27e012f1b76e6a174b.
2017-07-05 02:08:41 -07:00
James Lu
30b9f47023 unreal: remove handle_kill override; unneeded as of aa4e9335aa7dac5884b8662fac49713c7dc221cc 2017-07-05 01:26:45 -07:00
James Lu
c2e65ff9c3 IRCCommonProtocol: alias topic_burst to topic by default (#480) 2017-07-05 00:56:34 -07:00
James Lu
db06ff4338 Move handle_topic to IRCS2SProtocol 2017-07-05 00:48:58 -07:00
James Lu
aa4e9335aa IRCS2SProtocol: expand nicks to UIDs in handle_kill
This allows this handler to work natively on ngIRCd.
2017-07-05 00:36:10 -07:00
James Lu
3729b23e43 Move KICK handlers to IRCS2SProtocol 2017-07-05 00:34:48 -07:00
James Lu
449b547a23 ngircd: properly track server tokens so that users spawn on the right servers
This brings in utils.PUIDGenerator once again for pseudo-SIDs; the counter numbers in these are used directly as server tokens
2017-07-05 00:18:13 -07:00
James Lu
4e082c2bbf PUIDGenerator: allow custom counter start values 2017-07-05 00:12:25 -07:00
James Lu
163f0099e7 IRCCommonProtocol: also expand PSIDs in _expandPUID 2017-07-04 23:56:12 -07:00
James Lu
5d4f2149e6 Move squit() to ircs2s_common 2017-07-04 23:41:00 -07:00
James Lu
9132556fd9 PyLinkNetworkCore: fix __repr__ definition 2017-07-04 23:32:41 -07:00
James Lu
b780070ee6 ngircd: implement nick changing 2017-07-04 23:26:05 -07:00
James Lu
42a25300c4 ngircd: don't leave user TS none in spawn_client 2017-07-04 23:25:49 -07:00
James Lu
759210a1e4 ngircd: add inbound & outbound JOIN, SERVER 2017-07-04 23:10:12 -07:00
James Lu
84a6cec732 p10: fix endburst_delay note 2017-07-04 23:09:48 -07:00
James Lu
37f0dcb456 ngircd: fix SQUIT user tracking 2017-07-04 23:09:13 -07:00
James Lu
970b38719d core: rename ping() to _ping_uplink(), and drop the unused source/target arguments 2017-07-04 22:09:50 -07:00
James Lu
43af9d1bac protocols: move ping() into IRCCommonProtocol 2017-07-04 22:00:22 -07:00
James Lu
276b0b251d protocols: move handle_pong to IRCCommonProtocol 2017-07-04 21:55:09 -07:00
James Lu
2e5fc2467f ngircd: handle CHANINFO (channel mode/topic bursts) and NJOIN (userlist bursts) 2017-07-03 14:24:57 -07:00
James Lu
7b2f93fd4c ngircd: send an UID hook in user introductions 2017-07-03 14:24:45 -07:00
James Lu
b6b1cbeb2d ngircd: send our own server negotiation info to complete the connection
Aside from the 376, none of this is /required/, but it's best to be consistent...
2017-07-03 13:35:43 -07:00
James Lu
4cdae540b5 IRCCommonProtocol: fix type of maxnicklen 2017-07-03 13:07:38 -07:00
James Lu
66af57e74f IRCCommonProtocol: handle EXCEPTS, INVEX, NICKLEN, DEAF, CALLERID in 005 2017-07-03 13:05:47 -07:00
James Lu
06d69aadf7 clientbot: fix self.connected.set() order 2017-07-03 13:04:43 -07:00
James Lu
a5e7d76341 IRCCommonProtocol: only update the same tokens once per connection 2017-07-03 12:49:38 -07:00
James Lu
ec308acfcb protocols: move 005 handling code to IRCCommonProtocol
Also enable extended server negotiation for ngIRCd, which really just passes 005 between servers (nifty!)
2017-07-03 12:45:39 -07:00
James Lu
e9d7ac39ea ngircd: remove duplicate function 2017-07-03 12:21:44 -07:00
James Lu
091c763a0f Initial ngIRCd protocol stub 2017-07-03 00:24:26 -07:00
James Lu
8bf65f3820 ircs2s_common: implicitly expand PUIDs in _send_with_prefix 2017-07-03 00:13:17 -07:00
James Lu
78034096a8 protocols: merge _expandPUID into ircs2s_common 2017-07-03 00:11:49 -07:00
James Lu
8ddcc4d9a6 Move part, quit, message, notice, topic, _send_with_prefix, _expandPUID to ircs2s_common 2017-07-03 00:05:58 -07:00
James Lu
f163d7ddde protocols: remove extraneous "Error: " from exception messages 2017-07-02 22:52:46 -07:00
James Lu
c9272c25ce IRCS2SProtocol: skip implicit message sender fetching if the first arg starts with a : 2017-07-02 22:44:57 -07:00
James Lu
640e903dd6 Move _get_SID/_get_UID to IRCCommonProtocol 2017-07-02 22:36:47 -07:00
James Lu
bbc4dec8dd NetworkCoreWithUtils: shortcut _get_SID/UID if the target already exists 2017-07-02 22:35:39 -07:00
James Lu
e866e9eb7b NetworkCore: demote "stopping connect loop" messages to DEBUG 2017-07-02 22:26:28 -07:00
James Lu
e9a6328566 protocols: remove unnecessary handle_squit overrides 2017-07-02 22:20:52 -07:00
James Lu
d4260734dc clientbot: make sure incoming server messages don't clash with a PSID/PUID 2017-07-02 22:13:57 -07:00
James Lu
1e39fb78db clientbot: rename _validateNick -> _check_puid_collision 2017-07-02 22:11:40 -07:00
James Lu
ce2852bdc0 clientbot: get rid of _get_SID in message parsing
It isn't needed because any external server messages are sent raw.
2017-07-02 22:09:22 -07:00
James Lu
990a928602 relay: re-add 'CLAIM #channel -'
This was mistakenly removed in d51c39935118223a3e7087c821af50e52ff152fd due to a merge conflict, oops...

(cherry picked from commit 62669c085dad8b6e9ea057fe2b130535af1b0a52)
2017-07-02 22:07:58 -07:00
James Lu
62669c085d relay: re-add 'CLAIM #channel -'
This was mistakenly removed in d51c39935118223a3e7087c821af50e52ff152fd due to a merge conflict, oops...
2017-07-02 22:06:50 -07:00
James Lu
51d8d3b3b8 clientbot: store external server names raw instead of using server name mangling 2017-07-02 21:59:40 -07:00
James Lu
ee5a884328 clientbot: fix imports for PyLink 2.0 2017-07-02 21:59:29 -07:00
James Lu
883f9199ec control: move rehash signal to SIGUSR1, and shutdown on SIGHUP (terminal close)
PyLink technically isn't a daemon, so it's a bit odd to have it linger around after the controlling terminal has died.
2017-07-02 21:19:04 -07:00
James Lu
f0fab0c0ad games: remove 'fml'
This is a blocking command which can potentially freeze the server given enough network interruption.
It will likely be reintroduced later on in some sort of "Websites" plugin, possibly in the contrib repository.
2017-07-02 21:18:32 -07:00
James Lu
f800c9f7c2 Merge branch 'wip/irc-explosion-2' into devel (#475)
This brings in a major refactor of the IRC/protocol stack, to start off 2.0-dev.
2017-07-02 21:16:06 -07:00
James Lu
5158497125 Bump version to 2.0-dev 2017-07-02 21:15:15 -07:00
James Lu
2c32269b7f PyLink 1.2-beta1 2017-07-02 13:19:49 -07:00
James Lu
f2b644e2bb relay: be more verbose in 'grabbing spawnlocks' messages 2017-07-02 12:36:33 -07:00
James Lu
60788e4ba5 relay_clientbot: remove dark blue from the random colours list
It's difficult to read on clients configurated to use a dark background. Reported by @MrBenC
2017-07-01 18:06:50 -07:00
James Lu
06ef421578 classes: clean up references to deprecated classes/methods 2017-06-30 21:49:29 -07:00
James Lu
54d7fe6dc5 protocols: convert IrcServer usage to Server 2017-06-30 21:45:10 -07:00
James Lu
a204d2b2db core: convert IrcUser calls to User 2017-06-30 21:44:31 -07:00
James Lu
b81a03fda9 protocols: Channel.removeuser -> Channel.remove_user 2017-06-30 21:40:50 -07:00
James Lu
7d68c03101 various: convert sortPrefixes/getPrefixModes calls to snake case 2017-06-30 21:40:05 -07:00
James Lu
6d7e2c667d DeprecatedAttributesObject: don't clobber __ variables 2017-06-30 21:38:50 -07:00
James Lu
a73300e864 classes.Channel: migrate to snake case 2017-06-30 21:34:08 -07:00
James Lu
61ed209abb coremods, plugins: migrate to snake case for protocol communication 2017-06-30 21:30:20 -07:00
James Lu
927fa9aac9 protocols: updateClient -> update_client 2017-06-30 21:29:38 -07:00
James Lu
f38b9c9a2c protocols: topicBurst -> topic_burst 2017-06-30 21:29:11 -07:00
James Lu
d0846170c4 protocols: spawnServer -> spawn_server 2017-06-30 21:27:15 -07:00
James Lu
f60dc8fa37 protocols: spawnClient -> spawn_client 2017-06-30 21:25:58 -07:00
James Lu
ea455436c1 control: fix REHASH for 2.x protocol modules 2017-06-30 00:41:29 -07:00
James Lu
0c7fb861f1 classes, relay, ircs2s_c: tweak/remove various debug statements 2017-06-29 23:19:21 -07:00
James Lu
8e9a99f90c ServiceBot: migrate to snake case 2017-06-29 23:02:34 -07:00
James Lu
10bca676fc coremods, plugins: migrate to snake case 2017-06-29 23:01:39 -07:00
James Lu
a4e321522b protocols: migrate away from camel case 2017-06-29 22:56:14 -07:00
James Lu
3913a909ef utils: remove parseModes, applyModes wrappers
These have been deprecated since 0.8-alpha2.
2017-06-29 22:43:29 -07:00
James Lu
5647229c05 CamelCaseToSnakeCase: add deprecation warnings 2017-06-29 22:41:18 -07:00
James Lu
3f240bd9e8 p10: mark check_cloak_change as private 2017-06-29 22:21:08 -07:00
James Lu
930a7e19f1 unreal: checkCloakChange -> _check_cloak_change 2017-06-29 22:20:30 -07:00
James Lu
741528b0b3 Merge handle_invite into IRCS2SProtocol (#454) 2017-06-29 22:17:46 -07:00
James Lu
2034bfcc83 IRCS2SProtocol: sort handle_* methods alphabetically 2017-06-29 22:17:46 -07:00
James Lu
7cfc63d6ed ts6: fix wrong argument count when parsing INVITE ts 2017-06-29 22:17:46 -07:00
James Lu
d01e797219 Merge handle_part into IRCS2SProtocol (#454) 2017-06-29 22:17:43 -07:00
James Lu
58a4215690 ratbox: fix support for merged Irc/proto 2017-06-29 21:55:52 -07:00
James Lu
67347935b5 ircs2s_common: add missing ProtocolError import 2017-06-29 21:55:33 -07:00
James Lu
c9c01def8c ts6_common: continue using self.irc in TS6SIDGenerator 2017-06-29 21:51:02 -07:00
James Lu
963d5e11cc Merge 'utils: add a default to DeprecatedAttributesObject so that it works as is'
Merge commit 'ed33c8d5804387245b6b012cd1aaabaca173262a' into devel
2017-06-29 18:14:15 -07:00
James Lu
85fbc9ea9d relay_clientbot: use isinstance(obj, dict) & cleanup imports (#410) 2017-06-29 18:08:41 -07:00
James Lu
8eebcb0b06 relay_clientbot: drop colour from network names by default 2017-06-29 18:07:40 -07:00
James Lu
77357b765e clientbot: rename various private functions
* capEnd -> _do_cap_end
* requestNewCaps -> _request_ircv3_caps
* saslAuth -> _try_sasl_auth
* sendAuthChunk -> _send_auth_chunk
* parseMessageTags -> parse_message_tags
2017-06-27 22:28:31 -07:00
James Lu
310f3f23b8 protocols: rename various parse* functions (no migration stub)
Renamed to camel case:
- parseArgs -> parse_args
- parsePrefixedArgs -> parse_prefixed_args

Renamed to show that we're specifically parsing ISUPPORT data:
- parseCapabilities -> parse_isupport
- parsePrefixes -> parse_isupport_prefixes
2017-06-27 17:16:46 -07:00
James Lu
6684f9bf08 utils.CC2SC: slightly reword the "missing attribute" error 2017-06-27 16:26:53 -07:00
James Lu
91fe7e0ca7 utils.CC2SC: use self.__class__ to get the name of the current subclass
This is the intended behaviour instead of showing "CamelCaseToSnakeCase" in attribute errors.
2017-06-27 16:25:40 -07:00
James Lu
56275c5a3b NetworkCore: rename removeClient -> _remove_client (no migration stub) 2017-06-27 16:21:30 -07:00
James Lu
5e7529dae4 Move some functions back into NetworkCore
Things like is_internal_client() are specific to the way we track users, so it doesn't make much sense to override these per protocol. It can *still* be done though, but there's little point...
2017-06-27 16:17:28 -07:00
James Lu
56f1c9e919 NetworkCore: fix irc.protoname definition 2017-06-27 16:15:37 -07:00
James Lu
ad2d5a5ae0 Move ts_lock definition into PyLinkNetworkCoreWithUtils 2017-06-27 16:12:45 -07:00
James Lu
928dbf80bb Move more IRC-specific attributes into IRCNetwork.init_vars() 2017-06-27 16:05:58 -07:00
James Lu
62784a63e4 IRCNetwork: error when attempting to start multiple connection threads for a network 2017-06-27 15:58:55 -07:00
James Lu
c3cdf63253 Move some IRC-specific attributes to IRCNetwork 2017-06-27 15:58:38 -07:00
James Lu
fb34392fca IRCNetwork: mark schedule_ping, process_queue as private 2017-06-27 02:53:09 -07:00
James Lu
710a276c45 IRCNetwork: rename run() -> _run_irc(), this is a private function 2017-06-27 01:44:26 -07:00
James Lu
04c18f0bd5 docs: get rid of self.irc, self.proto, irc.proto 2017-06-25 02:10:03 -07:00
James Lu
4696519bad plugins: migrate irc.proto calls to irc 2017-06-25 02:09:59 -07:00
James Lu
748c1bc158 coremods: migrate irc.proto calls to irc 2017-06-25 02:09:55 -07:00
James Lu
7814914a05 classes, protocols: convert self.irc usage to self 2017-06-25 02:09:52 -07:00
James Lu
eef7a73ce9 classes: migrate self.proto calls to self 2017-06-25 02:09:41 -07:00
James Lu
d0209f720a Rewrite network intitialization bits
- Move protocols.connect -> protocols.post_connect to fix namespace conflict
- Starting an IRC connection is now explicit (via irc.connect instead of __init__)
2017-06-25 01:12:58 -07:00
James Lu
8acf39cad6 protocols: rename _send to _send_with_prefix to avoid clashing with process_queue 2017-06-24 23:47:30 -07:00
James Lu
df18e318a8 WIP: merge IRCNetwork and Protocol classes together
Eventually, the goal is to have both of these hotswappable with inheritance, so this distinction becomes moot.
2017-06-24 23:27:24 -07:00
James Lu
f8155ff74c protocols: sed -i 's/_getSid/_get_SID/g' 2017-06-16 17:13:30 -07:00
James Lu
a60d746e3b protocols: sed -i 's/_getUid/_get_UID/g' 2017-06-16 17:13:30 -07:00
James Lu
7ca98eb965 Split IRC-specific code from classes.Protocol into a new IRCCommonProtocol (#454) 2017-06-16 17:13:26 -07:00
James Lu
45ae1dd67e Merge ts6 and p10 handle_events, handle_privmsg into ircs2s_common (#454) 2017-06-16 17:00:22 -07:00
James Lu
37d8e8ad43 Irc: break protocol-agnostic [dis]connect code into _pre/_post functions (#371) 2017-06-16 16:53:23 -07:00
James Lu
2a978c498e Rename PyLinkIRCNetwork -> IRCNetwork
The "PyLink" prefix is sort of redundant here...
2017-06-15 21:55:08 -07:00
James Lu
c4f6d626d5 Drop Irc prefix from IrcServer/User/Channel classes 2017-06-15 21:54:40 -07:00
James Lu
d98d522387 Move Irc.runline => PyLinkNetworkCore.parse_protocol_command 2017-06-15 21:45:04 -07:00
James Lu
47e36a9249 classes: break Irc into three classes: PyLinkNetworkCore, PyLinkNetworkCoreWithUtils, PyLinkIRCNetwork (aliased to Irc) 2017-06-15 21:38:52 -07:00
James Lu
8dae235b8d Merge branch 'devel' into wip/irc-explosion 2017-06-15 21:13:49 -07:00
James Lu
28cb7168b1 Merge branch 'master' into devel
Sync README and CI configuration with master.

Conflicts:
	protocols/nefarious.py
2017-06-14 07:04:15 -07:00
James Lu
7794171d62 README: update with new nightly build repositories for Debian/Ubuntu 2017-06-06 17:30:11 -07:00
James Lu
6e3e188fd6 Merge branch 'master+travis-production' 2017-06-06 17:13:41 -07:00
James Lu
f8a4f003f1 setup.py: explicitly parse README.md as markdown_github 2017-06-06 17:12:34 -07:00
James Lu
805aa52f59 travis: Move to Ubuntu 14.04; it has a newer pandoc version which better supports GitHub flavoured markdown 2017-06-06 17:12:34 -07:00
James Lu
963e8e7180 travis: rm skip_upload_docs rule
This doesn't seem to do anything useful?
2017-06-06 17:12:27 -07:00
James Lu
41c0191cf9 setup.py: update comments and my email 2017-06-06 12:54:48 -07:00
James Lu
d4fae02540 Irc: migrate functions to camel case 2017-06-02 23:17:14 -07:00
James Lu
8f82b92a6a utils: add CamelCaseToSnakeCase class, which wraps missing attributes from camel case names to snake case 2017-06-02 23:16:51 -07:00
James Lu
ed33c8d580 utils: add a default to DeprecatedAttributesObject so that it works as is 2017-06-02 17:26:54 -07:00
James Lu
2217306ca1 p10: acknowledge incoming KICKs with a PART
Per https://github.com/evilnet/nefarious2/blob/ed12d64/doc/p10.txt#L611-L616. This fixes autorejoin-on-kick not working with prefix modes because the remote verifies whether the KICK has been acknowledged properly. Closes #465.

(backported from commit 1996b86e85aff5ba0465445574f9ff7e91b18034)
2017-06-02 09:13:52 -07:00
James Lu
f97db31533 Irc: show the current encoding setting in fullVersion() 2017-06-02 08:46:55 -07:00
James Lu
f80c5df971 control: log the start and end of rehash to INFO 2017-06-02 08:46:27 -07:00
James Lu
60a0bcdc7a Rename config option log:stdout -> log:console
Closes #386.
2017-06-02 08:42:32 -07:00
James Lu
caade5a308 log: flush the log queue AFTER setting up file loggers
This makes sure that messages sent during the config phase are logged to files, not just the console.

TODO: We should actually be extending this to log to IRC too...
2017-06-02 08:39:49 -07:00
James Lu
6e8f618f80 control: don't duplicate config validation 2017-06-02 08:34:59 -07:00
James Lu
9ea9f66dd7 conf: actually pass the logger object to _log in validateConf 2017-06-02 08:34:43 -07:00
James Lu
1996b86e85 p10: acknowledge incoming KICKs with a PART
Per https://github.com/evilnet/nefarious2/blob/ed12d64/doc/p10.txt#L611-L616. This fixes autorejoin-on-kick not working with prefix modes because the remote verifies whether the KICK has been acknowledged properly. Closes #465.
2017-06-02 08:09:19 -07:00
James Lu
1ce1f7b3e5 ircs2s_common: don't clobber the case of prefixmsg prefixes on RFC1459 networks
Fixes #464.
2017-06-02 07:56:33 -07:00
James Lu
6ef3bab0fc unreal: remove handle_privmsg/handle_notice override 2017-06-02 07:56:22 -07:00
James Lu
a32d937b91 Merge branch 'wip/protocol-cleanup' into devel 2017-06-02 07:40:08 -07:00
James Lu
3a934ef5b8 Merge branch 'wip/configurable-encoding' into devel 2017-06-02 07:32:07 -07:00
James Lu
b9aee6ae85 Irc: only apply encoding settings on connect
Changing the encoding after a connection has been established is somewhat dangerous, because it's possible to corrupt channel/user state if characters in the old encoding are no longer valid.

Also, mark this option as experimental.
2017-06-02 07:31:49 -07:00
James Lu
1ff027152a Irc: remove outdated cert/keyfile comment 2017-05-28 20:09:26 -07:00
James Lu
2737b6bbfc Irc: simplify _send() code and replace unencodable characters 2017-05-27 02:21:12 -07:00
James Lu
1246edaf2c Irc: initial work on encoding support (#101) 2017-05-27 01:27:09 -07:00
James Lu
9ec3cccaee example-conf: minor tweaks to the pylink: block
- Make the "pylink:prefixes" deprecation notice more visible
2017-05-21 20:20:03 -07:00
James Lu
297087c620 example-conf: update plugins notes
- Remove obsolete note about opercmds permissions
- Reword and rewrap most other descriptions
2017-05-21 20:15:48 -07:00
James Lu
69cf21c04e Merge ts6 and p10 handle_events, handle_privmsg into ircs2s_common 2017-05-20 19:41:19 -07:00
James Lu
76ecc60675 servprotect: only track kills and saves to PyLink clients
why wasn't this done before...
2017-05-20 15:02:04 -07:00
James Lu
ead20f5be9 Irc: log full tracebacks when disconnecting due to an error 2017-05-16 16:30:03 -07:00
James Lu
89f9b46ec0 relay: demote "PM from server" warnings to debug
InspIRCd's m_chanlog.so sends these on purpose, so it's best not to warn about this "feature" endlessly...
2017-05-15 21:32:41 -07:00
James Lu
b2643a0ac8 adv-relay-conf: add a missing $ 2017-05-15 17:22:58 -07:00
James Lu
a5b3011ea4 networks: clear the 'remote command used' state and break if overriding account/reply target fails 2017-05-12 19:58:03 -07:00
James Lu
cfec70730f clientbot: reinitialize PUID generators on connect
Closes #448.
2017-05-12 19:57:24 -07:00
James Lu
fd3236ddb7 Irc: fix another CPU loop on 'disconnect' 2017-05-12 19:52:40 -07:00
James Lu
084f58b499 automode: remove extraneous +'s from mode lists
Closes #447.
2017-05-12 19:41:55 -07:00
James Lu
04f88df385 Actually use 'irc' in main() and die() as a keyword argument (per docs) 2017-05-12 19:19:52 -07:00
James Lu
998beb51b5 control: move plugin shutdown & pidfile cleanup routines to atexit
It is possible for PyLink to shutdown indirectly by disconnecting all networks. In these cases, the shutdown routines never ran at all...
2017-05-12 19:13:02 -07:00
James Lu
5b73e0a691 service_support: fix service respawn on KILL
(cherry picked from commit 7e51d3a7f5b184e3edafdd55c5f834177fe44b3a)
2017-05-12 18:51:34 -07:00
James Lu
7e51d3a7f5 service_support: fix service respawn on KILL 2017-05-12 18:45:27 -07:00
James Lu
457325024a service_support: reuse existing internal clients for service bots if one exists
Potential fix for #458.
2017-05-12 18:27:54 -07:00
James Lu
daa6593534 Irc: block when the queue is empty instead of needlessly polling it
Rework Irc.processQueue() to block when the queue is full, and abort if the item "None" is sent to it.
To make sure that the None isn't caught by a full queue or pushed back by other elements, this modifies queue.Queue's underlying deque instance directly.

Closes #459.
2017-05-12 17:55:24 -07:00
James Lu
06d49f4433 Revert "Irc: only disconnect the write portion of the socket"
This reverts commit f4babc6f285eba7f49e538815512d15c355c6af4.
2017-05-12 17:19:16 -07:00
James Lu
1bd6149ee2 Merge branch 'devel+next' into devel 2017-05-12 16:57:32 -07:00
James Lu
7daef0000b Irc: break out of processQueue properly when a network disconnects 2017-05-09 23:31:20 -07:00
James Lu
08c0082430 protocols: rename checkCollision -> check_nick_collision() (#454) 2017-05-09 20:44:48 -07:00
James Lu
8f14cb238b p10: rename checkCloakChange -> check_cloak_change (consistency) (#454) 2017-05-09 20:36:43 -07:00
James Lu
c898da7378 p10: move command tokens dict into a class variable & drop _getCommand() 2017-05-09 20:15:23 -07:00
James Lu
701f01fa4f Irc: reword error message from last commit to be more concise 2017-05-09 18:19:35 -07:00
James Lu
bac6dc36b4 Irc: log socket.send() errors with a proper traceback 2017-05-09 18:06:51 -07:00
James Lu
4ca8667669 Irc: fix 62aea23879f61d82522bfb91c0eb4fd9d3059740 (lowercase queue) 2017-05-07 17:34:04 -07:00
James Lu
a3df47e88e ServiceBot: fix ce77f2cbd42037935365e0e087a4bcdbbcb7bf74 2017-05-07 17:33:15 -07:00
James Lu
62aea23879 Irc: fix throttle_time not actually blocking for the defined amount of time
Passing the timeout to queue.Queue.get is invalid because it'll only block if there ISN'T any text to send.
2017-05-07 17:31:31 -07:00
James Lu
ce77f2cbd4 ServiceBot: minor fix of logging syntax 2017-05-07 17:11:51 -07:00
James Lu
24b5fd92ef relay: don't error if the pylink service is gone (e.g. during shutdown) 2017-05-07 13:58:11 -07:00
James Lu
b83aba0b13 inspircd: stop ENDBURST timers when irc.aborted gets set 2017-05-07 13:56:55 -07:00
James Lu
15ed251ed7 Irc: refuse to queue send data if aborted is set 2017-05-07 13:46:25 -07:00
James Lu
de67fe0d37 Irc: log socket shutdown errors to debug 2017-05-07 13:46:15 -07:00
James Lu
f4babc6f28 Irc: only disconnect the write portion of the socket
Per https://docs.python.org/3/howto/sockets.html#disconnecting
2017-05-07 13:46:06 -07:00
James Lu
59dd8d3bf8 control: print remaining threads on forced shutdowns as well 2017-05-07 13:39:46 -07:00
James Lu
5c7752a203 relay: stop execution if spawn lock acquire fails
Also, make the lock timeout a consistent, global variable.

(partial merge of commit e24bc54bbcefc40bf73a197de2dc24f7cd42cf79)
2017-05-04 21:18:02 -07:00
James Lu
fd15600d80 docs: split exttargets info into a new page 2017-05-04 20:05:57 -07:00
James Lu
5d629f7331 matchHost: extend negation via "!" to regular hostmasks as well as exttargets 2017-05-04 19:04:03 -07:00
James Lu
d51c399351 Revert "relay: add locks in db read/writes (thread safety)"
Unfortunately, this made relay prone to freezing the entire PyLink server.

This reverts commit 2b4943a7801149f27d48cd798d7c5fe31cdcace5.
2017-05-04 18:53:07 -07:00
James Lu
1358fedca6 exttargets: add $and exttarget (#334) 2017-05-04 18:00:32 -07:00
James Lu
dbeacf9249 exttargets: add a $network target (#334) 2017-05-04 17:16:50 -07:00
James Lu
bab6a71a2e example-conf: fix reversed option description for password encryption
(cherry picked from commit e69e1f5f038deca72f2f1108818d231c4664792f)
2017-04-29 00:09:01 -07:00
James Lu
5cea0fa73a commands: remove extraneous private=True from showchan
(cherry picked from commit 79e2d20d9d113398deedba1297c07a9e65711193)
2017-04-29 00:09:01 -07:00
James Lu
e69e1f5f03 example-conf: fix reversed option description for password encryption 2017-04-29 00:08:07 -07:00
James Lu
79e2d20d9d commands: remove extraneous private=True from showchan 2017-04-27 07:28:26 -07:00
James Lu
4205694a65 PyLink 1.2-alpha1 2017-04-20 13:25:18 -07:00
James Lu
94de51ad53 README: rewrap steps in "Installing from source" 2017-04-16 01:25:50 -07:00
James Lu
f6d97374c5 README: Update Charybdis link to GitHub
http://charybdis.io/ has been dead (NXDOMAIN) for a while now...
2017-04-16 01:22:28 -07:00
James Lu
18c13c735e Merge branch 'master' into devel
Conflicts:
	VERSION
2017-04-10 15:14:48 -07:00
James Lu
2feb93aaf0 clientbot: generate PUIDs/PSIDs with the nick or server name as prefix 2017-04-09 15:32:13 -07:00
James Lu
0e6d33a668 networks.remote: suppress errors if restoring remoteirc.pseudoclient.account
This should rarely happen, but can be purposely caused by causing the remote network to disconnect through 'remote'.
2017-04-09 15:21:19 -07:00
James Lu
3f6501fa88 clientbot: make sure incoming nicks don't clash with a PUID/PSID
This should really never happen, but it might break quite a few things if it does.
2017-04-09 15:12:52 -07:00
James Lu
cc9ffd47b1 clientbot: fix misleading comment 2017-04-09 15:01:49 -07:00
James Lu
709b1d2ead commands: show TS on networks without has-ts as well 2017-04-09 14:54:16 -07:00
James Lu
bf1d7812e2 clientbot: track channel modes and TS on join
Closes #345.
2017-04-09 14:49:19 -07:00
James Lu
6d96dd21ac updateTS: remove usage of mutable as function default argument
This may subtly break things: https://docs.quantifiedcode.com/python-anti-patterns/correctness/mutable_default_value_as_argument.html
2017-04-09 14:45:12 -07:00
James Lu
539ad668cf PyLink 1.1.2 2017-04-06 15:33:28 -07:00
James Lu
e2ae091723 .travis.yml: bring PyPI deployment to production 2017-04-06 15:30:46 -07:00
James Lu
0eda9a9d95 Merge branch 'master+sasl-backport'
This merges in backported SASL fixes from 1.2-dev.
2017-04-05 23:20:15 -07:00
James Lu
22ceb3f699 clientbot: make SASL timeout configurable & raise default to 15 secs
(cherry picked from commit 9d50a4363be5ed2ca17b9bdf563017220ffba1bd)
2017-04-05 23:19:08 -07:00
James Lu
84448e9803 clientbot: time out CAP/SASL after 5 seconds
Closes #424.

(cherry picked from commit 47f0b7626f8d17b99fa4dd8cfd93a89f81bb7753)
2017-04-05 23:19:08 -07:00
James Lu
b14ea4f051 clientbot: send CAP LS before NICK/USER so that it consistently gets a response before connect
Previously, SASL was failing on networks like freenode, as the connection completed before a CAP response was received.

(cherry picked from commit 9420f21680058e50a8134eed7a050bb4abb9ae73)
2017-04-05 23:19:08 -07:00
James Lu
9d50a4363b clientbot: make SASL timeout configurable & raise default to 15 secs 2017-04-05 23:08:17 -07:00
James Lu
69c25b5954 channel-modes: document UnrealIRCd +D and +V 2017-04-02 14:04:44 -07:00
James Lu
2e5cccc381 relay: fix incrementing changes to modedelta 2017-04-01 12:50:32 -07:00
James Lu
13be8ff6d0 relay: apply modedelta rules on SJOIN as well 2017-04-01 12:50:32 -07:00
James Lu
4312983ca5 relay: initial modedelta implementation 2017-04-01 12:50:32 -07:00
James Lu
4daa94c014 Merge branch 'master' into devel 2017-04-01 12:42:00 -07:00
James Lu
df4acbf5d5 unreal: expand PUIDs in outgoing channel modes
This fixes things like relay modes / automode targets not working.
2017-04-01 12:39:38 -07:00
James Lu
8465edd5af ts6_common: fix outbound kicks to PUIDS not updating the state
This was previously trying to update the state based on the user's nick, but we use PUIDs internally. In other words, make sure that we don't replace the internal target when sending the outgoing text...
2017-04-01 12:26:08 -07:00
James Lu
fccec3a195 unreal: fix userlist parsing breaking with Unreal 3.2 nicks starting with a symbol 2017-04-01 12:10:19 -07:00
James Lu
d94bd3f28f rm pylink-contribdl, superseded by plugin loading from any folder 2017-03-31 18:57:07 -07:00
James Lu
348572bcb6 Irc: rewrite sendq to use queue.Queue, and add an upper bound (maxsendq)
Closes #430. Closes #442.
2017-03-31 17:41:56 -07:00
James Lu
ad4fe1924b networks: show an error instead of silently failing if the command is empty 2017-03-31 17:13:04 -07:00
James Lu
0749a42ef6 networks.remote: add recursion checks to prevent bad queries from crashing the server
For example, 'remote net1 remote net2 echo hi' was problematic if the source network was 'net1'.

It's a good thing this command is restricted by default...
2017-03-31 16:40:26 -07:00
James Lu
9d9b01839c Split Irc.reply() into _reply() to make 'networks.remote' actually thread-safe
Previously, the Irc.reply_lock check was in the reply() function itself: replacing it with another function checking for the same lock would delay execution,
but then run the wrong reply() code if another module used irc.reply() while 'remote' was executing.
2017-03-31 16:25:28 -07:00
James Lu
40fa4f71bc corecommands: add a 'clearqueue' command to force clear queue muckups (#441) 2017-03-28 22:39:11 -07:00
James Lu
ae6c68018b core: half the default throttle time (from 0.01 to 0.005) 2017-03-28 22:38:54 -07:00
James Lu
029bb38af8 protocols: skip queuing when responding to PING 2017-03-28 22:30:33 -07:00
James Lu
5d10ee39be ServiceBot: make displaying unknown command errors optional
Closes #441.
2017-03-28 22:18:51 -07:00
James Lu
4cd71d12ef fantasy: don't trigger when the fantasy prefix is followed by a space
This prevents false positives such as "& that", "@ person", etc.
2017-03-28 22:00:41 -07:00
James Lu
d425cb9d47 relay: fix case sensitivity in channel TS check 2017-03-28 08:08:39 -07:00
James Lu
5c8ddef60b Merge branch 'master' into devel
Conflicts:
	classes.py
2017-03-27 16:13:58 -07:00
James Lu
1d6da68963 Add Travis CI configuration
Squashed commit of the following:

commit b03e5f82405343d54015a40c1655d54f0fc5ce90
Author: James Lu <GLolol@overdrivenetworks.com>
Date:   Mon Mar 27 16:04:51 2017 -0700

    .travis: install pandoc, disable docs and email

commit 25cd4ad2a07686f7a196b151070efe3fd31a94e3
Author: James Lu <GLolol@overdrivenetworks.com>
Date:   Mon Mar 27 15:55:36 2017 -0700

    wrap python as a subprocess

commit 749177ac7b13921f8a04b023b54c1716ff0ce0b0
Author: James Lu <GLolol@overdrivenetworks.com>
Date:   Mon Mar 27 15:53:37 2017 -0700

    oops, do proper matching

commit 98f90c15fd5be91b70ecf70bdff3a005d1fedc94
Author: James Lu <GLolol@overdrivenetworks.com>
Date:   Mon Mar 27 15:52:15 2017 -0700

    i am not dealing with this shell.

commit 8b3dcb6c59617e7ca85363bf485dc295ff22a012
Author: James Lu <GLolol@overdrivenetworks.com>
Date:   Mon Mar 27 15:37:28 2017 -0700

    More .travis.yml tweaks

commit 49346bef1b7794eefd251d27e846a184c0569dad
Author: James Lu <GLolol@overdrivenetworks.com>
Date:   Mon Mar 27 15:28:30 2017 -0700

    .travis.yml: add missing directory to compileall

commit 7eb1d464481c958ef007b51234045a13f1a8c06e
Author: James Lu <GLolol@overdrivenetworks.com>
Date:   Mon Mar 27 15:23:21 2017 -0700

    Make travis do syntax verification on all modules

commit 0331356e91e22cd85859a4ebbaf1973f21db1650
Author: James Lu <GLolol@overdrivenetworks.com>
Date:   Mon Mar 27 15:19:24 2017 -0700

    Revert ".travis.yml: remove duplicate python version restriction"

    This reverts commit 51c74cedff78aa204813743e1a738e3aa63fe3c6.

commit 51c74cedff78aa204813743e1a738e3aa63fe3c6
Author: James Lu <GLolol@overdrivenetworks.com>
Date:   Wed Feb 22 16:23:14 2017 -0800

    .travis.yml: remove duplicate python version restriction

commit 04087af09a29e2f825545cdc36be55f86a7c7d10
Author: James Lu <GLolol@overdrivenetworks.com>
Date:   Wed Feb 22 16:07:51 2017 -0800

    Test travis-ci deployment
2017-03-27 16:13:05 -07:00
James Lu
657ed82958 launcher: catch all errors when loading plugins, not just ImportError / OSError 2017-03-27 15:09:27 -07:00
James Lu
ff0eda1fba networks: flip **kwargs position to fix Python 3.4 support
Reported by @koaxirc.
2017-03-27 15:00:01 -07:00
James Lu
d70ca9fa3b Irc: simplify runloop error catching, adding RuntimeError and SystemExit (closes #438)
socket.error is aliased to OSError since Python 3.3, and ConnectionError is actually a subclass of OSError.
So, it makes more sense to just catch the more generic type here.

Also, make ProtocolError derive from RuntimeError instead of Exception.

(cherry picked from commit 397df48efde42d702f49529801f55b6912e9caed)
2017-03-26 14:46:51 -07:00
James Lu
55873416a1 control: raise KeyboardInterrupt to speed up forced shutdowns? 2017-03-26 14:44:04 -07:00
James Lu
d6243d9f89 protocols: raise NotImplementedError with proper reasons 2017-03-26 14:32:54 -07:00
James Lu
938a1fb9d7 Irc: reorder comments from last commit 2017-03-26 14:25:02 -07:00
James Lu
397df48efd Irc: simplify runloop error catching, adding RuntimeError and SystemExit (closes #438)
socket.error is aliased to OSError since Python 3.3, and ConnectionError is actually a subclass of OSError.
So, it makes more sense to just catch the more generic type here.

Also, make ProtocolError derive from RuntimeError instead of Exception.
2017-03-26 14:17:37 -07:00
James Lu
ce0c84266e fantasy: make responding to nick a per-service configuration option (#343)
This also renames the "respondtonick" option to "respond_to_nick", deprecating the former name.
2017-03-26 14:03:31 -07:00
James Lu
f497044777 corecommands: use utils.PLUGIN_PREFIX in 'unload' 2017-03-25 14:08:37 -07:00
James Lu
36f9bef450 docs/t: refresh protocol-modules graphic 2017-03-25 13:47:16 -07:00
James Lu
7ff5f47064 protocols: tweak mode type definitions 2017-03-25 13:47:16 -07:00
James Lu
8fec778b9b Merge branch 'wip/generic-p10-support' into devel
Closes #330. Really fixes #329.
2017-03-25 13:47:16 -07:00
James Lu
7acfd80387 Update named mode definitions 2017-03-25 13:47:16 -07:00
James Lu
45729d48cc p10: mode definition tweaks
- rename cmode +d to "had_delayjoin" (from "had_delayjoins")
- add cmode +R for ircu
-
2017-03-25 13:47:16 -07:00
James Lu
1994d1171c p10: add support for ircu proper 2017-03-25 13:47:16 -07:00
James Lu
7f923ba047 README: update supported IRCds notes 2017-03-25 13:47:16 -07:00
James Lu
e27095dd67 p10: set mode +x on targets when changing vHost 2017-03-25 13:47:15 -07:00
James Lu
6a32ae94fc p10: implement FAKE and SETHOST IRCd checks 2017-03-25 13:47:15 -07:00
James Lu
235b296a69 Rename protocols/nefarious.py to protocols/p10.py 2017-03-25 13:47:15 -07:00
James Lu
fa6120a563 nefarious: add a snircd mode set, fix nef2 umode +Dd definitions 2017-03-25 13:47:15 -07:00
James Lu
205053aebe example-conf: account-based cloaking is not nefarious specific 2017-03-25 13:47:15 -07:00
James Lu
bb63d859c9 example-conf: refresh P10 config arguments
The new p10_ircd option shown here isn't implemented yet.
2017-03-25 13:47:15 -07:00
James Lu
6154a7fb09 nefarious: mark cmode +A and +U as type B (#330)
This is consistent with snircd as of 58af1fc4d0
2017-03-25 13:47:15 -07:00
James Lu
e3c0bf6a1b Irc: break out of processQueue as soon as self.aborted is set 2017-03-24 18:40:02 -07:00
James Lu
af3b350498 networks.remote: fix "unknown service" error format 2017-03-24 01:10:56 -07:00
James Lu
6c4e042307 networks.remote: break if the target network is the same as the source (#437)
_remote_reply() otherwise gets sent into a loop when remoteirc and irc is the same here.
2017-03-24 01:08:01 -07:00
James Lu
490f21ff9f networks.remote: override remoteirc.reply() to send replies back to the caller
Closes #437.
2017-03-24 00:59:48 -07:00
James Lu
99d3780773 Irc: add locking for reply() calls (#437) 2017-03-24 00:57:21 -07:00
James Lu
0f472c8959 networks.remote: add an optional --service option to call commands for other services. 2017-03-24 00:25:19 -07:00
James Lu
9e0e47064a hybrid, ratbox: re-disable slash-in-hosts 2017-03-24 00:24:06 -07:00
James Lu
c894beed2e Merge branch 'wip/protocol-caps' into devel 2017-03-23 23:54:46 -07:00
James Lu
90d3ac3cf6 relay: skip message prefixing when forwarding a message for a service client (#403) 2017-03-23 23:53:48 -07:00
James Lu
feb9cce4ee core: Drop fake ServiceBot client stubs on Clientbot (#403) 2017-03-23 23:53:43 -07:00
James Lu
f188b29911 plugins: migrate to server capabilities 2017-03-23 23:37:24 -07:00
James Lu
ad00fdfa53 relay: migrate normalizeHost, normalizeNick to protocol capabilities 2017-03-23 23:12:59 -07:00
James Lu
21670a5d51 relay: migrate most protocol checks to protocol capabilities
Some things I left out include modesync and op status checking on LINK, since these are pretty specific to Clientbot IRC.
2017-03-23 22:52:11 -07:00
James Lu
bde27b0dde nefarious: define protocol_caps in the right class... 2017-03-23 22:46:12 -07:00
James Lu
ff6d961922 Protocol: add missing self in hasCap() 2017-03-23 22:41:12 -07:00
James Lu
c19ea74fb4 ServiceBot: ignore attempts to call empty commands 2017-03-20 22:22:43 -07:00
James Lu
efe468b0d2 protocols: declare slash-in-nicks, slash-in-hosts, underscore-in-hosts as necessary (#337) 2017-03-15 23:58:51 -07:00
James Lu
936535786e clientbot: declare clear-channels-on-leave capability 2017-03-15 23:47:32 -07:00
James Lu
f512ae1b33 Protocol: add a hasCap() wrapper function (#337) 2017-03-15 23:46:13 -07:00
James Lu
2ca0cf05a0 Start work on protocol capabilities (#337) 2017-03-15 23:32:47 -07:00
James Lu
61cf48c37c pmodule-spec: various fixes
- Corrected the location of the `self.irc.cmodes/umodes/prefixmodes` attributes
- Mention `self.conf_keys` as a special variable for completeness
2017-03-15 23:11:44 -07:00
James Lu
a8c1a46e3d core: queue messages for logging when 'log' isn't available during init
Closes #428.
2017-03-15 23:00:26 -07:00
James Lu
7892e37bfa Documentation tweaks
- Add advanced-service-config to TOC
- Move "matching SERVICE name" note to docs/t/services-api.md
- Various wording / grammar tweaks
2017-03-13 13:50:16 -07:00
James Lu
9ccd11b6d5 Merge pull request #433 from IotaSpencer/devel
docs: Add services config guide and mention matching config<->register

Closes #427.
2017-03-13 13:42:50 -07:00
James Lu
4910355711 Merge branch 'wip/global' into devel (#434)
Closes #398.
2017-03-13 13:41:10 -07:00
James Lu
787b254840 permissions-reference: document global.global permission 2017-03-13 13:40:09 -07:00
James Lu
3ffbbe60ff global: configurable output format 2017-03-13 13:39:30 -07:00
James Lu
cfe72e2cd0 global: set loopback=False on messages to prevent duplicating them via relay 2017-03-13 13:29:04 -07:00
James Lu
0a57c084bb global: only send to connected networks 2017-03-13 13:23:22 -07:00
James Lu
a3dff204d3 global: remove extraneous metadata
Core plugins do not need to track authors and version, because git does that for us.
2017-03-13 13:21:11 -07:00
Ken Spencer
7e6ad089c0 plugins: add global notice plugin 2017-03-13 12:30:06 -04:00
Ken Spencer
9a3ef0122b docs: Add services config guide and mention matching config<->register 2017-03-13 11:35:32 -04:00
James Lu
a6e38e7e20 pylink-opers: minor wording tweaks 2017-03-12 22:03:28 -07:00
James Lu
2e0c7db4e3 using-ircparser.md: minor tweaks and reordering
Thanks again to @IotaSpencer for writing this article :)
2017-03-12 20:45:42 -07:00
James Lu
75ea743b4a docs/t: link to using-ircparser.md in contents 2017-03-12 20:38:58 -07:00
James Lu
c8f945d40f writing-plugins.md: mention that IRCParser is a 1.2+ feature 2017-03-12 20:37:53 -07:00
James Lu
8c4a13fdf0 Merge pull request #431 from IotaSpencer/devel
Document utils.IRCParser() (closes #420)
2017-03-12 20:35:19 -07:00
Ken Spencer
d6d330bec6 Add docs/technical/using-ircparser.md guide. 2017-03-12 23:08:56 -04:00
Ken Spencer
ed7a117247 doc/technical/writing-plugins.md: mention and link to using-ircparser.md 2017-03-12 21:55:22 -04:00
James Lu
afc5cc26fa Irc: more compact __repr__ for IrcChannel/IrcServer/IrcUser 2017-03-11 22:49:48 -08:00
James Lu
716ac681b6 exec: add 'pieval' and 'peval' to evaluate expressions pretty-printed 2017-03-11 22:47:30 -08:00
James Lu
c67c0aa2e6 rehash: fix resetting the wrong autoconnect multiplier variable 2017-03-11 01:05:07 -08:00
James Lu
2028cab04c core: Grow autoconnect delays by a configurable factor whenever connections fail
Closes #348.
2017-03-11 00:21:30 -08:00
James Lu
9e50c5e69a Irc: try to make breaking out of autoconnect loops faster 2017-03-10 23:57:37 -08:00
James Lu
0526e96dc5 control: remove an extraneous, obsolete comment 2017-03-10 23:57:37 -08:00
James Lu
bf42109d81 Split fantasy prefix definitions into service-specific blocks
Closes #426.

This makes the pylink::prefix (aka bot::prefix) option only affect the main PyLink bot, and deprecates the pylink::prefixes::<...> options.
As the bot: config block is no longer checked, this commit depends on commit 45ed5b962e215763f7659631d1b98a5031dbab92 (ref #343) to alias it to conf::pylink.
2017-03-10 23:47:07 -08:00
James Lu
fb626c8a97 services_support: remove workarounds for the PyLink service nick & ident being in conf::bot (#343) 2017-03-10 23:47:07 -08:00
James Lu
dc298b3182 conf: join conf::bot and conf::pylink so that they mean the same thing (#343) 2017-03-10 23:47:07 -08:00
James Lu
3096c54bb8 Irc: return in msg() if the main client is missing and no explicit source is set 2017-03-10 23:47:07 -08:00
James Lu
b09565a723 bots, opercmds: add a "Done" reply to most commands as they finish 2017-03-09 20:50:14 -08:00
James Lu
ebd5b77576 relay: normalize channel case in 'link'
(regression from commit 93c9b6289c50ebc89043c866d06eb0d360ea6102)

Reported by @koaxirc.
2017-03-09 20:42:38 -08:00
James Lu
b23a887edd login: remove all__vary_rounds setting (deprecated in Passlib 1.7) 2017-03-08 22:58:17 -08:00
James Lu
f6d9765f87 core: implement module loading from user-defined directories
Closes #350.
2017-03-08 22:31:57 -08:00
James Lu
805a0502d2 utils: add an alias (utils.IRCParser.REMAINDER) to argparse.REMAINDER
Reported by @IotaSpencer.
2017-03-08 10:02:46 -08:00
James Lu
225b0ac8b2 ServiceBot: catch InvalidArgumentsError for prettier error display on IRC 2017-03-06 16:30:43 -08:00
James Lu
ca93d1ad70 IRCParser: show an error when using "command --help" instead of silently outputting help text in the console 2017-03-06 16:30:43 -08:00
James Lu
76d74ab9bb SECURITY: prevent DoS when calling --help on commands using IRCParser
argparse's default behaviour is to exit after displaying --help and --version information. However, doing so freezes the current IRC listener and essentially allows for DoS via IRC...
This bug does not affect any released (stable) version of PyLInk - only commits after 93c9b6289c50ebc89043c866d06eb0d360ea6102
2017-03-06 16:11:04 -08:00
James Lu
2d03ceebc8 Merge branch 'wip/attr-deprecations' into devel
Closes #273.
2017-03-05 00:15:03 -08:00
James Lu
9a9e0b2c20 Irc: deprecate the botdata field as well (#273) 2017-03-05 00:14:43 -08:00
James Lu
e8bf1d08bc handlers, fantasy: migrate away from irc.botdata (#273) 2017-03-05 00:10:33 -08:00
James Lu
d318fbac77 protocols: migrate away from irc.botdata (#273) 2017-03-05 00:09:01 -08:00
James Lu
b4f70bdece Irc: remove duplicate botdata assignment 2017-03-05 00:06:49 -08:00
James Lu
4284853a4a Irc: remove internal use of 'conf' and 'botdata' (#273) 2017-03-05 00:06:44 -08:00
James Lu
7f070448b7 utils, Irc: add abstraction to warn on deprecated attribute usage (#273) 2017-03-05 00:00:26 -08:00
James Lu
47f0b7626f clientbot: time out CAP/SASL after 5 seconds
Closes #424.
2017-03-04 23:54:16 -08:00
James Lu
733d7d7c87 exec: add ieval command using the isolated local scope 2017-03-04 22:05:03 -08:00
James Lu
42ba1775d7 exec: add irc, source, and args to isolated locals scopes
This allows basic things like irc.reply() to work in 'iexec'.
2017-03-04 21:59:42 -08:00
James Lu
9420f21680 clientbot: send CAP LS before NICK/USER so that it consistently gets a response before connect
Previously, SASL was failing on networks like freenode, as the connection completed before a CAP response was received.
2017-03-03 15:39:28 -08:00
James Lu
3281e1e8c2 pylink-contribdl: normalize exit codes 2017-02-27 07:28:37 -08:00
James Lu
8a773dea4e core: normalize exit codes 2017-02-27 07:26:29 -08:00
James Lu
7a1a4d9161 example-conf: document service-specific autojoin channels (from #423) 2017-02-26 18:16:15 -08:00
James Lu
21c0c617c1 Merge branch 'server-service-channels' of https://github.com/IotaSpencer/PyLink into devel
Closes #423.
2017-02-26 18:12:34 -08:00
James Lu
ae56ed6a32 services_support: fix service-specific key name 2017-02-26 18:11:20 -08:00
Ken Spencer
b3ec8a6790 service_support: allow server:service_channels for per service autojoin 2017-02-26 21:05:33 -05:00
James Lu
5fe277f90d Irc: mention CIDR matching (#411) and casemappings in matchHost() desc. 2017-02-25 22:26:37 -08:00
James Lu
4df8567fa6 Irc: move PYLINK_DISCONNECT firing and connected.clear() into disconnect() (#421)
This may prevent extra irc.connected.clear() calls from messing with the was_connected state.
2017-02-25 22:06:43 -08:00
James Lu
6fcb129ad6 hooks-reference: document the 'was_successful' key in PYLINK_DISCONNECT 2017-02-24 22:27:51 -08:00
James Lu
7c0cb92696 Irc: hack in CIDR support in matchHost() (#411) 2017-02-24 22:27:26 -08:00
James Lu
04fa0520a6 Irc: make was_successful check the last Irc.connected state 2017-02-24 22:09:41 -08:00
James Lu
cd65da75c6 relay: only announce disconnects if the last connection was successful 2017-02-24 21:42:58 -08:00
James Lu
b30d696e3a Irc: add "was_successful" data key to PYLINK_DISCONNECT
This stores whether the network was actually connected before this disconnect message fired (i.e. the disconnect wasn't caused by a configuration error, etc.)
2017-02-24 21:42:44 -08:00
James Lu
cd3d795296 relay: implement optional network disconnect announcements
Closes #421.
2017-02-24 21:15:40 -08:00
James Lu
0ebb52e64f conf: simplify newlogins checks & allow missing permissions blocks if an old login is also present 2017-02-24 21:07:28 -08:00
James Lu
c03f2d772c relay: allow dropping messages from user-less clients
This adds two new options, both defaulting to True:
- relay:accept_weird_senders (global)
- servers:<name>:relay_weird_senders (per-server)

Closes #404.
2017-02-24 19:16:01 -08:00
James Lu
3d9f69dba7 Irc: deprecate checkAuthenticated() 2017-02-24 18:42:58 -08:00
James Lu
50ff330734 Merge branch 'master' into devel 2017-02-24 18:31:45 -08:00
James Lu
e07974f803 utils: remove reference to checkAuthenticated() in NotAuthorizedError 2017-02-24 18:31:33 -08:00
James Lu
a7280d2943 docs/t: more notes regarding the permissions API 2017-02-24 18:31:26 -08:00
James Lu
027c35b75a PyLink 1.1.1 2017-02-24 17:56:43 -08:00
James Lu
6dd08e7dcb corecommands: remove extraneous irc.checkAuthenticated() call
(cherry picked from commit fe3fa2872db4ef3692ce36416e75ff6e7ab8b665)
2017-02-24 17:50:20 -08:00
James Lu
fe3fa2872d corecommands: remove extraneous irc.checkAuthenticated() call 2017-02-24 17:49:54 -08:00
James Lu
21cbcb8cf6 Irc: fix / simplify defaults in msg(), reply, error() 2017-02-24 16:28:23 -08:00
James Lu
46b18512cf relay: less ambiguous error if a relay channel doesn't exist on the caller network
(cherry picked from commit 0b0efbaf9f49757c58aa9ec6a060f966e15e7677)
2017-02-22 15:42:52 -08:00
James Lu
f432f6f082 relay: don't allow linking to channels when the home network is down
This check can be overridden via --force, and should stop unreliable TS checks from appearing instead

Closes #419.
2017-02-21 21:58:32 -08:00
James Lu
93c9b6289c relay: switch to IRCParser in 'link' and add a --force option to skip TS checks
Closes #416.
2017-02-21 21:52:22 -08:00
James Lu
bf702575be servprotect: fix a syntax error 2017-02-21 21:49:41 -08:00
James Lu
0125c544ee utils: add an IRCParser class based off argparse, modified from @IotaSpencer's code
Closes #6.
2017-02-21 21:45:43 -08:00
James Lu
02faa3fcb6 relay: more verbosity in TS-related link errors 2017-02-21 21:14:03 -08:00
James Lu
84d62fc540 example-conf: update comment to match the last commit 2017-02-21 21:06:55 -08:00
Ken Spencer
b92ee03525 servprotect: match key (example-conf.yml) to grabbed key (servprotect.py) (#418) 2017-02-21 18:14:48 -08:00
James Lu
2e92f65782 example-conf: mention that servprotect::max_age needs a plugin reload to update 2017-02-21 17:07:32 -08:00
James Lu
0706b6cf78 Style/spacing fixes for last commit 2017-02-21 17:04:59 -08:00
Ken Spencer
a8fe353ba4 servprotect: make length and age configurable (#417)
Fixes #395
2017-02-21 17:02:26 -08:00
Ken Spencer
b3075d3414 conf: change asserts to validations (#414) 2017-02-21 10:10:54 -08:00
James Lu
f83a81242a conf: reuse already-fetched newlogins value 2017-02-20 19:31:31 -08:00
Ken Spencer
07ac649763 conf: check for permissions block, per my own experience (#413) 2017-02-20 19:27:15 -08:00
James Lu
4577bde05c example-conf: make the "permissions:" block migration note more prominent 2017-02-19 21:40:00 -08:00
James Lu
0c88602d1f fantasy: check for nick prefix case insensitively 2017-02-18 21:21:32 -08:00
James Lu
12bb59d257 Irc: more parseArgs tweaks
- Make parsePrefixedArgs() a class method
- Split the input if parseArgs() is given a raw string instead of a list
2017-02-18 19:58:24 -08:00
James Lu
03fc16dd5a Irc: rewrite parseArgs to be more efficient 2017-02-18 19:47:36 -08:00
James Lu
01dd209647 inspircd: major->important 2017-02-18 14:45:23 -08:00
James Lu
cda5d15e31 inspircd: work around OPERTYPE changes in InspIRCd 3.x 2017-02-18 14:28:28 -08:00
James Lu
a9d2a2c4bc relay, handlers: rewrite oper WHOIS replies to show the target's home network 2017-02-18 13:51:45 -08:00
James Lu
3c98ef172e relay: use the new Irc.getFullNetworkName() where applicable 2017-02-18 13:33:35 -08:00
James Lu
b3161d6d5d Irc: add a getFullNetworkName() function 2017-02-18 13:32:48 -08:00
James Lu
75b5be5baf ServiceBot: implement global and per-service spawn_service(s) options (#403) 2017-02-18 12:54:26 -08:00
James Lu
a776aab897 utils: ignore missing services in unregisterService instead of raising an error
This is a prerequisite for the next commit (service spawn toggle options). (#403)
2017-02-18 12:54:08 -08:00
James Lu
a0ed43bf64 example-conf: describe how some options are common to all service bots 2017-02-18 12:42:36 -08:00
James Lu
a3f122fee4 control: try a more stable force-shutdown routine 2017-02-18 12:19:53 -08:00
James Lu
050721af5b example-conf: describe more clearly the pylink_nick/ident options in the clientbot block 2017-02-18 12:09:00 -08:00
James Lu
223dd3bf7b nefarious: fix a typo causing crash on user mode change
(cherry picked from commit 3e4a980ea64652239aea0eab4aa39ee35a9e10cb)
2017-02-17 22:28:11 -08:00
James Lu
8424870ec3 clientbot: abort when receiving a QUIT from uplink (#405) 2017-02-17 22:27:38 -08:00
James Lu
3e4a980ea6 nefarious: fix a typo causing crash on user mode change 2017-02-17 22:27:38 -08:00
James Lu
75158c47e2 clientbot: block PRIVMSG/NOTICE from being routed the wrong way (#405) 2017-02-17 22:27:38 -08:00
James Lu
f1fddefeac protocols: catch S2S messages if they're being routed the wrong way (#405) 2017-02-17 22:27:38 -08:00
James Lu
2f968aca80 Irc: allow defaulting to private command replies (Closes #409)
Squashed commit of the following:

commit c168500235b65f833b1d7fe49ebde674159683ee
Author: James Lu <GLolol@overdrivenetworks.com>
Date:   Thu Feb 16 17:33:36 2017 -0800

    ServiceBot: default notice and private to None

    This is so that it respects the changes from the last commit.

commit f685f3ef522f7f0ee356082c3c1b8b5a4e34b211
Author: James Lu <GLolol@overdrivenetworks.com>
Date:   Thu Feb 16 15:10:33 2017 -0800

    Irc: implement a prefer_private_replies option (#409)
2017-02-16 17:41:07 -08:00
James Lu
ad873cfd7b ServiceBot: be more flexible in help formatting
* Fix text after indented docstring line breaks not showing (thanks @IotaSpencer for noticing this)
* Update formatting so that multiple consecutive newlines in a docstring are shown:
    - 2 newlines => 1 displayed new line
    - 3 newlines => 2 displayed new lines, and so on...
2017-02-15 17:06:16 -08:00
James Lu
902b246f96 bots: allow specifying channel prefixes (e.g. @+) in 'join'
This functionality should really be merged with what ServiceBot does, but whatever...
2017-02-13 17:01:09 -08:00
James Lu
f70e771000 unreal: ignore userpairs with only a prefix and no user
How is this even possible?!

Reported by @koaxirc.

(cherry picked from commit 9fac7cb1f321e154f3b99038b5bc2c66fef15e31)
2017-02-06 18:00:39 -08:00
James Lu
9fac7cb1f3 unreal: ignore userpairs with only a prefix and no user
How is this even possible?!

Reported by @koaxirc.
2017-02-05 22:23:20 -08:00
James Lu
0b0efbaf9f relay: less ambiguous error if a relay channel doesn't exist on the caller network 2017-02-05 21:47:11 -08:00
James Lu
6e94375ed9 relay: clarify/revise help text for 'claim' 2017-02-05 21:43:53 -08:00
James Lu
f7768a00a0 inspircd: work around extraneous letters sometimes sent in FJOIN TS
Anope 1.8 potentially sends a trailing 'd' after the timestamp, which causes int() to error. This is technically valid in InspIRCd S2S because atoi() ignores non-digit characters, but it's strange behaviour either way:
<- :3AX FJOIN #monitor 1485462109d + :,3AXAAAAAK

Thansk to @koaxirc for reporting.

(cherry picked from commit 663e657bf5a82d9c2d1a1f3e9a02709118dc796b)
2017-02-05 21:13:23 -08:00
James Lu
663e657bf5 inspircd: work around extraneous letters sometimes sent in FJOIN TS
Anope 1.8 potentially sends a trailing 'd' after the timestamp, which causes int() to error. This is technically valid in InspIRCd S2S because atoi() ignores non-digit characters, but it's strange behaviour either way:
<- :3AX FJOIN #monitor 1485462109d + :,3AXAAAAAK

Thansk to @koaxirc for reporting.
2017-02-05 21:06:42 -08:00
James Lu
18826ad5c6 exec: add 'iexec' to run code in an isolated, persistent local scope 2017-02-05 20:26:40 -08:00
James Lu
a3a5569156 exec: print a "Done" after executing code 2017-02-05 20:14:30 -08:00
James Lu
dcacfb0c10 relay: catch RuntimeError in spawnRelayServer as well
This is raised when a network runs out of SIDs, for example.
2017-02-03 18:06:49 -08:00
James Lu
b5cf2e8a4e stats: add an --all option to 'uptime', and check for disconnected networks 2017-01-30 00:18:50 -08:00
James Lu
efded33f4a stats: oops, use the right Irc object 2017-01-30 00:08:40 -08:00
James Lu
de99be720e stats: prettier formatting for the uptime command
Closes #381.
2017-01-30 00:06:42 -08:00
James Lu
1c19d82f53 Merge branch 'wip/stats' into devel 2017-01-29 23:31:43 -08:00
James Lu
8901ed72ee relay: s/spawnIfMissing/spawn_if_missing/g 2017-01-29 22:18:05 -08:00
James Lu
fa30d3c732 relay: rework fallback message routing to be less annoying (#384)
- PRIVMSGs from users not spawned on a network are sent via the main PyLink client in the format "<$orignick/$network> <$text>"
    - <PyLink> <user/net> blah blah
- NOTICEs from users use the same format above, and are routed from the relay subserver representing the network that the message originated from
    - Notice(somenet.relay): <user/net> blah blah
- PRIVMSGs from servers are blocked, because they aren't valid on all IRCds and are fairly obscure anyways (suggestsions/improvements welcome)
- NOTICEs from servers are forwarded as raw text, from the relay subserver representing the origin network
    - Notice(somenet.relay): some server announcement
2017-01-29 20:21:45 -08:00
James Lu
e936b9cfd2 relay: add a spawn_if_missing option to get_remote_sid() 2017-01-29 20:20:39 -08:00
James Lu
359132045d protocols: allow forwarding NOTICE from servers (#384) 2017-01-29 19:49:37 -08:00
James Lu
8fdcb9d2bb Bump version to 1.2-dev 2017-01-29 18:11:31 -08:00
James Lu
0d99bc62d2 -mkpasswd: fetch password via getpass instead of requiring command line entry
This gives better security, since running programs and their command lines are visible in 'ps'.
2017-01-29 00:12:05 -08:00
James Lu
62c4b79e04 setup.py: install pylink-mkpasswd as a script 2017-01-28 23:27:24 -08:00
James Lu
e036449c72 README: move Apt / PPA instructions to after source builds and pip
These aren't tested as thoroughly.
2017-01-28 19:58:32 -08:00
James Lu
69b112eb44 PyLink 1.1.0.1
This is identical to 1.1.0 - bumping the version due to a botched PyPI upload.
2017-01-24 20:52:01 -08:00
James Lu
c7ac8eeafb PyLink 1.1.0 2017-01-24 20:42:45 -08:00
James Lu
b18b2fff17 setup.py: fix last commit 2017-01-22 17:00:27 -08:00
James Lu
39d2243b11 setup.py: use extras_require for expiringdict and passlib 2017-01-22 16:44:42 -08:00
James Lu
9daa452f8e Update dependencies in setup.py & README 2017-01-22 16:42:52 -08:00
James Lu
d6a6d069bc Move 'mkpasswd' to the commands plugin 2017-01-22 16:42:46 -08:00
James Lu
fd12a5d919 core: make passlib an optional dependency 2017-01-22 16:42:46 -08:00
James Lu
354a3022a4 services-api.md: remove extra_channels argument per d31d09ce7ec1b3be8062e5e18f3572832f7f50b7 2017-01-22 00:55:37 -08:00
James Lu
ce742d40eb docs/technical: fix capitalization of ircd-ratbox 2017-01-21 13:13:53 -08:00
James Lu
c20c144222 automode, relay: remove unused imports 2017-01-21 12:01:41 -08:00
James Lu
170738ee10 Revert "automode: bandaid fix for "service already registered" errors on first load"
This reverts commit e2a853c98ed5487344149ecf611e2171d9e1bf68.
2017-01-21 11:59:43 -08:00
James Lu
3dd35ba5a1 Add an example services plugin
Closes #399.
2017-01-21 11:59:43 -08:00
James Lu
d33eb22ca3 ServiceBot: verify that the service name is a valid nick 2017-01-21 11:59:43 -08:00
James Lu
d31d09ce7e ServiceBot: remove extra_channels argument from the constructor
This is unused and a poor thing to hardcode anyways.
2017-01-21 11:59:43 -08:00
James Lu
1fe64cca04 example.py: update to reflect 1.1.x docstring changes (#307) 2017-01-21 11:59:43 -08:00
James Lu
7bbe77fe4a Irc: remove unused "bot_clients" attribute
Thanks to @IotaSpencer for pointing this out!
2017-01-21 11:08:37 -08:00
James Lu
d749fbb2ab ctcp: -that 2017-01-20 22:32:32 -08:00
James Lu
b7470c3c42 ctcp: remove puns 2017-01-20 22:31:46 -08:00
James Lu
5cd7e2e14c PyLink 1.1-beta2 2017-01-15 01:13:21 -08:00
James Lu
39db5aee04 ts6_common: properly handle KICK without a reason
This field is optional in TS6.
2017-01-14 11:08:24 -08:00
James Lu
7245e978cd unreal: in MODE, also wrap to 12 modes per line
Closes #393. Really fixes #253.
2017-01-13 23:58:11 -08:00
Mitchell Cooper
5e79ea908d inspircd: fix comment that says FIDENT in the FHOST handler (#392) 2017-01-12 21:00:24 -08:00
James Lu
ca8b025f9a Merge branch 'master' into devel
Conflicts:
	RELNOTES.md
	VERSION
2017-01-12 20:59:41 -08:00
James Lu
4f0ca6367c unreal: fix math error
len(":SIDAAAAAA ") is 11, not 9
2017-01-12 19:54:47 -08:00
James Lu
b1918f5392 Initial pass at a Stats plugin (#121, #381) 2017-01-11 23:35:35 -08:00
James Lu
165f8fa4a7 world: start tracking daemon start time (#381) 2017-01-11 23:35:10 -08:00
James Lu
487a07671b ts6: implement line wrapping in SJOIN ban bursts (#253) 2017-01-11 23:09:25 -08:00
James Lu
702ba84956 wrapArguments: add a max_args_per_line option (#253) 2017-01-11 23:09:25 -08:00
James Lu
4147af6546 hooks-reference: remove redundant Introduction heading 2017-01-09 22:25:16 -08:00
James Lu
fb9144a715 clientbot: always add channels to users' channel lists in names reply
Do this regardless of whether the user is already added to the channel's user list/kick queue, since those are tracked separately.

Closes #388.
2017-01-09 22:23:59 -08:00
James Lu
ccfc2f601d clientbot: explicitly send /names after join (#388) 2017-01-09 22:23:59 -08:00
James Lu
602f35cb70 clientbot: fix message recognition treating nick prefixes without ident@host as servers 2017-01-09 22:23:59 -08:00
James Lu
7fe65e1f8a Refresh protocol spec for 1.0-beta1+
- Replace utils.parseModes() with irc.parseModes()
- Update source code links to 1.0-beta1
2017-01-09 22:23:35 -08:00
James Lu
70deb5a285 ts6: rewrite MODE wrapping to check message length and argument count (#253) 2017-01-08 21:20:42 -08:00
James Lu
e2c0877e9b wrapModes: optionally check for max. modes per line (#253) 2017-01-08 21:19:26 -08:00
James Lu
aafd734e3a ts6: remove leftover mode filtering in mode() 2017-01-08 20:43:08 -08:00
James Lu
74755db6e1 example-conf: log settings no longer need a restart to update 2017-01-08 17:34:29 -08:00
James Lu
7594933550 parseModes: reorder logic so that -k * workarounds work again
Thanks @cooper for noticing this.
2017-01-08 17:31:50 -08:00
James Lu
b2286157ef core: update stdout log level on REHASH 2017-01-07 00:12:35 -08:00
James Lu
a2e7a35998 relay: remove incorrect network name in logging for blocked kicks 2017-01-06 22:51:50 -08:00
James Lu
e0bda6b850 relay: also filter out low TS values in burst relaying 2017-01-06 22:48:11 -08:00
James Lu
8878f77636 updateTS: don't save any broken TS values lower than 750000
Workaround for #385, and other related timestamp issues caused by the TS value getting cut off in mode messages (#283)

(cherry picked from commit ba330bbfac5c542bb2fc0661d01e1fd203253308)
2017-01-06 22:35:16 -08:00
James Lu
d4b4cfb32e wrap* funcs: convert input args to a list, so that pop(0) always works 2017-01-06 22:13:27 -08:00
James Lu
ba330bbfac updateTS: don't save any broken TS values lower than 750000
Workaround for #385, and other related timestamp issues caused by the TS value getting cut off in mode messages (#283)
2017-01-06 21:56:22 -08:00
James Lu
ef4e1ecbab unreal: count the server prefix in SJOIN line wrap (#253) 2017-01-06 18:29:47 -08:00
James Lu
4183a580d2 nefarious: implement text wrapping in BURST (#253)
Some of this is totally hacky, but it still works from my initial testing...
2017-01-06 18:24:32 -08:00
James Lu
92dcf3c28e nefarious: implement text wrapping in outgoing MODE (#253) 2017-01-06 18:08:21 -08:00
James Lu
3e706366bd relay: on Clientbot networks, don't relay prefix mode changes for the relayer bot
Closes #366.
2017-01-06 16:45:48 -08:00
James Lu
43002d466e nefarious: fix misplaced log line from efe38264efa8ffb29228d917d58db859156603ef 2017-01-06 16:43:10 -08:00
James Lu
94779326bc example-conf: mention what changehost is NOT meant to do 2017-01-03 07:30:35 -08:00
James Lu
31ddc37db4 example-conf: be more clear that network names (e.g. "inspnet:") should be changed 2017-01-02 12:30:38 -08:00
James Lu
a3ff32c22e core: implement bind host support
Closes #379.
2017-01-02 12:30:24 -08:00
James Lu
9a01a5285f unreal: handle user mode changes via MODE 2017-01-02 12:20:20 -08:00
James Lu
fc3ee8d402 unreal: implement line wrapping for outgoing MODE 2017-01-02 12:16:35 -08:00
James Lu
d1d3c3ef15 Irc: oops, wrapModes() needs to be a classmethod 2017-01-02 12:15:44 -08:00
James Lu
5843eebba2 Irc: add wrapModes() abstraction 2017-01-02 12:08:22 -08:00
James Lu
12c33fcddf utils: remove incomplete example text for wrapArguments() 2017-01-02 11:08:55 -08:00
James Lu
efe38264ef nefarious: remove extraneous userlist assignment in handle_burst 2017-01-02 10:25:37 -08:00
James Lu
605d242677 automode: remove repeated "Error:" in error messages 2017-01-02 10:11:55 -08:00
James Lu
4020c3dea1 ircs2s_common: fix last commit (missing import)
(cherry picked from commit 6a90401d563567ead0823be0a9a5b6b77f9f4378)
2017-01-01 20:39:46 -08:00
James Lu
6a90401d56 ircs2s_common: fix last commit (missing import) 2017-01-01 20:39:34 -08:00
James Lu
d5eb01b724 protocols: move handle_pong into ircs2s_common, be less strict about the ping argument
This fixes issues on UnrealIRCd where PONGs get ignored if the argument doesn't match the server name entirely (e.g. different case).
Treating all PONGs from the uplink as valid is totally fine, as all we care about is that the uplink is alive.

(cherry picked from commit 38350465c1dcc3f45fafc5706aaacb37e534b9bb)
2017-01-01 20:28:26 -08:00
James Lu
38350465c1 protocols: move handle_pong into ircs2s_common, be less strict about the ping argument
This fixes issues on UnrealIRCd where PONGs get ignored if the argument doesn't match the server name entirely (e.g. different case).
Treating all PONGs from the uplink as valid is totally fine, as all we care about is that the uplink is alive.
2017-01-01 20:24:34 -08:00
James Lu
945fc8f0f9 unreal: normalize whitespace in SJOIN as well
Sometimes there is an extra space after the mode list, so the query looks like

<- :000 SJOIN 1234567890 #channel +ntf [10t]:5  :000AAAAAA 000AAAAAB

instead of

<- :000 SJOIN 1234567890 #channel +ntf [10t]:5 :000AAAAAA 000AAAAAB
2017-01-01 19:55:29 -08:00
James Lu
4a519832e0 unreal: normalize nicks to UIDs in SJOIN handling
These can still be used by old Unreal 3.2 links.
2017-01-01 13:48:47 -08:00
James Lu
71bd5583fa unreal: parse mode parameters in SJOIN as well!
Thanks to kevin for pointing this out.
2017-01-01 11:32:44 -08:00
James Lu
1a1dff7609 IrcChannel: don't assume +nt on new channels
Revert "classes.IrcChannel: default modes to +nt on join"

This reverts commit 1062e47b725a001ced44783632d691e1979c74b0.
2017-01-01 00:37:12 -08:00
James Lu
dc11638eb9 ts6 (and derivatives): don't burst bans that were already set 2017-01-01 00:28:55 -08:00
James Lu
803ccf7708 unreal: add SJOIN to required caps 2017-01-01 00:21:37 -08:00
James Lu
69be532c3c unreal: actually send the remote's modes in SJOIN hooks 2017-01-01 00:20:29 -08:00
James Lu
0b8b4dc3cf inspircd, nefarious: stop applying remote modes on sjoin
I have no clue why this code exists, but it looks wrong and probably is wrong.
2017-01-01 00:19:10 -08:00
James Lu
f851dc8ac1 unreal: implement modes in SJOIN (SJ3), respect S2S message length limits
Closes #378. Ref #253
2017-01-01 00:00:01 -08:00
James Lu
eafec9d4ad utils: add wrapArguments() to deal with S2S message cutoffs
Ref: #253, #378
2016-12-31 23:35:27 -08:00
James Lu
278339b5e2 unreal: actually enable the SJOIN cap, handle ban bursts properly 2016-12-31 22:15:42 -08:00
James Lu
a340ab15e1 relay: local channel in 'link' is optional
Thanks to Digerati for noticing this.
2016-12-29 08:47:35 -08:00
James Lu
e566b99b75 clientbot: don't crash if we receive /who for someone we don't know 2016-12-27 22:16:12 -08:00
James Lu
f1da5c57e8 clientbot: don't repeat KICK hooks if the source is internal
This prevents KICK events from being relayed twice to Clientbot links, when the kicked user is also a Clientbot user.
2016-12-27 22:09:16 -08:00
James Lu
59f232d69f clientbot: fix SASL PLAIN auth on Python 3.4
A strange bug causes "TypeError: unsupported operand type(s) for %: 'bytes' and 'tuple'" when formatting multiple args into a byte string using %b.
2016-12-27 18:16:15 -08:00
James Lu
f6f951fba8 PyLink 1.1-beta1 2016-12-26 08:14:05 -08:00
James Lu
af6b191164 Merge remote-tracking branch 'origin/disable-default-pidcheck' into devel 2016-12-25 11:19:37 -08:00
James Lu
6728627f78 docs: document the permissions API for developers
Closes #368.
2016-12-25 11:17:10 -08:00
James Lu
e53c758471 docs: reorganize & include a list of all PyLink permissions
Closes #365.
2016-12-25 00:41:41 -08:00
James Lu
614c029538 relay: add missing 'relay.linked' permissions check 2016-12-25 00:31:38 -08:00
James Lu
ec13bae7e6 opercmds: remove pointless source argument from 'kick' and 'kill' 2016-12-24 10:58:03 -08:00
James Lu
3e6550b8ad example-conf: mention Clientbot mode sync's behaviour with CLAIM
Closes #344.
2016-12-22 00:38:39 -08:00
James Lu
7ee963f66e Document advanced relay config & custom clientbot formatting
Closes #335.
2016-12-22 00:27:20 -08:00
James Lu
77dd8224ae relay: use built-in hash() for colorizing text
This is way faster than md5.
2016-12-21 23:48:40 -08:00
James Lu
c66c85bc9a hooks-reference: refresh for 1.1.x 2016-12-21 23:26:30 -08:00
James Lu
930443f4cd PyLink 1.0.4 2016-12-19 22:29:25 -08:00
James Lu
3595e5fdbf changehost: limit 'applyhosts' to those with the changehost.applyhosts perm
(cherry picked from commit 12a85092990743d46808544e35a787e5e048ab8c)
2016-12-19 01:29:54 -08:00
James Lu
a96bb0ce11 clientbot: make unattended SASL reauth optional 2016-12-19 01:06:49 -08:00
James Lu
68c618887f clientbot: auto-attempt SASL when it is introduced in CAP NEW 2016-12-19 00:54:20 -08:00
James Lu
f42d49b8eb cleintbot: only CAP END if we haven't registered yet 2016-12-19 00:52:28 -08:00
James Lu
880714b2f2 clientbot: implement CAP DEL, CAP NEW 2016-12-19 00:40:19 -08:00
James Lu
fdda28799c clientbot: fix message tag parsing 2016-12-19 00:18:15 -08:00
James Lu
3a8710540c Merge branch 'devel' into wip/ircv3 2016-12-19 00:10:07 -08:00
James Lu
1765d61973 Merge branch 'wip/relay/better-normalizehost' into devel
Conflicts:
	plugins/relay.py
2016-12-19 00:01:16 -08:00
James Lu
c09fce344d relay: whitelist _ on InspIRCd, UnrealIRCd, Nefarious, clientbot 2016-12-19 00:00:24 -08:00
James Lu
ef8ec03e41 relay: re-add / in hosts for networks that support it 2016-12-18 23:56:47 -08:00
James Lu
5f6337a734 Merge branch 'master' into devel
Conflicts:
	plugins/networks.py
2016-12-18 00:15:24 -08:00
James Lu
194a62fcea protocols: implement basic nick collision detection in UID handlers
Closes #285. Closes #375.
2016-12-18 00:13:42 -08:00
James Lu
3339bf0fe6 relay: rewrite normalizeHost() to whitelist characters instead 2016-12-17 23:47:26 -08:00
James Lu
3a852393bc relay: remove _ from hosts on ts6, ratbox 2016-12-17 23:03:28 -08:00
James Lu
de3d9bb5c9 example-conf: add a SASL login example 2016-12-17 16:18:11 -08:00
James Lu
9bfa0c9bb8 clientbot: whitelist supported SASL mechanisms, and abort on invalid ones 2016-12-17 16:18:11 -08:00
James Lu
90e10f948e clientbot: rename conf value sasl_mech -> sasl_mechanism 2016-12-17 16:18:11 -08:00
James Lu
0cc1ff8fa3 clientbot: log CAP REQ, CAP NAK events 2016-12-17 16:18:11 -08:00
James Lu
8b0b4bfcc4 clientbot: log SASL failures to warning instead of info 2016-12-17 16:18:11 -08:00
James Lu
651752d23d clientbot: send CAP END if SASL is disabled 2016-12-17 16:18:11 -08:00
James Lu
9cc817d544 clientbot: require SSL for SASL external, better grammar in SASL misconfiguration errors 2016-12-17 16:18:11 -08:00
James Lu
a6b889c469 Irc: fix whitespace 2016-12-17 16:18:11 -08:00
James Lu
ffed5e3378 clientbot: also CAP END on 906 (SASL aborted) 2016-12-17 16:18:11 -08:00
James Lu
8666151189 clientbot: properly verify ACKed/NAKed caps, add support for SASL PLAIN & EXTERNAL 2016-12-17 16:17:36 -08:00
James Lu
3bc9b1bc55 clientbot: implement IRCv3.2 CAP and IRCv3.1 multi-prefix (#290) 2016-12-16 22:28:40 -08:00
James Lu
cbc7f438d2 clientbot: implement preliminary message tags parsing
Untested so far...
2016-12-16 22:28:22 -08:00
James Lu
231fd13429 launcher: disable pid check by default
I'm postponing this until PyLink 2.0 to make migration from 1.0.x to 1.1 less of a nuisance.

Closes #364.
2016-12-16 20:55:09 -08:00
James Lu
7e37a90c80 clientbot: delete channels on kick / part, or if a channel becomes empty after parting
Closes #314.
2016-12-16 20:50:36 -08:00
James Lu
e7a005b685 control: log remaining threads on shutdown, for debugging freezes 2016-12-16 19:42:12 -08:00
James Lu
95b58fc2c4 utils: abstract protocol/plugin import prefixes, and implement filtering by plugin in 'list'
Closes #369.
2016-12-16 19:25:41 -08:00
James Lu
7b5fcc3219 corecommands: use irc.error() in login fail wrapper 2016-12-16 19:06:33 -08:00
James Lu
f9adaa85ca commands: implement 'logout'
Closes #370.
2016-12-16 19:05:08 -08:00
James Lu
2be8d5b282 README: supported->possible for unreal 3.2 notes
UnrealIRCd 3.2 is EOL Dec. 31, so it's not really worth actively "supporting" it.
2016-12-16 19:04:44 -08:00
James Lu
1fd7aba3b0 example-conf: add migration notes for login:user/password deprecation 2016-12-16 18:40:27 -08:00
James Lu
045abfa9c1 commands: add permissions checks to echo, showuser, showchan, and status 2016-12-16 18:31:19 -08:00
James Lu
2de36caea0 commands: fix previous commit 2016-12-09 22:13:32 -08:00
IotaSpencer
b0296933a1 plugins/commands.py: change over to permissions API 2016-12-09 22:13:32 -08:00
IotaSpencer
9199186309 plugins/opercmds.py: change over to permissions API 2016-12-09 22:13:32 -08:00
IotaSpencer
69083cfcfd plugins/bots.py: change over to permissions API 2016-12-09 22:13:32 -08:00
James Lu
12a8509299 changehost: limit 'applyhosts' to those with the changehost.applyhosts perm 2016-12-09 22:13:32 -08:00
James Lu
611b07e6bd networks: update help for 'disconnect'
The 'disconnect' command has been changed to disable autoconnect, but this text was never updated.

(cherry picked from commit 34ad79744a816e5ac6bf41963954f1e83c352274)
2016-12-09 21:56:25 -08:00
James Lu
60ca5af813 networks: port to the permissions API (#367) 2016-12-09 21:55:47 -08:00
James Lu
fb463e3b26 networks.remote: override the login to a valid user, so that permissions can match it 2016-12-09 21:53:54 -08:00
James Lu
34ad79744a networks: update help for 'disconnect'
The 'disconnect' command has been changed to disable autoconnect, but this text was never updated.
2016-12-09 21:51:57 -08:00
James Lu
01f2a4ca20 servermaps: fix wrong permission name 2016-12-09 21:43:58 -08:00
James Lu
3d4cb02ce2 exec: rewrap docstirngs 2016-12-09 21:43:58 -08:00
James Lu
9e2598612a exec: port to permissions API (#367) 2016-12-09 21:43:58 -08:00
James Lu
5a066bfde4 corecommands: port to use the permissions API (#367) 2016-12-09 21:43:58 -08:00
James Lu
1e7210a6db Refresh permissions examples, adding a stub in the main example conf for admin access 2016-12-09 20:57:49 -08:00
James Lu
57aa844fcb permissions: limit "login = admin access" to old-style (< 1.1) login blocks 2016-12-09 20:57:01 -08:00
James Lu
506467193e Import RELNOTES for 1.1-alpha1 2016-12-09 20:14:26 -08:00
James Lu
1761effa28 VERSION: bump to 1.1-alpha1 2016-12-09 18:37:54 -08:00
James Lu
a2e0bb19dc launcher: set terminal title on launch (windows & posix) 2016-12-09 18:10:47 -08:00
James Lu
b5f244009a relay: rename camel case functions to lowercase with underscores
The exception is isRelayClient(), which is aliased only to is_relay_client() to be consistent with isXYZ() functions elsewhere in the framework
Also, getRemoteChan() was renamed to get_remote_channel()
2016-12-09 18:02:30 -08:00
James Lu
2b4943a780 relay: add locks in db read/writes (thread safety) 2016-12-09 17:44:11 -08:00
James Lu
e40b2f6529 relay: add 'purge' command to remove all relays involving a network
Closes #356.
2016-12-09 17:34:51 -08:00
James Lu
8855ef2a41 relay: don't break in removeChannel if irc.pseudoclient isn't set 2016-12-09 17:34:34 -08:00
James Lu
e7e2f2c98e core: update "missing dependencies" errors
Closes #363.
2016-12-09 17:15:53 -08:00
James Lu
638b9dc84a relay: rewrap help for LINKACL
More formatting changes to come.
2016-12-05 23:33:42 -08:00
James Lu
03766b9f89 ServiceBot: implement docstring rewrapping per #307.
Closes #307.
2016-12-05 23:33:03 -08:00
James Lu
b3387f2d41 conf: fix deprecation warnings crashing because log is unavailable
This allows conf methods to access to global logger by via an optional 'logger' argument. However,
the caveat is that the logging facilities are still unavailable on first start, because log can
only be imported *after* the configuration is loaded.
2016-12-05 22:43:01 -08:00
James Lu
62a4bc01ce Merge branch 'master' into devel 2016-12-05 00:59:41 -08:00
James Lu
664bd3a89f pylink-contribdl: link to contrib modules list if no argument is given 2016-12-05 00:45:53 -08:00
James Lu
3f4bf72248 pylink-contribdl: implement --yes 2016-12-05 00:39:09 -08:00
James Lu
c479dd1753 Add a contrib module downloader (pylink-contribdl)
Fixes #350.
2016-12-05 00:36:11 -08:00
James Lu
f5633329f8 utils: drop loadModuleFromFolder; it is unused since bcc84b8618f641c77b0a599de9ab9d24432b8c29 2016-12-04 23:35:16 -08:00
Ken Spencer
e59d829973 update mailmap with my (IotaSpencer) aliases
(cherry picked from commit 2c1b8c09529dd229d570eb464cb3105cede146e6)
2016-11-22 22:52:05 -08:00
Ken Spencer
a173e2a61f pylink: Warn users that configuration is changing. (closes #361) 2016-11-22 22:49:23 -08:00
James Lu
e4b2ea60ec Irc: demote unknown user errors in parseModes() to DEBUG
Some ancient services like Anope 1.8 set SVS2MODE +d on users when they connect, even if the user quits right after. Due to lag we may receive the MODE after the QUIT instead of before.

(cherry picked from commit ec4e71c8cfb86105a28e7bc0b63d91dcc8c37a60)
2016-11-22 16:27:49 -08:00
James Lu
1888f24ef0 Irc: fix matchHost not using realhost properly
(cherry picked from commit f9e798cf936a64bbd6d19341f444ad4ce6bcca36)
2016-11-20 13:41:46 -08:00
James Lu
a57f194123 changehost: add options to match users by IP and realhost 2016-11-20 12:34:11 -08:00
James Lu
476f84a181 changehost: implement enforce exceptions 2016-11-20 12:33:58 -08:00
James Lu
f9e798cf93 Irc: fix matchHost not using realhost [rp[er;y 2016-11-20 12:29:51 -08:00
James Lu
d90f44c510 changehost: explicitly ignore PyLink internal clients 2016-11-20 12:04:30 -08:00
James Lu
501647805c changehost: add optional vHost enforcement 2016-11-20 11:53:55 -08:00
James Lu
ffc271a53a login: Use a slightly faster CryptContext 2016-11-19 17:47:55 -08:00
James Lu
c4351d61c6 Add command line mkpasswd utility & encrypted password example 2016-11-19 17:21:45 -08:00
James Lu
b1e4b34b79 Switch 'identify' to use the new login backend, add passlib to README dependencies
This new backend supports optional encryption (sha256_crypt / sha512_crypt via passlib). Closes #322.
2016-11-19 17:21:45 -08:00
James Lu
69066029f1 Simplify/rewrite the login module 2016-11-19 17:01:05 -08:00
James Lu
3308db0cd2 Import coremods/login.py (abstracted login checking) from @IotaSpencer's repo 2016-11-19 17:01:00 -08:00
James Lu
797476f7ad Version bump to 1.1-dev1 (1.1.x milestone 1) 2016-11-18 23:18:07 -08:00
James Lu
048367d6c5 AUTHORS: add @IotaSpencer and update my email 2016-11-18 23:16:50 -08:00
James Lu
c77ad6faa9 bots: PseudoClient->client in help text & error messages 2016-11-18 23:13:56 -08:00
Ken Spencer
d467d27ecd plugins: change remaining plugins over irc.error() use 2016-11-18 23:11:44 -08:00
Ken Spencer
02dfe5aeab classes: pass force_privmsg_in_private and private booleans to irc.reply() in irc.error() 2016-11-18 23:10:45 -08:00
Ken Spencer
a1bbb4fdb9 utils: style points! WOOOO 2016-11-18 23:10:45 -08:00
Ken Spencer
289ab78052 plugins/bots: change over to irc.error() use 2016-11-18 23:10:45 -08:00
Ken Spencer
940430b075 plugins/automode: change errors over to irc.error() based use 2016-11-18 23:10:45 -08:00
Ken Spencer
fdd4135632 utils: add error() to use classes.py's irc.error() 2016-11-18 23:10:45 -08:00
Ken Spencer
c450d71f84 classes: add irc.error() for easier error replies 2016-11-18 23:10:45 -08:00
James Lu
57f7acc124 pylink: fix last commit (args.no_check_pid) 2016-11-16 21:52:07 -08:00
James Lu
7c93a6cdfc pylink: tidy up --no-check-pid setting 2016-11-14 21:38:02 -08:00
James Lu
8e7cffc016 docs/automode: update perms list & caveat section
- Automode remote management and its relevant permissions were added in the last commit (8ff292bd1f314d50c2a2f8355ac2ca171f62b7f2).
- Autoop on join for services bots was implemented in 0cce6ca4885c986e09b4afba78a416cf9016538b.
2016-11-12 12:24:00 -08:00
James Lu
8ff292bd1f automode: support remote channel manipulation in the form netname#channel
Closes #352.
2016-11-12 12:20:25 -08:00
James Lu
9dbf124e36 docs/automode: fix typo 2016-11-12 12:18:41 -08:00
James Lu
691a8178b2 relay: implement 'showchan' with links info
Closes #353.
2016-11-12 10:43:55 -08:00
James Lu
e570779a03 Remove update.sh helper script 2016-11-12 10:39:11 -08:00
James Lu
c077f9aebb DataStore: log the name of the current database implementation to DEBUG 2016-11-09 22:56:57 -08:00
James Lu
b94e11930e structures: directly retrieve DB save delay in DataStore (#303) 2016-11-09 22:53:13 -08:00
James Lu
b0636b40ab Finish the plugin migration to DataStore
Closes #303.
2016-11-09 22:47:22 -08:00
James Lu
089dce3853 structures: revise DataStore a bit (#303) 2016-11-09 22:19:30 -08:00
James Lu
377df413ed Irc: s/isServiceBot/getServiceBot/g (#355)
This function is renamed to better reflect its return value (ServiceBot object instead of boolean True).
2016-11-09 19:09:59 -08:00
James Lu
08fa64c3cc Irc, services_support: store service name in IrcUser objects (#355) 2016-11-09 19:07:01 -08:00
James Lu
0815df1bca Irc: rewrite isInternalClient to use getServer & return a boolean (#355) 2016-11-09 18:55:53 -08:00
James Lu
ddd0436937 Irc: rewrite getServer() to look for IrcUser.server attribute (#355)
The relevant attribute was introduced in c57fabc9ef2f9acecc70130e6f48c17ab435dc6d.
2016-11-09 18:55:08 -08:00
James Lu
c57fabc9ef core, protocols: add server argument to IrcUser (#355) 2016-11-09 18:40:16 -08:00
James Lu
b0bd5d47ae relay: fix logging format in spawnRelayUser error 2016-11-07 22:04:34 -08:00
James Lu
44743d860e relay: don't break autoconnect anymore when there's a server conflict
This reverts most of commits 5c7524b and f2a5e1d.
2016-11-07 21:53:52 -08:00
James Lu
4246a3d113 relay: work on sane fallbacks when a network's SID goes missing (#354) 2016-11-07 21:47:53 -08:00
James Lu
b79e693808 exec: Drop 'raw' text logging to DEBUG for security purposes
Closes #347.
2016-11-07 21:25:57 -08:00
James Lu
2d20256ed8 Relay: rework to use the permission system
This defines the following permissions:

Granted to opers by default:
- relay.create
- relay.destroy
- relay.claim
- relay.link
- relay.delink
- relay.linkacl.view
- relay.linkacl

Granted to all users by default:
- relay.linked

And the following which is not explicitly granted:
- relay.savedb

Closes #325.
2016-11-07 21:22:52 -08:00
James Lu
93ca62aa49 Revamp configuration to support multiple accounts (#319) 2016-11-07 21:01:28 -08:00
James Lu
e8cc7227a8 PyLink 1.0.3 2016-11-05 21:40:40 -07:00
James Lu
28b88a3840 README: add Ubuntu PPA instructions 2016-11-05 21:31:50 -07:00
James Lu
34fd45dce9 Merge remote-tracking branch 'origin/master' into devel 2016-11-05 21:27:50 -07:00
James Lu
d2f95acd2e Proofread and edit the example-conf 2016-11-04 18:13:15 -07:00
James Lu
6ad40c91b4 Irc: explicitly kill connect loop threads after an Irc object has been removed
Possible fix for #351.
2016-11-02 22:34:02 -07:00
James Lu
e977c95520 Merge branch 'master' into devel
Conflicts:
	VERSION
	example-conf.yml
2016-11-02 22:28:39 -07:00
James Lu
9f43c0fe17 automode: fix typo in DB error message 2016-11-02 22:27:01 -07:00
James Lu
9950e0948f conf: split off absolute paths in confname
This fixes invalid database names such as "automode-/tmp/test.db" from being generated when PyLink is started with an absolute path to its config.

Closes #358.
2016-11-02 22:23:45 -07:00
James Lu
441bf5f048 README: link to utopia repository site 2016-10-28 20:05:35 -07:00
James Lu
e0802192f6 README: add Debian repository instructions 2016-10-28 20:04:39 -07:00
James Lu
72818412e1 faq: fix link to dependencies list 2016-10-27 20:50:10 -07:00
James Lu
522e6df73c Actually, use LF line endings for everything
(cherry picked from commit e49d36d8246d008abe6fe1a695d9952341fc354a)
2016-10-27 18:43:57 -07:00
James Lu
9f4c68f114 relay: skip channel TS check for Clientbot
(cherry picked from commit d230af1d5e3e14a12dc267bbd66b1bcf219e0ae9)
2016-10-22 20:59:34 -07:00
James Lu
d230af1d5e relay: skip channel TS check for Clientbot 2016-10-22 20:45:43 -07:00
James Lu
a5d97c15e7 relay: disable nick collide messages to debug 2016-10-20 20:13:17 -07:00
James Lu
2ed8b68c44 relay: allow configuring custom relay server suffixes
Closes #333.

(cherry picked from commit 39008334584023ddc540f7f62e45e7910ecd6783)
2016-10-20 19:10:47 -07:00
James Lu
4f3fa04cde docs/t/services-api: mention ServiceBot.join() 2016-10-16 18:48:29 -07:00
James Lu
c4e0923fb5 pylink: add a migration note for pid checking 2016-10-15 14:34:21 -07:00
James Lu
b750bd4d15 coremods/control: ignore errors when removing PID file 2016-10-15 14:31:13 -07:00
James Lu
774f30c940 pylink: exit with a non-zero code if pid check fails; reword error to be more helpful 2016-10-15 14:23:29 -07:00
Ken Spencer
bc4be815e4 coremods: make _shutdown remove running 'config'.pid 2016-10-15 16:50:25 -04:00
Ken Spencer
a30942669a pylink: Stop daemon if pid file exists and we're checking 2016-10-15 16:43:14 -04:00
Ken Spencer
aaadb63137 Add PID file checking 2016-10-15 16:09:35 -04:00
James Lu
ac270c200c PyLink 1.0.2 2016-10-14 22:51:33 -07:00
James Lu
a09ec494f6 README: cherry-pick updates from the devel branch
Squashed commit of the following:

commit 00279a148abe5f1f5413116b7736f758883c8287
Author: James Lu <GLolol@overdrivenetworks.com>
Date:   Mon Oct 10 15:55:36 2016 -0700

    README: update clientbot notes
    (cherry picked from commit 0db194726318ce2bb46b454ebe792b3896f4b80c)

commit 33cd90c2553977f17e61006f29288abbe9fe5cac
Author: James Lu <GLolol@overdrivenetworks.com>
Date:   Mon Oct 10 15:54:30 2016 -0700

    README: update IRCd notes
    (cherry picked from commit 44d139f610f51d51bd59b00015fa75ad9548ee4c)

commit 639e185977e8cfe98932310d132751ea9d720609
Author: James Lu <GLolol@overdrivenetworks.com>
Date:   Sun Sep 25 19:32:00 2016 -0700

    README: briefly mention protocols/clientbot

    (cherry picked from commit 1fbd9edc3b9601841b06d94aca907ea27fcab677)
2016-10-14 22:39:28 -07:00
James Lu
405b886ba2 clientbot: overload _getUid() to deal with nick collisions between virtual clients and Clientbot users
Closes #327.

(cherry picked from commit 05e2d6d060de5cc3a04712c74df7f377cafbf2d5)
2016-10-14 22:33:29 -07:00
James Lu
eb64190228 Clientbot: use a more specific realname fallback
(cherry picked from commit 288a2fffd79d2f34a3af36fbf222bf8fd9d7641c)

This is cherry-picked as a prerequisite for the next commit.
2016-10-14 22:33:10 -07:00
James Lu
05e2d6d060 clientbot: overload _getUid() to deal with nick collisions between virtual clients and Clientbot users
Closes #327.
2016-10-14 22:29:13 -07:00
James Lu
9e6e30324f setup: reword fallback version format & warnings
Also, change the suffix from -dirty to -nogit.

(cherry picked from commit a4e1f2a4ab78bd8511378088ef785a397e76ad02)
2016-10-14 21:30:54 -07:00
James Lu
a4e1f2a4ab setup: reword fallback version format & warnings
Also, change the suffix from -dirty to -nogit.
2016-10-14 21:30:43 -07:00
James Lu
0db1947263 README: update clientbot notes 2016-10-10 15:55:36 -07:00
James Lu
44d139f610 README: update IRCd notes 2016-10-10 15:54:30 -07:00
James Lu
7917502799 clientbot: make oper status tracking a network-specific option 2016-10-09 19:04:59 -07:00
James Lu
288a2fffd7 Clientbot: use a more specific realname fallback 2016-10-07 21:08:55 -07:00
James Lu
5c2e7e9324 Clientbot: unconditionally block MODE hooks if we're the sender
This is another check to prevent possible infinite loops in MODE syncing.
2016-10-07 20:54:52 -07:00
James Lu
72ca41df33 Irc, clientbot: disallow unsetting bans that don't exist
This fixes an infinite loop when:
- Clientbot modesync is enabled
- 2 or more clientbot linked networks show unsetting modes that weren't enabled (e.g. charybdis)
- A user removes a ban

The workaround in clientbot prevents this process from triggering an infinite loop when a mode change acknowledgement is received for unsetting a non-existant ban,
though multiple -b mode changes may still be seen due to race conditions in updating the various networks' states.
2016-10-07 20:54:52 -07:00
James Lu
386c71475a servermaps: removing leading - from entries 2016-10-07 18:51:31 -07:00
James Lu
e2e5de009b Merge branch 'master' into devel
Bump version to 1.1-dev
2016-10-05 20:31:16 -07:00
James Lu
8bffffa000 example-conf: comment out filerotation: so it doesn't become an empty, null-valued block 2016-10-05 20:30:32 -07:00
James Lu
e22d75d1a1 setup: install README.md as well ... 2016-10-05 19:22:23 -07:00
James Lu
738f027f33 PyLink 1.0.1 2016-10-05 19:13:00 -07:00
James Lu
613bece13a .gitattributes: force .py and .md to LF
(cherry picked from commit 8acdfc81c1d40250f2eac2986a6f15f06a57303f)
2016-10-05 19:08:07 -07:00
James Lu
8486d7c85b setup.py: Ship the VERSION file as well 2016-10-05 19:07:37 -07:00
James Lu
ec4e71c8cf Irc: demote unknown user errors in parseModes() to DEBUG
Some ancient services like Anope 1.8 set SVS2MODE +d on users when they connect, even if the user quits right after. Due to lag we may receive the MODE after the QUIT instead of before.
2016-10-02 22:09:33 -07:00
James Lu
844a4d5f19 README: add missing notes for ratbox 2016-10-01 16:45:46 -07:00
James Lu
aa0d1596b0 ts6: don't crash when CHGHOST target is a nick instead of UID
(cherry picked from commit 4dcbc85a816eee8d509940dfefea9bdd078bb56d)
2016-10-01 13:43:18 -07:00
James Lu
9997fa9306 docs/t: update protocol modules list 2016-10-01 13:42:48 -07:00
James Lu
36e18929de docs, hybrid, ratbox: Mode definition updates 2016-10-01 13:41:11 -07:00
James Lu
eb79f77bd2 ratbox: fixes for mode name consistency 2016-10-01 13:40:10 -07:00
James Lu
e4e00b4cd3 README: add ratbox to supported IRCds 2016-10-01 13:00:20 -07:00
James Lu
19c5a8c64f ratbox: update mode definitions 2016-10-01 13:00:04 -07:00
James Lu
a4c6a72a9c README: move nefarious to Primary support 2016-10-01 12:44:18 -07:00
James Lu
4dd0709f66 protocols, core: remove unused imports 2016-10-01 12:43:12 -07:00
James Lu
625e7b8aae ratbox: handle ENCAP LOGIN (#338) 2016-10-01 12:39:37 -07:00
James Lu
d943a8286f ratbox: fix typo in outgoing REALHOST 2016-10-01 00:40:20 -07:00
James Lu
4dcbc85a81 ts6: don't crash when CHGHOST target is a nick instead of UID 2016-10-01 00:34:38 -07:00
James Lu
bd9885182e ratbox: stub updateClient to prevent hostname desyncs
In ratbox, arbitrary host changing via CHGHOST is not supported.
2016-10-01 00:20:54 -07:00
James Lu
39987b6dcc ts6: supply SAVETS_100 capability for ratbox 2016-10-01 00:11:50 -07:00
James Lu
d6cb5c1ed0 ratbox: implement REALHOST (#338) 2016-09-30 23:46:23 -07:00
James Lu
f618feea26 Initial protocol support for Ratbox (#338) 2016-09-30 23:33:27 -07:00
James Lu
d9fdd9dfcb ts6: modularize required capabilities 2016-09-30 23:33:04 -07:00
James Lu
1cb320f5f4 clientbot: only send MODE if there are modes left after filtering 2016-09-25 20:21:01 -07:00
James Lu
57b566286d relay: make clientbot modesync more configurable (#287) 2016-09-25 20:07:16 -07:00
James Lu
8dd0cb19af clientbot: fix outgoing mode filtering 2016-09-25 20:07:16 -07:00
James Lu
1fbd9edc3b README: briefly mention protocols/clientbot 2016-09-25 20:07:16 -07:00
James Lu
4222cc30a8 relay, clientbot: implement clientbot mode sync
Closes #287.
2016-09-25 20:07:16 -07:00
James Lu
13a42c17b2 servermaps: skip clientbot networks (servers aren't properly tracked) 2016-09-24 16:36:28 -07:00
James Lu
438838f81d ServiceBot: handle autojoin additions even if bots haven't spawned yet 2016-09-24 12:33:57 -07:00
James Lu
1c60ad7251 services_support: honour joinmodes settings on kick-rejoin 2016-09-24 12:22:12 -07:00
James Lu
7d20b70d33 relay_cb: fix UnboundLocalError when kicking a service bot 2016-09-24 12:20:18 -07:00
James Lu
b9d8ec5039 relay: listen to PYLINK_SERVICE_JOIN from services_support 2016-09-24 12:13:33 -07:00
James Lu
eb18a6cf67 Bump VERSION to 1.0.0-dev 2016-09-24 12:08:46 -07:00
James Lu
dfa75f6606 servermaps: simplify output format
Drawing all the |'s and `'s is a rather complex process, so just replace this with bullet-point output.
2016-09-24 12:02:43 -07:00
James Lu
60b595ea6f servermaps: fix help for 'localmap' 2016-09-24 11:58:28 -07:00
James Lu
595bceda2e servermaps: implement network maps over relay!
Local-only map is available too using the 'localmap' command.
2016-09-24 11:56:37 -07:00
James Lu
68c247f764 New servermaps plugin: displays network /map's from the PyLink server's perspective 2016-09-24 11:19:33 -07:00
James Lu
d59732f6dd automode: simplify join routines to use ServiceBot.join() (#326) 2016-09-23 23:43:27 -07:00
James Lu
a040c3c7d2 ServiceBot: modularize join() for explicit channel joining (#326) 2016-09-23 23:43:07 -07:00
James Lu
0cce6ca488 service_support: allow specifying modes to join with for each service (#326)
This updates the example config to, by default, join Automode bots as op in channels.
2016-09-23 23:10:38 -07:00
James Lu
3900833458 relay: allow configuring custom relay server suffixes
Closes #333.
2016-09-23 22:49:04 -07:00
James Lu
d05917222d relay: clobber colour codes in hosts
(cherry picked from commit b467da13b12b87b972714d89129e7e93f11e7137)
2016-09-20 07:01:16 -07:00
James Lu
df50b7b137 bots: allow JOIN/NICK/QUIT on ServiceBot clients
(cherry picked from commit 1c86f3200302c3398a448436be79e507601b2f9c)
2016-09-20 07:01:16 -07:00
James Lu
b467da13b1 relay: clobber colour codes in hosts 2016-09-20 06:58:04 -07:00
James Lu
e0f56a157d relay_cb: remove dark gray from colours index
It's too hard to read on a dark background.
2016-09-19 21:56:12 -07:00
James Lu
8acdfc81c1 .gitattributes: force .py and .md to LF 2016-09-19 18:23:25 -07:00
James Lu
1c86f32003 bots: allow JOIN/NICK/QUIT on ServiceBot clients 2016-09-19 18:23:11 -07:00
James Lu
c62dd272d6 relay_clientbot: display the remote channel in join/kick/etc relay, not the local channel 2016-09-19 17:40:50 -07:00
James Lu
e60c020634 relay_clientbot: redo color hashing to be more unique 2016-09-19 17:40:33 -07:00
James Lu
fe5a40d632 ts6: handle legacy UID by wrapping around EUID 2016-09-18 14:13:05 -07:00
James Lu
a7662f8327 PyLink 1.0.0 2016-09-16 22:25:09 -07:00
James Lu
c94a92bd5f Document the PyLink release process 2016-09-16 22:01:10 -07:00
James Lu
c690916da9 example-conf: fix inverted config desc. for relay:show_netsplits 2016-09-16 21:07:32 -07:00
James Lu
b9d6efa677 docs: refresh Automode guide with permissions info and more (#284)
Also... fix the line endings to LF
2016-09-12 20:21:34 -07:00
James Lu
e3627e4721 Normalize line endings 2016-09-12 20:16:56 -07:00
James Lu
8589feaedf protocols: fix incomplete renames from c5c77eeb9784101d690eb4252988787273ad8ea4 2016-09-12 20:16:56 -07:00
James Lu
da24b85ccd example-conf: more consistent default nick for Automode 2016-09-12 20:16:52 -07:00
James Lu
eaa09d68ce Automode: limit 'list' permissions on all channels to opers 2016-09-12 11:26:31 -07:00
James Lu
1eb48c63e5 README: actually, let's make these links more topical 2016-09-10 14:31:21 -07:00
James Lu
9adab9f6f4 README: add badges pointing to PyPI & webchat
Closes #332.
2016-09-10 14:28:31 -07:00
James Lu
9bb7cfc81b README: badges! 2016-09-09 07:28:53 -07:00
James Lu
b43d714011 clientbot: ignore WHO replies for clients we don't know
This fixes various KeyErrors, etc. when processing the WHO reply later on, as Clientbot doesn't track state for anyone who doesn't share a channel with or talks to the bot.

Reported by Kev`Bz via IRC.
2016-09-08 18:45:34 -07:00
James Lu
d8990e8439 example-conf: add debug log example 2016-09-08 18:02:55 -07:00
James Lu
1ffb1bb1ec example-conf: remove extraneous example line 2016-09-08 18:00:03 -07:00
James Lu
90f1999c6d clientbot: downgrade bad updateClient() calls to warning 2016-09-08 17:44:02 -07:00
James Lu
838ea6bfc4 relay: skip iterating over modetype definitions during reverse mode lookup
This fixes a bug when setting +l on UnrealIRCd, where the type C mode definition is coincidentally also equal to 'l'. Reported by kevin via IRC.
2016-09-07 20:37:57 -07:00
James Lu
ffa89f1e01 nefarious: fix UnboundLocalError when no modes are given on user introduction
Reported by Kev`Bz via IRC.
2016-09-07 06:57:03 -07:00
James Lu
bbdffc797d relay_clientbot: Implement relaying of text sent from service bots 2016-09-06 20:11:08 -07:00
James Lu
dd083b9b8d relay: remove extraneous comment 2016-09-06 20:11:08 -07:00
James Lu
a12ed6ad35 services_support: hack around nick clashes between service clients & real users in Clientbot
For #327. Essentially what this does is tack on a nick prefix to all service bots introduced on a Clientbot network, using characters invalid for regular nicks.
2016-09-06 20:04:19 -07:00
James Lu
aacc3149ce fantasy: don't error when bots are removed while processing (e.g. on shutdown) 2016-09-06 18:06:29 -07:00
James Lu
e2747839d1 1.0-beta1 2016-09-03 00:48:33 -07:00
James Lu
c5c77eeb97 protocols: chandata->channeldata for MODE and JOIN hooks 2016-09-02 17:52:19 -07:00
James Lu
4a80b2ce1e setup.py: install example-permissions.yml as a data file 2016-09-02 17:45:01 -07:00
James Lu
d1e2dfcf61 clientbot: only call spawnClient for new message sources after irc.pseudoclient is set 2016-09-01 13:08:20 -07:00
James Lu
4d4dbb7764 Revert "clientbot: return existing PUIDs in spawnClient if nick exists"
This reverts commit 113fbf9eb8d401d75b21507b5d0cca53b2454f7a.

Incomplete fix: would confuse virtual service bots and external users if they had the same nick.
2016-09-01 13:00:27 -07:00
James Lu
113fbf9eb8 clientbot: return existing PUIDs in spawnClient if nick exists
This fixes some silly UID duplication with FNC handling.
2016-08-31 23:22:24 -07:00
James Lu
ae63f72cf9 clientbot: decouple inbound and outbound nick changes 2016-08-31 23:12:45 -07:00
James Lu
9bda4094e4 clientbot: handle pre-auth FNC better
Closes #321.
2016-08-31 23:05:36 -07:00
James Lu
be960bf27b clientbot: handle numerics 463 to 465 as fatal error 2016-08-31 22:46:46 -07:00
James Lu
cf5898fb45 clientbot: only send updateClient() hooks if something changes
Closes #323.
2016-08-31 22:32:12 -07:00
James Lu
9b38ca7d68 automode: join modebot client on setacc
Reported by kevin via IRC.
2016-08-31 22:23:55 -07:00
James Lu
a25a6b7e8d Merge branch 'master' into devel 2016-08-31 22:08:47 -07:00
James Lu
a99cab9130 Merge branch 'master' into devel 2016-08-31 22:07:32 -07:00
James Lu
eeca2c080d README: update webchat link 2016-08-31 22:07:13 -07:00
James Lu
c4a68d2f73 README: fix formatting 2016-08-31 22:05:32 -07:00
James Lu
02f3c71e8b README: more thorough description of branches & installation 2016-08-31 22:03:43 -07:00
James Lu
e85cc684a1 relay_cb: respect allow_clientbot_pms in 'rpm' (#292) 2016-08-31 19:51:52 -07:00
James Lu
7eb5e59842 relay_cb: implement outgoing PMs from clientbot networks via an 'rpm' command
Closes #292.
2016-08-31 19:48:17 -07:00
James Lu
7817898c14 relay_cb: distinguish between PM and private notice, switch to irc.msg() 2016-08-31 18:30:51 -07:00
James Lu
0a436cdf4c Irc: make loopback hook in msg() optional 2016-08-31 18:28:13 -07:00
James Lu
e06a6ae5bd example-conf: add an autoperform example 2016-08-31 18:10:51 -07:00
James Lu
663d03ed2c relay/clientbot: begin work on outgoing PMs to clientbot users (#318)
This still needs to implement PMs going the other way around, and should eventually distinguish between PMs and private notices.
2016-08-31 14:08:28 -07:00
James Lu
7bae4062b1 automode: log successful sync/add/remove/clear events
Closes #320.
2016-08-31 13:32:50 -07:00
James Lu
e903a8226a permissions: move admin login clause into checkPermissions()
This is more persistent.
2016-08-31 13:32:42 -07:00
James Lu
53de6542f6 clientbot: in nick(), make sure irc.pseudoclient exists 2016-08-27 20:39:35 -07:00
James Lu
42a104534a unreal: ensure type safety from last commit 2016-08-27 19:19:47 -07:00
James Lu
8f8cd95395 unreal: use umode +xt instead of SETHOST in spawnClient
This is to ensure vHosts for all PyLink clients are respected.
2016-08-27 19:14:37 -07:00
James Lu
1c4cb94a13 relay: block /OJOIN in claim 2016-08-27 19:09:02 -07:00
James Lu
ae94bec6b8 protocols: add a chandata key to SJOIN hook payloads 2016-08-27 18:56:36 -07:00
James Lu
3c7b201f57 protocols: rename 'oldchan' in MODE payloads to 'chandata' 2016-08-27 17:46:35 -07:00
James Lu
87757a60a3 Irc: rename 'chandata' in SQUIT payload to 'channeldata' 2016-08-27 17:42:07 -07:00
James Lu
c68c941c1d Irc.msg: break on empty text strings
Closes #306.
2016-08-27 09:52:01 -07:00
James Lu
9e7d0a50ca Revert "ServiceBot: fall back to a space in 'help' when stripped text is empty"
This reverts commit 7a3c8ab637d9b49e5b9dca5a3f4ec8c226b85573.
2016-08-27 09:49:59 -07:00
James Lu
556b388a4e core: Rehashable permissions; import coremods on start 2016-08-25 12:07:55 -07:00
James Lu
6af8e77ee1 permissions: apply add/removeDefaultPermissions on the right list 2016-08-25 12:07:36 -07:00
James Lu
f890ddac1b permissions, automode: work on default permissions & add example permissions config (#190)
- Fix possible type errors in add/removeDefaultPermissions by converting permlist values to sets.
- Fix wrong permission string being checked in automode.<command>.#channel
- automode: register and unregister default permissions on load/unload.
- permissions: add an 'also_show' argument to checkPermissions(), to display alternative permissions that weren't directly checked.
2016-08-25 11:45:57 -07:00
James Lu
03a780f397 automode: reorder functions in a way that makes more sense 2016-08-25 11:10:55 -07:00
James Lu
06cbbbb019 ServiceBot: display the NotAuthorizedError argument raw 2016-08-25 00:58:19 -07:00
James Lu
b4b772354c permissions: fix inverted permissions list lookup 2016-08-25 00:58:19 -07:00
James Lu
104c0cef4b automode: switch from irc.checkAuthenticated to new-style permissions 2016-08-25 00:58:19 -07:00
James Lu
91e39b7df9 WIP Permissions API (#190) 2016-08-25 00:45:05 -07:00
James Lu
5908776a86 API CHANGE: Rename NotAuthenticatedError -> NotAuthorizedError 2016-08-25 00:43:44 -07:00
James Lu
7d2b22630d ServiceBot: display custom error messages for NotAuthenticatedError 2016-08-25 00:41:53 -07:00
James Lu
945818e1d8 Refresh protocol module spec for 0.10 (#284)
Reword various things, and update links to source code references. Also, mention the following:

- conf_keys validation (#282)
- Topic acceptance rules (or rather, the lack thereof: #277)
2016-08-24 23:25:36 -07:00
James Lu
3e2c6ea1b7 launcher: show VCS version in "pylink -v"
(cherry picked from commit a04bf6119e69343f69d37f75e1382d99f2c952db)
2016-08-24 22:58:19 -07:00
James Lu
441f2244a9 relay: fix overzealous host normalization due to variable replacement during iteration
(cherry picked from commit 09c98f66ff34d98127b8fa82aa91b6ae70f491b2)
2016-08-24 22:58:19 -07:00
James Lu
814c714145 clientbot: fix nick() using the wrong arguments
I should learn to proofread...

Also, outgoing NICK changes should not implicitly update the state; we should wait for the IRCd's acknowledgement instead.
2016-08-24 22:54:53 -07:00
James Lu
a04bf6119e launcher: show VCS version in "pylink -v" 2016-08-23 07:28:48 -07:00
James Lu
09c98f66ff relay: fix overzealous host normalization due to variable replacement during iteration 2016-08-21 18:06:53 -07:00
James Lu
29bfe108fe clientbot: only send SQUIT payload if nicks are affected 2016-08-21 17:43:10 -07:00
James Lu
5444b808b1 Revert "relay_clientbot: lowercase network name (stylistic choice)"
This reverts commit 42da216f5db4f11fe9305b1025e59f9f3aa5a7ec.
2016-08-21 17:28:33 -07:00
James Lu
a546bae341 Irc: make throttle time configurable per server (defaults to 0.01s) 2016-08-21 17:25:09 -07:00
James Lu
7a5b64bdc9 Irc: implement basic message queueing (1 message sent per X seconds)
Ref #293.
2016-08-21 17:12:51 -07:00
James Lu
e171708aba PyLink 0.9.2 2016-08-21 16:58:07 -07:00
James Lu
689c0dd45b relay: {} are valid nick chars too...
(cherry picked from commit b572c58223c6d9b2e7ec0045f809767bebffe625)
2016-08-20 20:54:42 -07:00
109 changed files with 20248 additions and 7123 deletions

View File

@ -1,13 +0,0 @@
# This file is used for the configuration of https://codeclimate.com/github/GLolol/PyLink/
# You needn't change this if you're running your own copy of PyLink.
engines:
duplication:
enabled: true
config:
languages:
- python
pep8:
enabled: true
fixme:
enabled: true

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"),
]

4
.gitattributes vendored Normal file
View File

@ -0,0 +1,4 @@
* eol=lf
*.png binary
*.jpg binary

6
.gitignore vendored
View File

@ -3,15 +3,20 @@
!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
env/ env/
build/ build/
__pycache__/ __pycache__/
.idea/
*.py[cod] *.py[cod]
*.bak *.bak
*~ *~
*#
*.save* *.save*
*.db *.db
*.pid *.pid
@ -19,3 +24,4 @@ __pycache__/
.eggs .eggs
*.egg-info/ *.egg-info/
dist/ dist/
log/

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

View File

@ -1,2 +1,7 @@
James Lu <GLolol@overdrivenetworks.com> <GLolol1@hotmail.com> James Lu <james@overdrivenetworks.com> <GLolol@overdrivenetworks.com>
James Lu <GLolol@overdrivenetworks.com> <GLolol@overdrive.pw> James Lu <james@overdrivenetworks.com> <bitflip3+github@gmail.com>
James Lu <james@overdrivenetworks.com> <GLolol1@hotmail.com>
James Lu <james@overdrivenetworks.com> <GLolol@overdrive.pw>
Ken Spencer <ken@electrocode.net> <kspencer@electrocode.net>
Ken Spencer <ken@electrocode.net> <iota@electrocode.net>
Ken Spencer <ken@electrocode.net> <iota@e-code.in>

4
.pylintrc Normal file
View File

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

View File

@ -1,4 +1,5 @@
The following people have contributed substantially to PyLink: The following people have contributed substantially to PyLink:
James Lu <glolol@overdrivenetworks.com> James Lu <james@overdrivenetworks.com>
Daniel Oaks <daniel@danieloaks.net> Daniel Oaks <daniel@danieloaks.net>
Ken Spencer <iota@electrocode.net>

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"]

136
README.md
View File

@ -1,50 +1,83 @@
# PyLink # PyLink IRC Services
## 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/)
[![Docker image version](https://img.shields.io/docker/v/jlu5/pylink/latest?label=docker)](https://hub.docker.com/r/jlu5/pylink)
[![Supported Python versions](https://img.shields.io/badge/python-3.7%20and%20later-50e)](https://www.python.org/downloads/)
-->
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 and gateway to IRC. 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 ## Getting help
**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/GLolol/PyLink/issues). Pull requests are open if you'd like to contribute, though new stuff generally goes to the **devel** branch. Please report any bugs you find to the [issue tracker](https://github.com/PyLink/PyLink/issues). Pull requests are likewise welcome.
You can also find support via our IRC channels: `#PyLink @ irc.overdrivenetworks.com `([webchat](https://webchat.overdrivenetworks.com/?channels=PyLink,dev)) or `#PyLink @ chat.freenode.net`. Ask your questions and be patient for a response.
## Installation ## Installation
### Installing from source (recommended) ### Pre-requisites
* 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.
First, make sure the following dependencies are met: If you are a developer and want to help make PyLink more portable, patches are welcome.
* Python 3.4+ ### Installing from source
* Setuptools (`pip3 install setuptools`)
* PyYAML (`pip3 install pyyaml`)
* [ircmatch](https://github.com/mammon-ircd/ircmatch) (`pip3 install ircmatch`)
* *For the servprotect plugin*: [expiringdict](https://github.com/mailgun/expiringdict) (install this from source; installation is broken in pip due to [mailgun/expiringdict#13](https://github.com/mailgun/expiringdict/issues/13))
1) Clone the repository: `git clone https://github.com/GLolol/PyLink && cd PyLink` 1) First, make sure the following dependencies are met:
2) Install PyLink using `python3 setup.py install` (global install) or `python3 setup.py install --user` (local install) * Setuptools (`pip3 install setuptools`)
- Note: `--user` is a *literal* string; *do not* replace it with your username. * PyYAML (`pip3 install pyyaml`)
* cachetools (`pip3 install cachetools`)
* *For hashed password support*: Passlib >= 1.7.0 (`pip3 install passlib`)
* *For Unicode support in Relay*: unidecode (`pip3 install Unidecode`)
* *For extended PID file tracking (i.e. removing stale PID files after a crash)*: psutil (`pip3 install psutil`)
### Installing via PyPI 2) Clone the repository: `git clone https://github.com/PyLink/PyLink && cd PyLink`
1) Make sure you're running the right pip command: on most distros, pip for Python3 uses the command `pip3`. - Previously there was a *devel* branch for testing versions of PyLink - this practice has since been discontinued.
3) 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.
* **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)
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/GLolol/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`).
@ -54,24 +87,55 @@ First, make sure the following dependencies are met:
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](http://charybdis.io/) (3.5+ / git master) - module `ts6` * [InspIRCd](http://www.inspircd.org/) (2.0 - 3.x) - module `inspircd`
* [InspIRCd](http://www.inspircd.org/) 2.0.x - module `inspircd` - Set the `target_version` option to `insp3` to target InspIRCd 3.x (default), or `insp20` to target InspIRCd 2.0 (legacy).
- For vHost setting to work, `m_chghost.so` must be loaded. - For vHost setting to work, `m_chghost.so` must be loaded. For ident and realname changing support, `m_chgident.so` and `m_chgname.so` must be loaded respectively.
- Supported channel, user, and prefix modes are negotiated on connect, but hotloading modules that change these is not supported. After changing module configuration, it is recommended to SQUIT PyLink to force a protocol renegotiation.
* [Nefarious IRCu](https://github.com/evilnet/nefarious2) (2.0.0+) - module `p10`
- Note: Both account cloaks (user and oper) and hashed IP cloaks are optionally supported (`HOST_HIDING_STYLE` settings 0 to 3). Make sure you configure PyLink to match your IRCd settings.
* [UnrealIRCd](https://www.unrealircd.org/) (4.2.x - 5.0.x) - module `unreal`
- 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.
* [UnrealIRCd](https://www.unrealircd.org/) 4.x - module `unreal`
- Linking to UnrealIRCd 3.2 servers is only supported 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.
* [Elemental-IRCd](https://github.com/Elemental-IRCd/elemental-ircd) (6.6.x / git master) - module `ts6` * [charybdis](https://github.com/charybdis-ircd/charybdis) (3.5+) - module `ts6`
* [InspIRCd](http://www.inspircd.org/) 2.2 (git master) - module `inspircd` - For KLINE support to work, a `shared{}` block should be added for PyLink on all servers.
* [IRCd-Hybrid](http://www.ircd-hybrid.org/) (8.2.x / svn trunk) - module `hybrid` * [ChatIRCd](http://www.chatlounge.net/software) (1.2.x / git master) - module `ts6`
- Note: 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 be added for PyLink on all servers.
* [juno-ircd](https://github.com/cooper/yiria) (11.x / janet) - module `ts6` (see [configuration example](https://github.com/cooper/juno/blob/master/doc/ts6.md#pylink)) * [juno-ircd](https://github.com/cooper/juno) (13.x / ava) - module `ts6` (see [configuration example](https://github.com/cooper/juno/blob/master/doc/ts6.md#pylink))
* [Nefarious IRCu](https://github.com/evilnet/nefarious2) (2.0.0+) - module `nefarious` * [ngIRCd](https://ngircd.barton.de/) (24+) - module `ngircd`
- 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. - For GLINEs to propagate, the `AllowRemoteOper` option must be enabled in ngIRCd.
- For optimal functionality (mode overrides in relay, etc.), consider adding `UWorld{}` blocks / U-lines for every server that PyLink spawns. - `+` (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.
Other TS6 and P10 variations may work, but are not officially 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`
- Because bircd disallows BURST after ENDBURST for regular servers, U-lines are required for all PyLink servers. Fortunately, wildcards are supported in U-lines, so you can add something along the lines of `U:<your pylink server>:` and `U:*.relay:` (adjust accordingly for your relay server suffix).
- Use `ircd: snircd` as the target IRCd.
- Halfops, `sethost` (`+h`), and account-based cloaking (`VHostStyle=1`) are supported. Crypted IPs and static hosts (`VHostStyle` 2 and 3) are NOT.
* [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.
* [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 KLINE support to work, a `shared{}` block should also be added for PyLink on all servers.
* [ircd-ratbox](http://www.ratbox.org/) (3.x) - module `ts6`
- Host changing is not supported.
- On ircd-ratbox, all known IPs of users will be shown in `/whois`, even if the client is 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.
* [IRCu](http://coder-com.undernet.org/) (u2.10.12.16+) - module `p10`
- Host changing (changehost, relay) is not supported.
* [snircd](https://development.quakenet.org/) (1.3.x+) - module `p10`
- Outbound host changing (i.e. for the `changehost` plugin) is not supported.
### Clientbot
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`.
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).

File diff suppressed because it is too large Load Diff

View File

@ -1 +1 @@
0.10-alpha1 3.1.0

2820
classes.py

File diff suppressed because it is too large Load Diff

108
conf.py
View File

@ -8,13 +8,22 @@ It provides simple checks for validating and loading YAML-format configurations
try: try:
import yaml import yaml
except ImportError: except ImportError:
raise ImportError("Please install PyYAML and try again.") raise ImportError("PyLink requires PyYAML to function; please install it and try again.")
import logging
import os.path
import sys 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):
"""Error when config conditions aren't met."""
conf = {'bot': conf = {'bot':
{ {
'nick': 'PyLink', 'nick': 'PyLink',
@ -24,7 +33,7 @@ conf = {'bot':
}, },
'logging': 'logging':
{ {
'stdout': 'INFO' 'console': 'INFO'
}, },
'servers': 'servers':
# Wildcard defaultdict! This means that # Wildcard defaultdict! This means that
@ -41,35 +50,104 @@ conf = {'bot':
'sidrange': '0##' 'sidrange': '0##'
}) })
} }
conf['pylink'] = conf['bot']
confname = 'unconfigured' confname = 'unconfigured'
def validateConf(conf): def validate(condition, errmsg):
"""Raises ConfigurationError with errmsg unless the given condition is met."""
if not condition:
raise ConfigurationError(errmsg)
def _log(level, text, *args, logger=None, **kwargs):
if logger:
logger.log(level, text, *args, **kwargs)
else:
world._log_queue.append((level, text))
def _validate_conf(conf, logger=None):
"""Validates a parsed configuration dict.""" """Validates a parsed configuration dict."""
assert type(conf) == dict, "Invalid configuration given: should be type dict, not %s." % type(conf).__name__ validate(isinstance(conf, dict),
"Invalid configuration given: should be type dict, not %s."
% type(conf).__name__)
for section in ('bot', 'servers', 'login', 'logging'): if 'pylink' in conf and 'bot' in conf:
assert conf.get(section), "Missing %r section in config." % section _log(logging.WARNING, "Since PyLink 1.2, the 'pylink:' and 'bot:' configuration sections have been condensed "
"into one. You should merge any options under these sections into one 'pylink:' block.", logger=logger)
assert type(conf['login'].get('password')) == type(conf['login'].get('user')) == str and \ new_block = conf['bot'].copy()
conf['login']['password'] != "changeme", "You have not set the login details correctly!" new_block.update(conf['pylink'])
conf['bot'] = conf['pylink'] = new_block
elif 'pylink' in conf:
conf['bot'] = conf['pylink']
elif 'bot' in conf:
conf['pylink'] = conf['bot']
# TODO: add a migration warning in the next release.
for section in ('pylink', 'servers', 'login', 'logging'):
validate(conf.get(section), "Missing %r section in config." % section)
# Make sure at least one form of authentication is valid.
# Also we'll warn them that login:user/login:password is deprecated
if conf['login'].get('password') or conf['login'].get('user'):
_log(logging.WARNING, "The 'login:user' and 'login:password' options are deprecated since PyLink 1.1. "
"Please switch to the new 'login:accounts' format as outlined in the example config.", logger=logger)
old_login_valid = isinstance(conf['login'].get('password'), str) and isinstance(conf['login'].get('user'), str)
newlogins = conf['login'].get('accounts', {})
validate(old_login_valid or newlogins, "No accounts were set, aborting!")
for account, block in newlogins.items():
validate(isinstance(account, str), "Bad username format %s" % account)
validate(isinstance(block.get('password'), str), "Bad password %s for account %s" % (block.get('password'), account))
validate(conf['login'].get('password') != "changeme", "You have not set the login details correctly!")
if newlogins and not old_login_valid:
validate(conf.get('permissions'), "New-style accounts enabled but no permissions block was found. You will not be able to administrate your PyLink instance!")
if conf['logging'].get('stdout'):
_log(logging.WARNING, 'The log:stdout option is deprecated since PyLink 1.2 in favour of '
'(a more correctly named) log:console. Please update your '
'configuration accordingly!', logger=logger)
return conf return conf
def loadConf(filename, errors_fatal=True): def load_conf(filename, errors_fatal=True, logger=None):
"""Loads a PyLink configuration file from the filename given.""" """Loads a PyLink configuration file from the filename given."""
global confname, conf, fname global confname, conf, fname
# Note: store globally the last loaded conf filename, for REHASH in coremods/control.
fname = filename fname = filename
confname = filename.split('.', 1)[0] # For the internal config name, strip off any .yml extensions and absolute paths
confname = os.path.splitext(os.path.basename(filename))[0]
try: try:
with open(filename, 'r') as f: with open(filename, 'r') as f:
conf = yaml.load(f) conf = yaml.safe_load(f)
conf = validateConf(conf) conf = _validate_conf(conf, logger=logger)
except Exception as e: except Exception as e:
print('ERROR: Failed to load config from %r: %s: %s' % (filename, type(e).__name__, e), file=sys.stderr) e = 'Failed to load config from %r: %s: %s' % (filename, type(e).__name__, e)
print(' Users upgrading from users < 0.9-alpha1 should note that the default configuration has been renamed to *pylink.yml*, not *config.yml*', file=sys.stderr)
if logger: # Prefer using the Python logger when available
logger.exception(e)
else: # Otherwise, fall back to a print() call.
print('ERROR: %s' % e, file=sys.stderr)
if errors_fatal: if errors_fatal:
sys.exit(4) sys.exit(1)
raise raise
else: else:
return conf return conf
def get_database_name(dbname):
"""
Returns a database filename with the given base DB name appropriate for the
current PyLink instance.
This returns '<dbname>.db' if the running config name is PyLink's default
(pylink.yml), and '<dbname>-<config name>.db' for anything else. For example,
if this is called from an instance running as 'pylink testing.yml', it
would return '<dbname>-testing.db'."""
if confname != 'pylink':
dbname += '-%s' % confname
dbname += '.db'
return dbname

View File

@ -1,2 +1,3 @@
# Service support has to be imported first, so that utils.add_cmd works # Note: Service support has to be imported first, so that utils.add_cmd() works for corecommands,
from . import service_support, control, handlers, corecommands, exttargets # etc.
from . import service_support, permissions, control, handlers, corecommands, exttargets

View File

@ -1,11 +1,18 @@
""" """
control.py - Implements SHUTDOWN and REHASH functionality. control.py - Implements SHUTDOWN and REHASH functionality.
""" """
import signal import atexit
import os 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, classes
from pylinkirc.log import log, makeFileLogger, stopFileLoggers
def remove_network(ircobj): def remove_network(ircobj):
"""Removes a network object from the pool.""" """Removes a network object from the pool."""
@ -14,78 +21,142 @@ def remove_network(ircobj):
ircobj.disconnect() ircobj.disconnect()
del world.networkobjects[ircobj.name] del world.networkobjects[ircobj.name]
def _shutdown(irc=None): def _print_remaining_threads():
"""Shuts down the Pylink daemon.""" log.debug('shutdown(): Remaining threads: %s', ['%s/%s' % (t.name, t.ident) for t in threading.enumerate()])
def _remove_pid():
pidfile = "%s.pid" % conf.confname
if world._should_remove_pid:
# Remove our pid file.
log.info("Removing PID file %r.", pidfile)
try:
os.remove(pidfile)
except OSError:
log.exception("Failed to remove PID file %r, ignoring..." % pidfile)
else:
log.debug('Not removing PID file %s as world._should_remove_pid is False.' % pidfile)
def _kill_plugins(irc=None):
if not world.plugins:
# No plugins were loaded or we were in a pre-initialized state, ignore.
return
log.info("Shutting down plugins.")
for name, plugin in world.plugins.items(): for name, plugin in world.plugins.items():
# Before closing connections, tell all plugins to shutdown cleanly first. # Before closing connections, tell all plugins to shutdown cleanly first.
if hasattr(plugin, 'die'): if hasattr(plugin, 'die'):
log.debug('coremods.control: Running die() on plugin %s due to shutdown.', name) log.debug('coremods.control: Running die() on plugin %s due to shutdown.', name)
try: try:
plugin.die(irc) plugin.die(irc=irc)
except: # But don't allow it to crash the server. except: # But don't allow it to crash the server.
log.exception('coremods.control: Error occurred in die() of plugin %s, skipping...', name) log.exception('coremods.control: Error occurred in die() of plugin %s, skipping...', name)
# We use atexit to register certain functions so that when PyLink cleans up after itself if it
# shuts down because all networks have been disconnected.
atexit.register(_remove_pid)
atexit.register(_kill_plugins)
def shutdown(irc=None):
"""Shuts down the Pylink daemon."""
if world.shutting_down.is_set(): # We froze on shutdown last time, so immediately abort.
_print_remaining_threads()
raise KeyboardInterrupt("Forcing shutdown.")
world.shutting_down.set()
# HACK: run the _kill_plugins trigger with the current IRC object. XXX: We should really consider removing this
# argument, since no plugins actually use it to do anything.
atexit.unregister(_kill_plugins)
_kill_plugins(irc=irc)
# Remove our main PyLink bot as well. # Remove our main PyLink bot as well.
utils.unregisterService('pylink') utils.unregister_service('pylink')
for ircobj in world.networkobjects.copy().values(): for ircobj in world.networkobjects.copy().values():
# Disconnect all our networks. # Disconnect all our networks.
remove_network(ircobj) try:
remove_network(ircobj)
except NotImplementedError:
continue
def sigterm_handler(signo, stack_frame): 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.")
_print_remaining_threads()
# Done.
def _sigterm_handler(signo, stack_frame):
"""Handles SIGTERM and SIGINT gracefully by shutting down the PyLink daemon.""" """Handles SIGTERM and SIGINT gracefully by shutting down the PyLink daemon."""
log.info("Shutting down on signal %s." % signo) log.info("Shutting down on signal %s." % signo)
_shutdown() shutdown()
signal.signal(signal.SIGTERM, sigterm_handler) signal.signal(signal.SIGTERM, _sigterm_handler)
signal.signal(signal.SIGINT, sigterm_handler) signal.signal(signal.SIGINT, _sigterm_handler)
def _rehash(): def rehash():
"""Rehashes the PyLink daemon.""" """Rehashes the PyLink daemon."""
log.info('Reloading PyLink configuration...')
old_conf = conf.conf.copy() old_conf = conf.conf.copy()
fname = conf.fname fname = conf.fname
new_conf = conf.loadConf(fname, errors_fatal=False) new_conf = conf.load_conf(fname, errors_fatal=False, logger=log)
new_conf = conf.validateConf(new_conf)
conf.conf = new_conf conf.conf = new_conf
# Reset any file logger options. # Reset any file logger options.
stopFileLoggers() _stop_file_loggers()
files = new_conf['logging'].get('files') files = new_conf['logging'].get('files')
if files: if files:
for filename, config in files.items(): for filename, config in files.items():
makeFileLogger(filename, config.get('loglevel')) _make_file_logger(filename, config.get('loglevel'))
log.debug('rehash: updating console log level')
world.console_handler.setLevel(_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)
else: else:
ircobj.conf = new_conf # XXX: we should really just add abstraction to Irc to update config settings...
ircobj.serverdata = new_conf['servers'][network] ircobj.serverdata = new_conf['servers'][network]
ircobj.botdata = new_conf['bot']
ircobj.autoconnect_active_multiplier = 1
# Clear the IRC object's channel loggers and replace them with # Clear the IRC object's channel loggers and replace them with
# new ones by re-running logSetup(). # new ones by re-running log_setup().
while ircobj.loghandlers: while ircobj.loghandlers:
log.removeHandler(ircobj.loghandlers.pop()) log.removeHandler(ircobj.loghandlers.pop())
ircobj.logSetup() ircobj.log_setup()
# TODO: update file loggers here too. utils._reset_module_dirs()
for network, sdata in new_conf['servers'].items(): for network, sdata in new_conf['servers'].items():
# Connect any new networks or disconnected networks if they aren't already. # Connect any new networks or disconnected networks if they aren't already.
if (network not in world.networkobjects) or (not world.networkobjects[network].connection_thread.is_alive()): if network not in world.networkobjects:
proto = utils.getProtocolModule(sdata['protocol']) try:
world.networkobjects[network] = classes.Irc(network, proto, new_conf) proto = utils._get_protocol_module(sdata['protocol'])
# API note: 2.0.x style of starting network connections
world.networkobjects[network] = newirc = proto.Class(network)
newirc.connect()
except:
log.exception('Failed to initialize network %r, skipping it...', network)
log.info('Finished reloading PyLink configuration.')
if os.name == 'posix': if os.name == 'posix':
# Only register SIGHUP on *nix. # Only register SIGHUP/SIGUSR1 on *nix.
def sighup_handler(_signo, _stack_frame): def _sighup_handler(signo, _stack_frame):
"""Handles SIGHUP by rehashing the PyLink daemon.""" """Handles SIGHUP/SIGUSR1 by rehashing the PyLink daemon."""
log.info("SIGHUP received, reloading config.") log.info("Signal %s received, reloading config." % signo)
_rehash() rehash()
signal.signal(signal.SIGHUP, sighup_handler) signal.signal(signal.SIGHUP, _sighup_handler)
signal.signal(signal.SIGUSR1, _sighup_handler)

View File

@ -2,66 +2,37 @@
corecommands.py - Implements core PyLink commands. corecommands.py - Implements core PyLink commands.
""" """
# Get the package name that plugins are stored under.
plugin_root = __name__.split('.')[0] + '.plugins.'
import gc import gc
import sys import sys
import importlib
from . import control 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.
@utils.add_cmd
def identify(irc, source, args):
"""<username> <password>
Logs in to PyLink using the configured administrator account."""
if utils.isChannel(irc.called_in):
irc.reply('Error: This command must be sent in private. '
'(Would you really type a password inside a channel?)')
return
try:
username, password = args[0], args[1]
except IndexError:
irc.reply('Error: Not enough arguments.')
return
# Usernames are case-insensitive, passwords are NOT.
if username.lower() == conf.conf['login']['user'].lower() and password == conf.conf['login']['password']:
realuser = conf.conf['login']['user']
irc.users[source].account = realuser
irc.reply('Successfully logged in as %s.' % realuser)
log.info("(%s) Successful login to %r by %s",
irc.name, username, irc.getHostmask(source))
else:
irc.reply('Error: Incorrect credentials.')
u = irc.users[source]
log.warning("(%s) Failed login to %r from %s",
irc.name, username, irc.getHostmask(source))
@utils.add_cmd @utils.add_cmd
def shutdown(irc, source, args): def shutdown(irc, source, args):
"""takes no arguments. """takes no arguments.
Exits PyLink by disconnecting all networks.""" Exits PyLink by disconnecting all networks."""
irc.checkAuthenticated(source, allowOper=False) permissions.check_permissions(irc, source, ['core.shutdown'])
u = irc.users[source] log.info('(%s) SHUTDOWN requested by %s, exiting...', irc.name, irc.get_hostmask(source))
control.shutdown(irc=irc)
log.info('(%s) SHUTDOWN requested by "%s!%s@%s", exiting...', irc.name, u.nick,
u.ident, u.host)
control._shutdown(irc)
@utils.add_cmd @utils.add_cmd
def load(irc, source, args): def load(irc, source, args):
"""<plugin name>. """<plugin name>.
Loads a plugin from the plugin folder.""" Loads a plugin from the plugin folder."""
irc.checkAuthenticated(source, allowOper=False) # Note: reload capability is acceptable here, because all it actually does is call
# load after unload.
permissions.check_permissions(irc, source, ['core.load', 'core.reload'])
try: try:
name = args[0] name = args[0]
except IndexError: except IndexError:
@ -70,9 +41,9 @@ def load(irc, source, args):
if name in world.plugins: if name in world.plugins:
irc.reply("Error: %r is already loaded." % name) irc.reply("Error: %r is already loaded." % name)
return return
log.info('(%s) Loading plugin %r for %s', irc.name, name, irc.getHostmask(source)) log.info('(%s) Loading plugin %r for %s', irc.name, name, irc.get_hostmask(source))
try: try:
world.plugins[name] = pl = utils.loadPlugin(name) world.plugins[name] = pl = utils._load_plugin(name)
except ImportError as e: except ImportError as e:
if str(e) == ('No module named %r' % name): if str(e) == ('No module named %r' % name):
log.exception('Failed to load plugin %r: The plugin could not be found.', name) log.exception('Failed to load plugin %r: The plugin could not be found.', name)
@ -82,7 +53,7 @@ def load(irc, source, args):
else: else:
if hasattr(pl, 'main'): if hasattr(pl, 'main'):
log.debug('Calling main() function of plugin %r', pl) log.debug('Calling main() function of plugin %r', pl)
pl.main(irc) pl.main(irc=irc)
irc.reply("Loaded plugin %r." % name) irc.reply("Loaded plugin %r." % name)
@utils.add_cmd @utils.add_cmd
@ -90,7 +61,8 @@ def unload(irc, source, args):
"""<plugin name>. """<plugin name>.
Unloads a currently loaded plugin.""" Unloads a currently loaded plugin."""
irc.checkAuthenticated(source, allowOper=False) permissions.check_permissions(irc, source, ['core.unload', 'core.reload'])
try: try:
name = args[0] name = args[0]
except IndexError: except IndexError:
@ -99,10 +71,10 @@ def unload(irc, source, args):
# Since we're using absolute imports in 0.9.x+, the module name differs from the actual plugin # Since we're using absolute imports in 0.9.x+, the module name differs from the actual plugin
# name. # name.
modulename = plugin_root + name modulename = utils.PLUGIN_PREFIX + name
if name in world.plugins: if name in world.plugins:
log.info('(%s) Unloading plugin %r for %s', irc.name, name, irc.getHostmask(source)) log.info('(%s) Unloading plugin %r for %s', irc.name, name, irc.get_hostmask(source))
pl = world.plugins[name] pl = world.plugins[name]
log.debug('sys.getrefcount of plugin %s is %s', pl, sys.getrefcount(pl)) log.debug('sys.getrefcount of plugin %s is %s', pl, sys.getrefcount(pl))
@ -122,18 +94,20 @@ def unload(irc, source, args):
del world.services['pylink'].commands[cmdname] del world.services['pylink'].commands[cmdname]
# Remove any command hooks set by the plugin. # Remove any command hooks set by the plugin.
for hookname, hookfuncs in world.hooks.copy().items(): for hookname, hookpairs in world.hooks.copy().items():
for hookfunc in hookfuncs: for hookpair in hookpairs:
hookfunc = hookpair[1]
if hookfunc.__module__ == modulename: if hookfunc.__module__ == modulename:
world.hooks[hookname].remove(hookfunc) log.debug('Trying to remove hook func %s (%s) from plugin %s', hookfunc, hookname, modulename)
world.hooks[hookname].remove(hookpair)
# If the hookfuncs list is empty, remove it. # If the hookfuncs list is empty, remove it.
if not hookfuncs: if not hookpairs:
del world.hooks[hookname] del world.hooks[hookname]
# Call the die() function in the plugin, if present. # Call the die() function in the plugin, if present.
if hasattr(pl, 'die'): if hasattr(pl, 'die'):
try: try:
pl.die(irc) pl.die(irc=irc)
except: # But don't allow it to crash the server. except: # But don't allow it to crash the server.
log.exception('(%s) Error occurred in die() of plugin %s, skipping...', irc.name, pl) log.exception('(%s) Error occurred in die() of plugin %s, skipping...', irc.name, pl)
@ -163,6 +137,8 @@ def reload(irc, source, args):
except IndexError: except IndexError:
irc.reply("Error: Not enough arguments. Needs 1: plugin name.") irc.reply("Error: Not enough arguments. Needs 1: plugin name.")
return return
# Note: these functions do permission checks, so there are none needed here.
if unload(irc, source, args): if unload(irc, source, args):
load(irc, source, args) load(irc, source, args)
@ -171,13 +147,21 @@ def rehash(irc, source, args):
"""takes no arguments. """takes no arguments.
Reloads the configuration file for PyLink, (dis)connecting added/removed networks. Reloads the configuration file for PyLink, (dis)connecting added/removed networks.
Plugins must be manually reloaded."""
irc.checkAuthenticated(source, allowOper=False) Note: plugins must be manually reloaded."""
permissions.check_permissions(irc, source, ['core.rehash'])
try: try:
control._rehash() control.rehash()
except Exception as e: # Something went wrong, abort. except Exception as e: # Something went wrong, abort.
log.exception("Error REHASHing config: ")
irc.reply("Error loading configuration file: %s: %s" % (type(e).__name__, e)) irc.reply("Error loading configuration file: %s: %s" % (type(e).__name__, e))
return return
else: else:
irc.reply("Done.") irc.reply("Done.")
@utils.add_cmd
def clearqueue(irc, source, args):
"""takes no arguments.
Clears the outgoing text queue for the current connection."""
permissions.check_permissions(irc, source, ['core.clearqueue'])
irc._queue.queue.clear()

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.
@ -21,7 +24,7 @@ def account(irc, host, uid):
$account -> Returns True (a match) if the target is registered. $account -> Returns True (a match) if the target is registered.
$account:accountname -> Returns True if the target's account name matches the one given, and the $account:accountname -> Returns True if the target's account name matches the one given, and the
target is connected to the local network.. target is connected to the local network.
$account:accountname:netname -> Returns True if both the target's account name and origin $account:accountname:netname -> Returns True if both the target's account name and origin
network name match the ones given. network name match the ones given.
$account:*:netname -> Matches all logged in users on the given network. $account:*:netname -> Matches all logged in users on the given network.
@ -41,10 +44,10 @@ def account(irc, host, uid):
homenet, realuid) homenet, realuid)
return False return False
slogin = irc.toLower(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.toLower, 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):
@ -74,10 +77,10 @@ def ircop(irc, host, uid):
if len(groups) == 1: if len(groups) == 1:
# 1st scenario. # 1st scenario.
return irc.isOper(uid, allowAuthed=False) return irc.is_oper(uid)
else: else:
# 2nd scenario. Use matchHost (ircmatch) to match the opertype glob to the opertype. # 2nd scenario. Match the opertype glob to the opertype.
return irc.matchHost(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):
@ -93,10 +96,10 @@ def server(irc, host, uid):
log.debug('(%s) exttargets.server: groups to match: %s', irc.name, groups) log.debug('(%s) exttargets.server: groups to match: %s', irc.name, groups)
if len(groups) >= 2: if len(groups) >= 2:
sid = irc.getServer(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.matchHost(query, irc.getFriendlyName(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
@ -118,12 +121,16 @@ def channel(irc, host, uid):
except IndexError: # No channel given, abort. except IndexError: # No channel given, abort.
return False return False
if channel not in irc.channels:
# Channel doesn't even exist...
return False
if len(groups) == 2: if len(groups) == 2:
# Just #channel was given as query # Just #channel was given as query
return uid in irc.channels[channel].users return uid in irc.channels[channel].users
elif len(groups) >= 3: elif len(groups) >= 3:
# For things like #channel:op, check if the query is in the user's prefix modes. # For things like #channel:op, check if the query is in the user's prefix modes.
return (uid in irc.channels[channel].users) and (groups[2].lower() in irc.channels[channel].getPrefixModes(uid)) return (uid in irc.channels[channel].users) and (groups[2].lower() in irc.channels[channel].get_prefix_modes(uid))
@bind @bind
def pylinkacc(irc, host, uid): def pylinkacc(irc, host, uid):
@ -134,8 +141,8 @@ def pylinkacc(irc, host, uid):
$pylinkacc -> Returns True if the target is logged in to PyLink. $pylinkacc -> Returns True if the target is logged in to PyLink.
$pylinkacc:accountname -> Returns True if the target's PyLink login matches the one given. $pylinkacc:accountname -> Returns True if the target's PyLink login matches the one given.
""" """
login = irc.toLower(irc.users[uid].account) login = irc.to_lower(irc.users[uid].account)
groups = list(map(irc.toLower, host.split(':'))) groups = list(map(irc.to_lower, host.split(':')))
log.debug('(%s) exttargets.pylinkacc: groups to match: %s', irc.name, groups) log.debug('(%s) exttargets.pylinkacc: groups to match: %s', irc.name, groups)
if len(groups) == 1: if len(groups) == 1:
@ -144,3 +151,79 @@ def pylinkacc(irc, host, uid):
elif len(groups) == 2: elif len(groups) == 2:
# Second scenario. Return True if the user's login matches the one given. # Second scenario. Return True if the user's login matches the one given.
return login == groups[1] return login == groups[1]
@bind
def network(irc, host, uid):
"""
$network exttarget handler. This exttarget takes one argument: a network name, and returns
a match for all users on that network.
Note: network names are case sensitive.
"""
try:
targetnet = host.split(':')[1]
except IndexError: # No network arg given, bail.
return False
userobj = irc.users[uid]
if hasattr(userobj, 'remote'):
# User is a PyLink Relay client; set the correct network name.
homenet = userobj.remote[0]
else:
homenet = irc.name
return homenet == targetnet
# Note: "and" can't be a function name so we use this.
def exttarget_and(irc, host, uid):
"""
$and exttarget handler. This exttarget takes a series of exttargets (or hostmasks) joined with
a "+", and returns True if all sub exttargets match.
Examples:
$and:($ircop:*admin*+$network:ovd) -> Matches all opers on the network ovd.
$and:($account+$pylinkirc) -> Matches all users logged in to both services and PyLink.
$and:(*!*@localhost+$ircop) -> Matches all opers with the host `localhost`.
$and:(*!*@*.mibbit.com+!$ircop+!$account) -> Matches all mibbit users that aren't opered or logged in to services.
"""
targets = host.split(':', 1)[-1]
# For readability, this requires that the exttarget list be wrapped in brackets.
if not (targets.startswith('(') and targets.endswith(')')):
return False
targets = targets[1:-1]
targets = list(filter(None, targets.split('+')))
log.debug('exttargets_and: using raw subtargets list %r (original query=%r)', targets, host)
# Wrap every subtarget into irc.match_host and return True if all subtargets return True.
return all(map(lambda sub_exttarget: irc.match_host(sub_exttarget, uid), targets))
world.exttarget_handlers['and'] = exttarget_and
@bind
def realname(irc, host, uid):
"""
$realname exttarget handler. This takes one argument: a glob, which is compared case-insensitively to the user's real name.
Examples:
$realname:*James* -> matches anyone with "James" in their real name.
"""
groups = host.split(':')
if len(groups) >= 2:
return irc.match_text(groups[1], irc.users[uid].realname)
@bind
def service(irc, host, uid):
"""
$service exttarget handler. This takes one optional argument: a glob, which is compared case-insensitively to the target user's service name (if present).
Examples:
$service -> Matches any PyLink service bot.
$service:automode -> Matches the Automode service bot.
"""
if not irc.users[uid].service:
return False
groups = host.split(':')
if len(groups) >= 2:
return irc.match_text(groups[1], irc.users[uid].service)
return True # It *is* a service bot because of the check at the top.

View File

@ -3,27 +3,30 @@ 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']
user = irc.users.get(target) user = irc.users.get(target)
f = lambda num, source, text: irc.proto.numeric(irc.sid, num, source, text) f = lambda num, source, text: irc.numeric(irc.sid, num, source, text)
# Get the server that the target is on. # Get the server that the target is on.
server = irc.getServer(target) server = irc.get_server(target)
if user is None: # User doesn't exist 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:
nick = user.nick nick = user.nick
sourceisOper = ('o', None) in irc.users[source].modes source_is_oper = ('o', None) in irc.users[source].modes
sourceisBot = (irc.umodes.get('bot'), None) in irc.users[source].modes source_is_bot = (irc.umodes.get('bot'), None) in irc.users[source].modes
# Get the full network name. # Get the full network name.
netname = irc.serverdata.get('netname', irc.name) netname = irc.serverdata.get('netname', irc.name)
@ -35,7 +38,7 @@ def handle_whois(irc, source, command, args):
# 319: RPL_WHOISCHANNELS; Show public channels of the target, respecting # 319: RPL_WHOISCHANNELS; Show public channels of the target, respecting
# hidechans umodes for non-oper callers. # hidechans umodes for non-oper callers.
isHideChans = (irc.umodes.get('hidechans'), None) in user.modes isHideChans = (irc.umodes.get('hidechans'), None) in user.modes
if (not isHideChans) or (isHideChans and sourceisOper): if (not isHideChans) or (isHideChans and source_is_oper):
public_chans = [] public_chans = []
for chan in user.channels: for chan in user.channels:
c = irc.channels[chan] c = irc.channels[chan]
@ -44,13 +47,13 @@ def handle_whois(irc, source, command, args):
if ((irc.cmodes.get('secret'), None) in c.modes or \ if ((irc.cmodes.get('secret'), None) in c.modes or \
(irc.cmodes.get('private'), None) in c.modes) \ (irc.cmodes.get('private'), None) in c.modes) \
and not (sourceisOper or source in c.users): and not (source_is_oper or source in c.users):
continue continue
# Show the highest prefix mode like a regular IRCd does, if there are any. # Show the highest prefix mode like a regular IRCd does, if there are any.
prefixes = c.getPrefixModes(target) prefixes = c.get_prefix_modes(target)
if prefixes: if prefixes:
highest = prefixes[-1] highest = prefixes[0]
# Fetch the prefix mode letter from the named mode. # Fetch the prefix mode letter from the named mode.
modechar = irc.cmodes[highest] modechar = irc.cmodes[highest]
@ -74,22 +77,27 @@ def handle_whois(irc, source, command, args):
# 2) +H is set, but the caller is oper # 2) +H is set, but the caller is oper
# 3) +H is set, but whois_use_hideoper is disabled in config # 3) +H is set, but whois_use_hideoper is disabled in config
isHideOper = (irc.umodes.get('hideoper'), None) in user.modes isHideOper = (irc.umodes.get('hideoper'), None) in user.modes
if (not isHideOper) or (isHideOper and sourceisOper) or \ if (not isHideOper) or (isHideOper and source_is_oper) or \
(isHideOper and not irc.botdata.get('whois_use_hideoper', True)): (isHideOper and not conf.conf['pylink'].get('whois_use_hideoper', True)):
opertype = user.opertype
# Let's be gramatically correct. (If the opertype starts with a vowel, # Let's be gramatically correct. (If the opertype starts with a vowel,
# write "an Operator" instead of "a Operator") # write "an Operator" instead of "a Operator")
n = 'n' if user.opertype[0].lower() in 'aeiou' else '' n = 'n' if opertype[0].lower() in 'aeiou' else ''
# I want to normalize the syntax: PERSON is an OPERTYPE on NETWORKNAME. # Remove the "(on $network)" bit in relay oper types if the target network is the
# This is the only syntax InspIRCd supports, but for others it doesn't # same - this prevents duplicate text such as "jlu5/ovd is a Network Administrator
# really matter since we're handling the WHOIS requests by ourselves. # (on OVERdrive-IRC) on OVERdrive-IRC" from showing.
f(313, source, "%s :is a%s %s on %s" % (nick, n, user.opertype, netname)) # XXX: does this post-processing really belong here?
opertype = opertype.replace(' (on %s)' % irc.get_full_network_name(), '')
f(313, source, "%s :is a%s %s" % (nick, n, opertype))
# 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd to show user modes. # 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd to show user modes.
# Only show this to opers! # Only show this to opers!
if sourceisOper: if source_is_oper:
f(378, source, "%s :is connecting from %s@%s %s" % (nick, user.ident, user.realhost, user.ip)) f(378, source, "%s :is connecting from %s@%s %s" % (nick, user.ident, user.realhost, user.ip))
f(379, source, '%s :is using modes %s' % (nick, irc.joinModes(user.modes, sort=True))) f(379, source, '%s :is using modes %s' % (nick, irc.join_modes(user.modes, sort=True)))
# 301: used to show away information if present # 301: used to show away information if present
away_text = user.away away_text = user.away
@ -101,10 +109,14 @@ 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 jlu5 jlu5 1946 1499867833 :seconds idle, signon time
if irc.get_service_bot(target) and conf.conf['pylink'].get('whois_show_startup_time', True):
f(317, source, "%s 0 %s :seconds idle (placeholder), signon time" % (nick, irc.start_ts))
# Call custom WHOIS handlers via the PYLINK_CUSTOM_WHOIS hook, unless the # Call custom WHOIS handlers via the PYLINK_CUSTOM_WHOIS hook, unless the
# caller is marked a bot and the whois_show_extensions_to_bots option is False # caller is marked a bot and the whois_show_extensions_to_bots option is False
if (sourceisBot and conf.conf['bot'].get('whois_show_extensions_to_bots')) or (not sourceisBot): if (source_is_bot and conf.conf['pylink'].get('whois_show_extensions_to_bots')) or (not source_is_bot):
irc.callHooks([source, 'PYLINK_CUSTOM_WHOIS', {'target': target, 'server': server}]) irc.call_hooks([source, 'PYLINK_CUSTOM_WHOIS', {'target': target, 'server': server}])
else: else:
log.debug('(%s) coremods.handlers.handle_whois: skipping custom whois handlers because ' log.debug('(%s) coremods.handlers.handle_whois: skipping custom whois handlers because '
'caller %s is marked as a bot', irc.name, source) 'caller %s is marked as a bot', irc.name, source)
@ -119,15 +131,15 @@ def handle_mode(irc, source, command, args):
modes = args['modes'] modes = args['modes']
# If the sender is not a PyLink client, and the target IS a protected # If the sender is not a PyLink client, and the target IS a protected
# client, revert any forced deoper attempts. # client, revert any forced deoper attempts.
if irc.isInternalClient(target) and not irc.isInternalClient(source): if irc.is_internal_client(target) and not irc.is_internal_client(source):
if ('-o', None) in modes and (target == irc.pseudoclient.uid or not irc.isManipulatableClient(target)): if ('-o', None) in modes and (target == irc.pseudoclient.uid or not irc.is_manipulatable_client(target)):
irc.proto.mode(irc.sid, target, {('+o', None)}) irc.mode(irc.sid, target, {('+o', None)})
utils.add_hook(handle_mode, 'MODE') utils.add_hook(handle_mode, 'MODE')
def handle_operup(irc, source, command, args): def handle_operup(irc, source, command, args):
"""Logs successful oper-ups on networks.""" """Logs successful oper-ups on networks."""
otype = args.get('text', 'IRC Operator') otype = args.get('text', 'IRC Operator')
log.debug("(%s) Successful oper-up (opertype %r) from %s", irc.name, otype, irc.getHostmask(source)) log.debug("(%s) Successful oper-up (opertype %r) from %s", irc.name, otype, irc.get_hostmask(source))
irc.users[source].opertype = otype irc.users[source].opertype = otype
utils.add_hook(handle_operup, 'CLIENT_OPERED') utils.add_hook(handle_operup, 'CLIENT_OPERED')
@ -146,11 +158,58 @@ def handle_version(irc, source, command, args):
"""Handles requests for the PyLink server version.""" """Handles requests for the PyLink server version."""
# 351 syntax is usually "<server version>. <server hostname> :<anything else you want to add> # 351 syntax is usually "<server version>. <server hostname> :<anything else you want to add>
fullversion = irc.version() fullversion = irc.version()
irc.proto.numeric(irc.sid, 351, source, fullversion) irc.numeric(irc.sid, 351, source, fullversion)
utils.add_hook(handle_version, 'VERSION') utils.add_hook(handle_version, 'VERSION')
def handle_time(irc, source, command, args): def handle_time(irc, source, command, args):
"""Handles requests for the PyLink server time.""" """Handles requests for the PyLink server time."""
timestring = time.ctime() timestring = time.ctime()
irc.proto.numeric(irc.sid, 391, source, '%s :%s' % (irc.hostname(), timestring)) irc.numeric(irc.sid, 391, source, '%s :%s' % (irc.hostname(), timestring))
utils.add_hook(handle_time, 'TIME') utils.add_hook(handle_time, 'TIME')
def _state_cleanup_core(irc, source, channel):
"""
Handles PART and KICK on clientbot-like networks (where only the users and channels we see are available)
by deleting channels when we leave and users when they leave all shared channels.
"""
if irc.has_cap('visible-state-only'):
# Delete channels that we were removed from.
if irc.pseudoclient and source == irc.pseudoclient.uid:
log.debug('(%s) state_cleanup: removing channel %s since we have left', irc.name, channel)
del irc._channels[channel]
# Delete external users no longer sharing a channel with us.
if (not irc.users[source].channels) and (not irc.is_internal_client(source)):
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._remove_client(source)
# Clear empty non-permanent channels.
if channel in irc.channels and not (irc._channels[channel].users or ((irc.cmodes.get('permanent'), None) \
in irc._channels[channel].modes)):
log.debug('(%s) state_cleanup: removing empty channel %s', irc.name, channel)
del irc._channels[channel]
def _state_cleanup_part(irc, source, command, args):
for channel in args['channels']:
_state_cleanup_core(irc, source, channel)
utils.add_hook(_state_cleanup_part, 'PART', priority=-100)
def _state_cleanup_kick(irc, source, command, args):
_state_cleanup_core(irc, args['target'], args['channel'])
utils.add_hook(_state_cleanup_kick, 'KICK', priority=-100)
def _state_cleanup_mode(irc, source, command, args):
"""
Cleans up and removes empty channels when -P (permanent mode) is removed from them.
"""
target = args['target']
if target in irc.channels and 'permanent' in irc.cmodes:
c = irc.channels[target]
mode = '-%s' % irc.cmodes['permanent']
if (not c.users) and (mode, None) in args['modes']:
log.debug('(%s) _state_cleanup_mode: deleting empty channel %s as %s was set', irc.name, target, mode)
del irc._channels[target]
return False # Block further hooks from running
utils.add_hook(_state_cleanup_mode, 'MODE', priority=10000)

139
coremods/login.py Normal file
View File

@ -0,0 +1,139 @@
"""
login.py - Implement core login abstraction.
"""
from pylinkirc import conf, utils
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:
from passlib.context import CryptContext
except ImportError:
log.warning("Hashed passwords are disabled because passlib is not installed. Please install "
"it (pip3 install passlib) and rehash for this feature to work.")
return
context_settings = conf.conf.get('login', {}).get('cryptcontext_settings') or _DEFAULT_CRYPTCONTEXT_SETTINGS
global pwd_context
if pwd_context is None:
log.debug("Initialized new CryptContext with settings: %s", context_settings)
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):
"""
Returns the login data block for the given account name (case-insensitive), or False if none
exists.
"""
accounts = {k.lower(): v for k, v in
conf.conf['login'].get('accounts', {}).items()}
try:
return accounts[accountname.lower()]
except KeyError:
return False
def check_login(user, password):
"""Checks whether the given user and password is a valid combination."""
account = _get_account(user)
if account:
passhash = account.get('password')
if not passhash:
# No password given, return. XXX: we should allow plugins to override
# this in the future.
return False
# Hashing in account passwords is optional.
if account.get('encrypted', False):
return verify_hash(password, passhash)
else:
return password == passhash
return False
def verify_hash(password, passhash):
"""Checks whether the password given matches the hash."""
if password:
if not pwd_context:
raise utils.NotAuthorizedError("Cannot log in to an account with a hashed password "
"because passlib is not installed.")
return pwd_context.verify(password, passhash)
return False # No password given!
def _irc_try_login(irc, source, username, skip_checks=False):
"""Internal function to process logins via IRC."""
if irc.is_internal_client(source):
irc.error("Cannot use 'identify' via a command proxy.")
return
if not skip_checks:
logindata = _get_account(username)
network_filter = logindata.get('networks')
require_oper = logindata.get('require_oper', False)
hosts_filter = logindata.get('hosts', [])
if network_filter and irc.name not in network_filter:
log.warning("(%s) Failed login to %r from %s (wrong network: networks filter says %r but we got %r)",
irc.name, username, irc.get_hostmask(source), ', '.join(network_filter), irc.name)
raise utils.NotAuthorizedError("Account is not authorized to login on this network.")
elif require_oper and not irc.is_oper(source):
log.warning("(%s) Failed login to %r from %s (needs oper)", irc.name, username, irc.get_hostmask(source))
raise utils.NotAuthorizedError("You must be opered.")
elif hosts_filter and not any(irc.match_host(host, source) for host in hosts_filter):
log.warning("(%s) Failed login to %r from %s (hostname mismatch)", irc.name, username, irc.get_hostmask(source))
raise utils.NotAuthorizedError("Hostname mismatch.")
irc.users[source].account = username
irc.reply('Successfully logged in as %s.' % username)
log.info("(%s) Successful login to %r by %s",
irc.name, username, irc.get_hostmask(source))
return True
def identify(irc, source, args):
"""<username> <password>
Logs in to PyLink using the configured administrator account."""
if irc.is_channel(irc.called_in):
irc.reply('Error: This command must be sent in private. '
'(Would you really type a password inside a channel?)')
return
try:
username, password = args[0], args[1]
except IndexError:
irc.reply('Error: Not enough arguments.')
return
# Process new-style accounts.
if check_login(username, password):
_irc_try_login(irc, source, username)
return
# Process legacy logins (login:user).
if username.lower() == conf.conf['login'].get('user', '').lower() and password == conf.conf['login'].get('password'):
realuser = conf.conf['login']['user']
_irc_try_login(irc, source, realuser, skip_checks=True)
return
# Username not found or password incorrect.
log.warning("(%s) Failed login to %r from %s", irc.name, username, irc.get_hostmask(source))
raise utils.NotAuthorizedError('Bad username or password.')
utils.add_cmd(identify, aliases=('login', 'id'))

67
coremods/permissions.py Normal file
View File

@ -0,0 +1,67 @@
"""
permissions.py - Permissions Abstraction for PyLink IRC Services.
"""
from collections import defaultdict
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.
default_permissions = defaultdict(set)
def add_default_permissions(perms):
"""Adds default permissions to the index."""
global default_permissions
for target, permlist in perms.items():
default_permissions[target] |= set(permlist)
addDefaultPermissions = add_default_permissions
def remove_default_permissions(perms):
"""Remove default permissions from the index."""
global default_permissions
for target, permlist in perms.items():
default_permissions[target] -= set(permlist)
removeDefaultPermissions = remove_default_permissions
def check_permissions(irc, uid, perms, also_show=[]):
"""
Checks permissions of the caller. If the caller has any of the permissions listed in perms,
this function returns True. Otherwise, NotAuthorizedError is raised.
"""
# For old (< 1.1 login blocks):
# If the user is logged in, they automatically have all permissions.
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',
irc.get_hostmask(uid))
return True
permissions = defaultdict(set)
# Enumerate the configured permissions list.
for k, v in (conf.conf.get('permissions') or {}).items():
permissions[k] |= set(v)
# Merge in default permissions if enabled.
if conf.conf.get('permissions_merge_defaults', True):
for k, v in default_permissions.items():
permissions[k] |= v
for host, permlist in permissions.items():
log.debug('permissions: permlist for %s: %s', host, permlist)
if irc.match_host(host, uid):
# Now, iterate over all the perms we are looking for.
for perm in permlist:
# Use irc.match_host to expand globs in an IRC-case insensitive and wildcard
# friendly way. e.g. 'xyz.*.#Channel\' will match 'xyz.manage.#channel|' on IRCds
# using the RFC1459 casemapping.
log.debug('permissions: checking if %s glob matches anything in %s', perm, permlist)
if any(irc.match_host(perm, p) for p in perms):
return True
raise utils.NotAuthorizedError("You are missing one of the following permissions: %s" %
(', '.join(perms+also_show)))
checkPermissions = check_permissions

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."""
@ -14,55 +17,67 @@ def spawn_service(irc, source, command, args):
# Service name # Service name
name = args['name'] name = args['name']
if name != 'pylink' and not irc.has_cap('can-spawn-clients'):
log.debug("(%s) Not spawning service %s because the server doesn't support spawning clients",
irc.name, name)
return
# Get the ServiceBot object. # Get the ServiceBot object.
sbot = world.services[name] sbot = world.services[name]
# Look up the nick or ident in the following order: old_userobj = irc.users.get(sbot.uids.get(irc.name))
# 1) Network specific nick/ident settings for this service (servers::irc.name::servicename_nick) if old_userobj and old_userobj.service:
# 2) Global settings for this service (servicename::nick) # A client already exists, so don't respawn it.
# 3) The preferred nick/ident combination defined by the plugin (sbot.nick / sbot.ident) log.debug('(%s) spawn_service: Not respawning service %r as service client %r already exists.', irc.name, name,
# 4) The literal service name. irc.pseudoclient.nick)
# settings, and then falling back to the literal service name. return
nick = irc.serverdata.get("%s_nick" % name) or conf.conf.get(name, {}).get('nick') or sbot.nick or name
ident = irc.serverdata.get("%s_ident" % name) or conf.conf.get(name, {}).get('ident') or sbot.ident or name
# TODO: make this configurable? if name == 'pylink' and irc.pseudoclient:
host = irc.hostname() # irc.pseudoclient already exists, reuse values from it but
# spawn a new client. This is used for protocols like Clientbot,
# so that they can override the main service nick, among other things.
log.debug('(%s) spawn_service: Using existing nick %r for service %r', irc.name, irc.pseudoclient.nick, name)
userobj = irc.pseudoclient
userobj.opertype = "PyLink Service"
userobj.manipulatable = sbot.manipulatable
else:
# No client exists, spawn a new one
nick = sbot.get_nick(irc)
ident = sbot.get_ident(irc)
host = sbot.get_host(irc)
realname = sbot.get_realname(irc)
# Spawning service clients with these umodes where supported. servprotect usage is a # Spawning service clients with these umodes where supported. servprotect usage is a
# configuration option. # configuration option.
preferred_modes = ['oper', 'hideoper', 'hidechans', 'invisible', 'bot'] preferred_modes = ['oper', 'hideoper', 'hidechans', 'invisible', 'bot']
modes = [] modes = []
if conf.conf['bot'].get('protect_services'): if conf.conf['pylink'].get('protect_services'):
preferred_modes.append('servprotect') preferred_modes.append('servprotect')
for mode in preferred_modes: for mode in preferred_modes:
mode = irc.umodes.get(mode) mode = irc.umodes.get(mode)
if mode: if mode:
modes.append((mode, None)) modes.append((mode, None))
# Track the service's UIDs on each network. # Track the service's UIDs on each network.
userobj = irc.proto.spawnClient(nick, ident, host, modes=modes, opertype="PyLink Service", log.debug('(%s) spawn_service: Spawning new client %s for service %s', irc.name, nick, name)
manipulatable=sbot.manipulatable) userobj = irc.spawn_client(nick, ident, host, modes=modes, opertype="PyLink Service",
realname=realname, manipulatable=sbot.manipulatable)
# Store the service name in the User object for easier access.
userobj.service = name
sbot.uids[irc.name] = u = userobj.uid sbot.uids[irc.name] = u = userobj.uid
# Special case: if this is the main PyLink client being spawned, # Special case: if this is the main PyLink client being spawned,
# assign this as irc.pseudoclient. # assign this as irc.pseudoclient.
if name == 'pylink': if name == 'pylink' and not irc.pseudoclient:
log.debug('(%s) spawn_service: irc.pseudoclient set to UID %s', irc.name, u)
irc.pseudoclient = userobj irc.pseudoclient = userobj
# TODO: channels should be tracked in a central database, not hardcoded # Enumerate & join network defined channels.
# in conf. sbot.join(irc, sbot.get_persistent_channels(irc))
channels = set(irc.serverdata.get('channels', [])) | sbot.extra_channels.get(irc.name, set())
for chan in channels:
if utils.isChannel(chan):
irc.proto.join(u, chan)
irc.callHooks([irc.sid, 'PYLINK_SERVICE_JOIN', {'channel': chan, 'users': [u]}])
else:
log.warning('(%s) Ignoring invalid autojoin channel %r.', irc.name, chan)
utils.add_hook(spawn_service, 'PYLINK_NEW_SERVICE') utils.add_hook(spawn_service, 'PYLINK_NEW_SERVICE')
@ -86,24 +101,76 @@ def handle_endburst(irc, source, command, args):
for name, sbot in world.services.items(): for name, sbot in world.services.items():
spawn_service(irc, source, command, {'name': name}) spawn_service(irc, source, command, {'name': name})
utils.add_hook(handle_endburst, 'ENDBURST') utils.add_hook(handle_endburst, 'ENDBURST', priority=500)
def handle_kill(irc, source, command, args): def handle_kill(irc, source, command, args):
"""Handle KILLs to PyLink service bots, respawning them as needed.""" """Handle KILLs to PyLink service bots, respawning them as needed."""
target = args['target'] target = args['target']
sbot = irc.isServiceBot(target) if irc.pseudoclient and target == irc.pseudoclient.uid:
if sbot: irc.pseudoclient = None
spawn_service(irc, source, command, {'name': sbot.name}) userdata = args.get('userdata')
return sbot = irc.get_service_bot(target)
servicename = None
if userdata and hasattr(userdata, 'service'): # Look for the target's service name attribute
servicename = userdata.service
elif sbot: # Or their service bot instance
servicename = sbot.name
if servicename:
log.info('(%s) Received kill to service %r (nick: %r) from %s (reason: %r).', irc.name, servicename,
userdata.nick if userdata else irc.users[target].nick, irc.get_hostmask(source), args.get('text'))
spawn_service(irc, source, command, {'name': servicename})
utils.add_hook(handle_kill, 'KILL') utils.add_hook(handle_kill, 'KILL')
def handle_join(irc, source, command, args):
"""Monitors channel joins for dynamic service bot joining."""
if irc.has_cap('visible-state-only'):
# No-op on bot-only servers.
return
channel = args['channel']
users = irc.channels[channel].users
for servicename, sbot in world.services.items():
if channel in sbot.get_persistent_channels(irc) and \
sbot.uids.get(irc.name) not in users:
log.debug('(%s) Dynamically joining service %r to channel %r.', irc.name, servicename, channel)
sbot.join(irc, channel)
utils.add_hook(handle_join, 'JOIN')
utils.add_hook(handle_join, 'PYLINK_SERVICE_JOIN')
def _services_dynamic_part(irc, channel):
"""Dynamically removes service bots from empty channels."""
if irc.has_cap('visible-state-only'):
# No-op on bot-only servers.
return
if 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(irc.get_service_bot(u) for u in irc.channels[channel].users):
for u in irc.channels[channel].users.copy():
sbot = irc.get_service_bot(u)
if sbot:
log.debug('(%s) Dynamically parting service %r from channel %r.', irc.name, sbot.name, channel)
irc.part(u, channel)
return True
def handle_part(irc, source, command, args):
"""Monitors channel joins for dynamic service bot joining."""
for channel in args['channels']:
_services_dynamic_part(irc, channel)
utils.add_hook(handle_part, 'PART')
def handle_kick(irc, source, command, args): def handle_kick(irc, source, command, args):
"""Handle KICKs to the PyLink service bots, rejoining channels as needed.""" """Handle KICKs to the PyLink service bots, rejoining channels as needed."""
kicked = args['target']
channel = args['channel'] channel = args['channel']
if irc.isServiceBot(kicked): # Skip autorejoin routines if the channel is now empty.
irc.proto.join(kicked, channel) if not _services_dynamic_part(irc, channel):
irc.callHooks([irc.sid, 'PYLINK_SERVICE_JOIN', {'channel': channel, 'users': [kicked]}]) kicked = args['target']
sbot = irc.get_service_bot(kicked)
if sbot and channel in sbot.get_persistent_channels(irc):
sbot.join(irc, channel)
utils.add_hook(handle_kick, 'KICK') utils.add_hook(handle_kick, 'KICK')
def handle_commands(irc, source, command, args): def handle_commands(irc, source, command, args):
@ -111,18 +178,14 @@ def handle_commands(irc, source, command, args):
target = args['target'] target = args['target']
text = args['text'] text = args['text']
sbot = irc.isServiceBot(target) sbot = irc.get_service_bot(target)
if sbot: if sbot:
sbot.call_cmd(irc, source, text) sbot.call_cmd(irc, source, text)
utils.add_hook(handle_commands, 'PRIVMSG') utils.add_hook(handle_commands, 'PRIVMSG')
# Register the main PyLink service. All command definitions MUST go after this! # Register the main PyLink service. All command definitions MUST go after this!
mynick = conf.conf['bot'].get("nick", "PyLink") # TODO: be more specific in description, and possibly allow plugins to modify this to mention
myident = conf.conf['bot'].get("ident", "pylink")
# TODO: be more specific, and possibly allow plugins to modify this to mention
# their features? # their features?
mydesc = "\x02%s\x02 provides extended network services for IRC." % mynick mydesc = "\x02PyLink\x02 provides extended network services for IRC."
utils.register_service('pylink', default_nick="PyLink", desc=mydesc, manipulatable=True)
utils.registerService('pylink', nick=mynick, ident=myident, desc=mydesc, manipulatable=True)

View File

@ -5,8 +5,19 @@ This folder contains general documentation for PyLink IRC services.
## Contents ## Contents
- [PyLink FAQ (Frequently Asked Questions)](faq.md) - [PyLink FAQ (Frequently Asked Questions)](faq.md)
- [PyLink Relay Tutorial & Oper Guide](pylink-opers.md) - [PyLink Relay Quick Start Guide](relay-quickstart.md)
- [Automode & Exttargets Guide](automode.md)
----
- [Automode Tutorial](automode.md)
- [Advanced Relay Configuration](advanced-relay-config.md)
- [Advanced Services Configuration](advanced-services-config.md)
- [Extended Targets (Exttargets) Guide](exttargets.md)
- [PyLink Permissions Reference](permissions-reference.md)
----
- [PyLink named modes tables](modelists/)
- [Developer documentation](technical/) - [Developer documentation](technical/)
There is also a (WIP) Doxygen-powered API reference at https://pylink.github.io/ There is also a Doxygen-powered API reference at https://pylink.github.io/

View File

@ -0,0 +1,93 @@
# Advanced Configuration for PyLink Relay
PyLink Relay provides a few configuration options not documented in the example configuration, either because they have limited use or are too complicated to be described briefly.
**This guide assumes that you are relatively familiar with the way YAML syntax works (lists, named arrays/dicts, etc.).** In this document, configuration options will be referred to in the format `a::b::c`, which represents the "`c`" option inside a "`b`" config block, all within an "`a`" config block.
In actual YAML, that translates to this:
```yaml
a:
b:
c: "some value"
```
### 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 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 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).
These options take template strings as documented here: https://docs.python.org/3/library/string.html#template-strings. Supported substitution values differ by event, but usually include the [hook values for each](technical/hooks-reference.md#irc-command-hooks), *plus* the following:
- For all events:
- `$netname`: origin network name
- `$sender`: nick of sender
- `$sender_identhost`: ident@host string of the sender
- `$colored_sender`: color hashed version of `$sender`
- `$colored_netname`: color hashed version of `$netname`
- For KICK, and other events that have a `$target` field corresponding to a user:
- `$target_nick`: the nick of the target (as opposed to `$target`, which is an user ID)
- For events that have a `$channel` field attached (e.g. JOIN, PART):
- `$local_channel`: the *local* channel name (i.e. the channel on the clientbot network)
- `$channel`: the real channel name on the sender's network
- `$mode_prefix`: the highest prefix mode of the sender, if they are a user. This is normally either empty or one of (common prefix modes) `~&!@%+`.
- For SJOIN, SQUIT:
- `$nicks`: a comma-joined list of nicks that were bursted
- `$colored_nicks`: a comma-joined list of each bursted nick, color hashed
To disable relaying for any specific event, set the template string to an empty string (`''`).
#### List of supported events
|Event name|Default value|
| :---: | :--- |
MESSAGE | \x02[$netname]\x02 <$colored\_sender> $text
KICK | \x02[$netname]\x02 - $colored_sender$sender\_identhost has kicked $target_nick from $channel ($text)
PART | \x02[$netname]\x02 - $colored_sender$sender\_identhost has left $channel ($text)
JOIN | \x02[$netname]\x02 - $colored_sender$sender\_identhost has joined $channel
NICK | \x02[$netname]\x02 - $colored_sender$sender\_identhost is now known as $newnick
QUIT | \x02[$netname]\x02 - $colored_sender$sender\_identhost has quit ($text)
ACTION | \x02[$netname]\x02 * $colored\_sender $text
NOTICE | \x02[$netname]\x02 - Notice from $colored\_sender: $text
SQUIT | \x02[$netname]\x02 - Netsplit lost users: $colored\_nicks
SJOIN | \x02[$netname]\x02 - Netjoin gained users: $colored\_nicks
MODE | \x02[$netname]\x02 - $colored_sender$sender_identhost sets mode $modes on $channel
PM | PM from $sender on $netname: $text
PNOTICE | <$sender> $text
- Note: the `PM` and `PNOTICE` events represent private messages and private notices respectively, when they're relayed to users behind a Clientbot link.
- Note 2: as of 1.1.x, all public channel events are sent to channels as PRIVMSG, while `PM` and `PNOTICE` are relayed privately as NOTICE.
#### Disabling Colors/Control Codes
If you don't want the messages PyLink sends for clientbot messages to be emboldened or colored,
remove all escape sequences (e.g. `\x02`) from the format template and replace the colored variants
of applicable substitutions with their non-colored versions.
This is a example clientbot_styles config block, which you can copy *into* your `relay` configuration block.
(*Do not* make multiple `relay` config blocks, or duplicate any config blocks with the same name!)
```yaml
clientbot_styles:
ACTION: "[$netname] * $sender $text"
JOIN: "[$netname] - $sender$sender_identhost has joined $channel"
KICK: "[$netname] - $sender$sender_identhost has kicked $target_nick from $channel ($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"
NOTICE: "[$netname] - Notice from $sender: $text"
PART: "[$netname] - $sender$sender_identhost has left $channel ($text)"
PM: "PM from $sender on $netname: $text"
PNOTICE: "<$sender> $text"
QUIT: "[$netname] - $sender$sender_identhost has quit ($text)"
SJOIN: "[$netname] - Netjoin gained users: $nicks"
SQUIT: "[$netname] - Netsplit lost users: $nicks"
```
### Misc. options
- `relay::clientbot_startup_delay`: Defines the amount of seconds Clientbot should wait after startup, before relaying any non-PRIVMSG events. This is used to prevent excess floods when the bot connects. Defaults to 5 seconds.
- `servers::NETNAME::relay_force_slashes`: This network specific option forces Relay to use `/` in nickname separators. You should only use this option on TS6 or P10 variants that are less strict with nickname validation, as **it will cause protocol violations** on most IRCds. UnrealIRCd and InspIRCd users do not need to set this either, as `/` in nicks is automatically enabled.
- `servers::NETNAME::relay_endburst_delay`: InspIRCd networks only: sets the endburst delay for relay subservers. If relay server bursts are causing +j (join flood) protection to trigger, raising this value can work around the issue.

View File

@ -0,0 +1,61 @@
# Advanced Services Configuration
There are some service configuration options that you may want to be aware of.
#### Nick / Ident
You can override the `nick` or `ident` of a service bot using a directive liek this:
```yaml
servers:
somenet:
# ...
SERVICE_nick: OTHERNICK
SERVICE_ident: OTHERIDENT
```
You can also set an arbitrary nick/ident using a per-**service** directive.
```yaml
SERVICE:
nick: OTHERNICK
ident: OTHERIDENT
```
#### joinmodes
By default, service bots join channels without giving themselves any modes. You can configure what modes a service bot joins channels with using this directive:
```yaml
SERVICE:
joinmodes: 'o'
```
This would request the mode 'o' (op on most IRCds) when joining the channel.
Technically any mode can be put here, but if an IRCd in question doesn't support
the mode then it will be ignored.
You can also use combinations of modes, such as 'ao' (usually admin/protect + op)
```yaml
SERVICE:
joinmodes: 'ao'
```
Combinations should work provided an IRCd in question supports it.
#### Fantasy prefix
You can also set the service bot's fantasy prefix; of course this is only
applicable if the `fantasy` plugin is loaded.
The setting allows for one or more characters to be set as the prefix.
```yaml
SERVICE:
prefix: './'
```
The above is perfectly valid, as is any other string.

View File

@ -1,44 +1,37 @@
# Automode Tutorial # Automode Tutorial
The Automode plugin was introduced in PyLink 0.9 as a simple way of managing channel access control lists. That said, it is not designed to entirely replace traditional IRC services such as ChanServ. The Automode plugin was introduced in PyLink 0.9 as a simple mechanism to manage channel access. That said, it is not designed to entirely replace traditional IRC services such as ChanServ.
## Starting steps ## Starting steps
Upon loading the `automode` plugin, you should see a ModeBot service client connect, using the name you defined. This client provides the commands used to manage access. Upon loading the `automode` plugin, you should see an Automode service bot connect, using the name that you defined (this guide uses the default, `Automode`). This service provides the commands used to manage access.
For a list of commands: For a list of commands:
- `/msg ModeBot help` - `/msg Automode help`
Adding access lists to a channel: Adding access to a channel:
- `/msg ModeBot setacc #channel [MASK] [MODE LIST]` - `/msg Automode setacc #channel [MASK] [MODE LIST]`
- The mask can be a simple `nick!user@host` hostmask or any of the extended targets (exttargets) mentioned below. MODE LIST is a string of any prefix modes that you want to set (no `+` before needed), such as `qo`, `h`, or `ov`. - The mask can be a simple `nick!user@host` hostmask or any of the extended targets (exttargets) mentioned below. MODE LIST is a string of any prefix modes that you want to set (no leading `+` needed): e.g. `qo`, `h`, or `ov`.
Removing access from a channel: Removing access from a channel:
- `/msg ModeBot delacc #channel [MASK]` - `/msg Automode delacc #channel [MASK]`
Listing access entries on a channel: Listing access entries on a channel:
- `/msg ModeBot listacc #channel` - `/msg Automode listacc #channel`
Applying all access entries on a channel (sync): Applying all access entries on a channel (sync):
- `/msg ModeBot syncacc #channel` - `/msg Automode syncacc #channel`
Clearing all access entries on a channel: Clearing all access entries on a channel:
- `/msg ModeBot clearacc #channel` - `/msg Automode clearacc #channel`
## Supported masks and extended targets ## Supported masks and extended targets
Automode supports any hostmask or PyLink extended target; see the [Exttargets Guide](exttargets.md) for more details.
Extended targets or exttargets *replace* regular hostmasks with conditional matching based on the given situation. The following exttargets are supported: ## Permissions
- `$account` -> Returns True (a match) if the target is registered. See the [Permissions Reference](permissions-reference.md#automode) for a list of permissions defined by Automode.
- `$account:accountname` -> Returns True if the target's account name matches the one given, and the target is connected to the local network. Account names are case insensitive.
- `$account:accountname:netname` -> Returns True if both the target's account name and origin network name match the ones given. Account names are case insensitive, but network names ARE case sensitive. ## Caveats
- `$account:*:netname` -> Matches all logged in users on the given network. Globs are not supported here; only a literal `*`.
- `$ircop` -> Returns True (a match) if the target is opered. - Service bot joining and Relay don't always behave consistently: see https://github.com/jlu5/PyLink/issues/265
- `$ircop:*admin*` -> Returns True if the target's is opered and their oper type matches the glob given (case insensitive).
- `$server:server.name` -> Returns True (a match) if the target is connected on the given server. Server names are matched case insensitively.
- `$server:*server.glob*` -> Returns True (a match) if the target is connected on a server matching the glob.
- `$server:1XY` -> Returns True if the target's is connected on the server with the given SID. Note: SIDs ARE case sensitive.
- `$channel:#channel` -> Returns True if the target is in the given channel (case insensitive).
- `$channel:#channel:op` -> Returns True if the target is in the given channel, and is opped. Any supported prefix mode (owner, admin, op, halfop, voice) can be used for the last part, but only one at a time.
- `$pylinkacc` -> Returns True if the target is logged in to PyLink.
- `$pylinkacc:accountname` -> Returns True if the target's PyLink login matches the one given (case insensitive).

72
docs/exttargets.md Normal file
View File

@ -0,0 +1,72 @@
# Exttargets Guide
**Extended targets** or **exttargets** extend regular hostmask matching by checking users against specific conditions. PyLink exttargets are supported by most plugins in the place of `nick!user@host` masks (provided they use `IRCNetwork.match_host()` as their backend).
Exttargets were introduced in PyLink 0.9 alongside [Automode](automode.md), with the goal of making user/ACL matching more versatile. As of PyLink 2.0, the following exttargets are supported:
### The "$account" target (PyLink 0.9+)
Used to match users by their services account.
- `$account` -> Returns True (a match) if the target is registered.
- `$account:accountname` -> Returns True if the target's account name matches the one given, and the target is connected to the local network. Account names are case insensitive.
- `$account:accountname:netname` -> Returns True if both the target's account name and origin network name match the ones given. Account names are case insensitive, but network names ARE case sensitive.
- `$account:*:netname` -> Matches all logged in users on the given network. Globs are not supported here; only a literal `*`.
### The "$channel" target (PyLink 0.9+)
Used to match users in certain channels. Channel names are matched case insensitively.
- `$channel:#channel` -> Returns True if the target is in the given channel.
- `$channel:#channel:PREFIXMODE` -> Returns True if the target is in the given channel, and is opped. Any supported prefix mode (owner, admin, op, halfop, voice) can be used for the last part, but only one at a time.
### The "$ircop" target (PyLink 0.9+)
Used to match users by IRCop status.
- `$ircop` -> Returns True (a match) if the target is opered.
- `$ircop:*admin*` -> Returns True if the target's is opered and their oper type matches the glob given (case insensitive).
### Target inversion (PyLink 1.2+)
In PyLink 1.2 and above, all targets and hostmasks can be inverted by placing a `!` before the target:
- `!$account` -> Matches all users not registered with services.
- `!$ircop` -> Matches all non-opers.
- `!*!*@localhost` -> Matches all users not connecting from localhost.
- `!*!*@*:*` -> Matches all non-IPv6 users.
For users on PyLink version **before 1.2**, target inversion is *only* supported with exttargets (i.e. `!$account` will work, but not `!*!*@localhost`.
### The "$network" target (PyLink 1.2+)
Used to match users on specific networks.
- `$network:netname` -> Returns True if the target user originates from the given network (this supports and looks up the home network of Relay users).
### The "$and" target (PyLink 1.2+)
The `$and` target is slightly more complex, and involves chaining together multiple exttargets or hosts with a `+` between each. Note that parentheses are required around the list of targets to match.
- `$and:($ircop:*admin*+$network:ovd)` -> Matches all opers on the network ovd.
- `$and:($account+$pylinkirc)` -> Matches all users logged in to both services and PyLink.
- `$and:(*!*@localhost+$ircop)` -> Matches all opers with the host `localhost`.
- `$and:(*!*@*.mibbit.com+!$ircop+!$account)` -> Matches all (non-CGIIRC) Mibbit users that aren't opered or logged in to services.
### The "$server" target (PyLink 0.9+)
Used to match users on specific IRC servers.
- `$server:server.name` -> Returns True (a match) if the target is connected on the given server. Server names are matched case insensitively.
- `$server:*server.glob*` -> Returns True (a match) if the target is connected on a server matching the glob.
- `$server:1XY` -> Returns True if the target's is connected on the server with the given SID. Note: SIDs ARE case sensitive.
### The "$pylinkacc" target (PyLink 0.9+)
Used to match users logged in to *PyLink* (i.e. via the `identify` command). **As of PyLink 2.0, The "$pylinkirc:" prefix is implied if you specify a PyLink account name without it.**
- `$pylinkacc` -> Returns True if the target is logged in to PyLink.
- `$pylinkacc:accountname` -> Returns True if the target's PyLink login matches the one given (case insensitive).
### The "$realname" target (PyLink 2.0+)
Used to match users with certain realnames.
- `$realname:*James*`: matches anyone with "James" in their real name (case insensitive).
### The "$service" target (PyLink 2.0+)
Used to match service bots. This exttarget takes one optional argument: a glob, which is compared case-insensitively to the target user's service name if present.
- `$service`: matches any PyLink service bot.
- `$service:automode`: matches the Automode service bot.

View File

@ -1,37 +1,144 @@
# PyLink FAQ # PyLink FAQ
## Startup errors
### I get errors like "ImportError: No module named 'yaml'" when I start PyLink ### I get errors like "ImportError: No module named 'yaml'" when I start PyLink
You are missing dependencies - re-read https://github.com/GLolol/PyLink#dependencies You are missing dependencies - re-read https://github.com/jlu5/PyLink/blob/master/README.md#installation
### I get errors like "yaml.scanner.ScannerError: while scanning for the next token, found character '\t' that cannot start any token" ### I get errors like "yaml.scanner.ScannerError: while scanning for the next token, found character '\t' that cannot start any token"
You must use SPACES and not tabs in your configuration! (`\t` is the escaped code for a tab, which is disallowed by YAML) You must use **spaces** and not tabs to indent your configuration file! (`\t` is the escaped code for a tab, which is not allowed in YAML)
### I get errors like "ParserError: while parsing a block mapping ... expected &lt;block end&gt;, but found '&lt;block sequence start&gt;'
This likely indicates an indentation issue. When you create a list in YAML (PyLink's config format), all entries must be indented consistently. For example, this is **bad**:
```yaml
# This will cause an error!
someblock:
- abcd
- def
- ghi
```
This is good:
```yaml
someblock:
- abcd
- def
- ghi
```
### I keep getting YAML / syntax errors trying to set up my instance!
Take a few minutes to familiarize yourself with YAML, the markup language we use for the config file.
[CraftIRC](https://github.com/Animosity/CraftIRC/wiki/Complete-idiot%27s-introduction-to-yaml), [Ansible](https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html), and [Wikipedia](https://en.wikipedia.org/wiki/YAML) all provide excellent guides (with examples) on its basic structure.
A common misconception is that the YAML format is something specific to Python or PyLink, but this is not the case! YAML is a programming language-independent standard which *happens* to use indents for structures like Python does, but [parsers for it exist just about everywhere](http://yaml.org/).
The reason I (James) chose it for this project is essentially a restatement of its common benefits:
- It's compact and human readable (compared to raw JSON or XML)
- It's powerful, supporting everything from nested config blocks to multi-line strings
- It fits well in the Python landscape, compared to a flat .ini or C-style config format
- It's language independent, which means it's not a giant pain if we decide to rewrite the project in some other language one day... 🙃
## Linking / Connection issues
### PyLink won't connect to my network!
As a general guide, you should check the following before asking for help:
- Is the target network's IRCd showing failed connection attempts?
- If not:
1) Is PyLink connecting to the right port (i.e. one the IRCd is listening on?)
2) Is the target network's IRCd actually binding to the port you're trying to use? If there is a port conflict with another program, the IRCd may fail to bind to specific ports but *still start* on others which are free.
3) Is the target port firewalled on the target machine?
4) Is there a working connection between the source and target servers? Use ping to test this, as routing issues between providers can cause servers to become unreachable.
- If your servers purposely block ping, you're going to have to figure this one out yourself... 😬
- If so:
1) Check for recvpass/sendpass/server hostname/IP mismatches - usually the IRCd will tell you if you're running into one of these, provided you have the right server notices enabled (consult your IRCd documentation for how to enable these).
2) Make sure you're not connecting with SSL on a non-SSL port, or vice versa.
If these steps haven't helped you so far, maybe you've found a bug...?
### My networks keep disconnecting with SSL errors!
See https://github.com/jlu5/PyLink/issues/463 - the problem appears to be caused somewhere in Python's SSL stack and/or OpenSSL, and not directly by our code.
Unfortunately, the only workarounds so far are to either disable SSL/TLS, or wrap a plain IRC connection in an external service (stunnel, OpenVPN, etc.)
### I turned autoconnect for PyLink on, and now I'm getting errors! ### I turned autoconnect for PyLink on, and now I'm getting errors!
PyLink does not support inbound connections - much like regular services such as Atheme or Anope, it only connects outwards *to* IRCds. (If you don't understand what this means, it means you should turn autoconnect OFF for PyLink) PyLink does not support inbound connections - much like regular services such as Atheme or Anope, it only connects outwards *to* IRCds. (If you don't understand what this means, it means you should turn autoconnect **off** for PyLink)
### Does PyLink support Clientbot relay like Janus? ## Relay issues
Yes. However, Clientbot support is in alpha stages as of PyLink 0.10 and is far from complete: [Clientbot TODO](https://github.com/GLolol/PyLink/issues?q=is%3Aissue+is%3Aopen+label%3Aprotocols%2Fclientbot).
### Clientbot doesn't relay both ways!
Load the `relay_clientbot` plugin. https://github.com/GLolol/PyLink/blob/e1fab8c/example-conf.yml#L303-L306
### Does everyone need to install PyLink Relay for it to work? ### Does everyone need to install PyLink Relay for it to work?
**No!** Only the PyLink administrator needs to host a PyLink instance, as each can connect to multiple networks. Everyone else only needs to add a link block on their IRCd. **No!** Only the PyLink administrator needs to host a PyLink instance with the `relay` plugin loaded, as each instance can connect to multiple networks. Everyone else only needs to add a link block on their IRCd.
InterJanus-style links between PyLink daemons are not supported yet; see https://github.com/GLolol/PyLink/issues/99 for any progress regarding that. InterJanus-style links between PyLink daemons are not supported yet; see https://github.com/jlu5/PyLink/issues/99 for any progress regarding that.
### What are PyLink Relay's benefits over Janus? ### What are PyLink's advantages over Janus?
In no particular order: PyLink provides, in no particular order:
- More complete support for modern IRCds (UnrealIRCd 4.x, InspIRCd 2.0, charybdis 4, Nefarious IRCu, etc.). - More complete support for modern IRCds (UnrealIRCd 4.x, InspIRCd 2.0, charybdis 4, Nefarious IRCu, etc.).
- PyLink is built upon a flexible, maintainable codebase. - A flexible, maintainable codebase extensible beyond Relay.
- Cross platform (*nix, Windows, and probably others too). - Proper protocol negotiation leading to fewer SQUIT/DoS possibilities:
- Proper protocol negotiation leading to fewer DoS possibilities:
- Better support for channel modes such as +fjMOR, etc. - Better support for channel modes such as +fjMOR, etc.
- Configurable nick length limits for relayed users. - Proper support for nick length limits with relayed users.
### My IRCd SQUITs the Relay server with errors like "Bad nickname introduced"!
First, check whether the SQUIT message includes the nick that triggered the netsplit. If this nick includes any characters not allowed in regular IRC, such as the slash ("/"), or is otherwise an invalid nick (e.g. beginning with a hyphen or number), this likely indicates a bug in PyLink Relay. These problems should be reported on the issue tracker.
However, if the nick mentioned is legal on IRC, this issue is likely caused by a max nick length misconfiguration: i.e. the Relay server is introducing nicks too long for the target network. This can be fixed by setting the `maxnicklen` option in the affected network's PyLink `server:` block to the same value as that network's `005` `NICKLEN` (that is, the `NICKLEN=<num>` value in `/raw version`).
### Clientbot doesn't relay both ways!
Load the `relay_clientbot` plugin. https://github.com/jlu5/PyLink/blob/1.3-beta1/example-conf.yml#L465-L468
### How do I turn off colors in Clientbot?
See https://github.com/jlu5/PyLink/blob/master/docs/advanced-relay-config.md#custom-clientbot-styles, especially the section "Disabling Colors/Control Codes".
### Relay is occasionally dropping users from channels!
This usually indicates a serious bug in either Relay or PyLink's protocol modules, and should be reported as an issue. When asking for help, please state which IRCds your PyLink instance is linking to: specifically, which IRCd the missing users are *from* and which IRCd the users are missing *on*. Also, be prepared to send debug logs as you reproduce the issue!
- Another tip in debugging this is to run `showchan` on the affected channels. If PyLink shows users in `showchan` that aren't in the actual user list, this is most likely a protocol module issue. If `showchan`'s output is correct, it is instead probably a Relay issue where users aren't spawning correctly.
### Does Relay support mode +R, +M, etc.? How does Relay handle modes supported on one IRCd but not on another?
Essentially, PyLink maps IRCd modes together by name, so modes that use different characters on different IRCds can be recognized as the same "mode". Tables of supported channel modes, user modes, and extbans (in 2.0+) can be found at https://github.com/jlu5/PyLink/tree/devel/docs/modelists. Note that third party/contrib modules implementing modes are generally *not* tested / supported.
Relay in particular uses whitelists to determine which modes are safe to relay: for 2.0.0, this is https://github.com/jlu5/PyLink/blob/71a24b8/plugins/relay.py#L903-L969. **Most *channel* modes recognized by PyLink are whitelisted and usable with Relay**, with the following exceptions:
- "registered" channel / user modes (InspIRCd, UnrealIRCd **+r**) - this is to prevent conflicts with local networks's services.
- "permanent" channel modes (commonly **+P**) - it's not necessary for remote networks' channels to also be permanent locally.
- Flood protection modes are only relayed between networks running the same IRCd (UnrealIRCd <-> UnrealIRCd or InspIRCd <-> InspIRCd).
- Modes and extbans that specify a forwarding channel - mangling channel names between networks is far too complicated and desync prone.
- InspIRCd's m_ojoin **+Y** and m_operprefix **+y** are ignored by Relay.
- auditorium (InspIRCd **+u**), delayjoin (UnrealIRCd, P10, InspIRCd **+D**), and any other modes affecting join visibilites are not supported.
Support for user modes is not as complete:
- Filter type modes such as callerid (**+g**), regonly (**+R**), noctcp (UnrealIRCd **+T**) are *not yet* supported by Relay.
- Service protection modes (UnrealIRCD **+S**, InspIRCd **+k**, etc.) are not forwarded by Relay to prevent abuse.
### How does Relay handle kills?
See https://github.com/jlu5/PyLink/blob/devel/docs/relay-quickstart.md#kill-handling
### How does Relay handle KLINE/GLINE/ZLINE?
It doesn't. https://github.com/jlu5/PyLink/issues/521#issuecomment-352316396 explains my reasons for skipping over this:
* The weakest link, whether this be a malicious/compromised/mistaken oper or a misconfigured services instance, can easily wreak havoc by banning something they shouldn't.
* KLINE relaying goes against the concept of partial network links and creates serious animosity when opers disagree on policy. If KLINEs are shared, opers are essentially shared as well, and this is not the goal of Relay.
## Services issues
### Service bots aren't spawning on my network, even though PyLink connects
This indicates either a bug in PyLink's protocol module or (less commonly) a bug in your IRCd. Hint: ENDBURST is likely not being sent or received properly, which causes service bot spawning to never trigger.
Make sure you're using an [officially supported IRCd](https://github.com/jlu5/PyLink#supported-ircds) before requesting help, as custom IRCd code can potentially trigger S2S bugs and is not something we can support.

5
docs/modelists/README.md Normal file
View File

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

View File

@ -0,0 +1,71 @@
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 (customprefix),a,,,,,a (when enabled),a (when enabled),,a
adminonly,,,,,,,a,,A (ext/chm_adminonly),A (ext/chm_adminonly),A (ext/chm_adminonly.so),,
allowinvite,,,A (m_allowinvite),A (allowinvite),,,,,g,g,g,,
auditorium,,,u (m_auditorium),u (auditorium),,,,,,,,,
autoop,,,w (m_autoop),w (autoop),,,,,,,,,
ban,b,b,b,b,b,b,b,b,b,b,b,b,b
banexception,,e,e (m_banexception),e (banexception),e,,e,,e,e,e,e,e
blockcaps,,,B (m_blockcaps),"B (anticaps, blockcaps)",,,,,,,G (ext/chm_nocaps.so),,
blockcolor,,c,c (m_blockcolor),c (blockcolor),,c,c,c,,,,,c (chanmodes/nocolor)
blockhighlight,,,V (contrib/m_blockhighlight),V (contrib/blockhighlight),,,,,,,,,
censor,,,G (m_censor),G (censor),,,,,,,,,G (chanmodes/censor)
delayjoin,,,D (m_delayjoin),D (delayjoin),,D,D,D,,,,,D (chanmodes/delayjoin)
delaymsg,,,d (m_delaymsg),d (delaymsg),,,,,,,,,
exemptchanops,,,X (m_exemptchanops),X (exemptchanops),,,,,,,,,
filter,,,g (m_filter),g (filter),,,,,,,,,(via extban ~T:block:)
flood,,,f (m_messageflood),f (messageflood),,,,,,,,,
flood_unreal,,,,,,,,,,,,,f (chanmodes/floodprot)
freetarget,,,,,,,,,F,F,F,,
had_delayjoin,,,,,,d,d,d,,,,,
halfop,,h,"h (m_customprefix, m_halfop)",h (customprefix),h,,,,,h (when enabled),h (when enabled),,h
hiddenbans,,,,,,,,,,,u,,
hidequits,,,,,,,Q,u,,,,,
history,,,H (m_chanhistory),H (chanhistory),,,,,,,,,
invex,,I,I (m_inviteexception),I (inviteexception),I,,,,I,I,I,I,I
inviteonly,i,i,i,i,i,i,i,i,i,i,i,i,i
issecure,,,,,,,,,,,,,Z (chanmodes/issecure)
joinflood,,,j (m_joinflood),j (joinflood),,,,,j,j,j,,
key,k,k,k,k,k,k,k,k,k,k,k,k,k
kicknorejoin,,,,,,,,,,,J,,
kicknorejoin_insp,,,J (m_kicknorejoin),J (kicknorejoin),,,,,,,,,
largebanlist,,,,,,,,,L,L,L,,
limit,l,l,l,l,l,l,l,l,l,l,l,l,l
moderated,m,m,m,m,m,m,m,m,m,m,m,m,m
netadminonly,,,,,,,,,,N (ext/chm_netadminonly),,,
nickflood,,,F (m_nickflood),F (nickflood),,,,,,,,,
noamsg,,,,,,,T,T,,,,,
noctcp,,C,C (m_noctcp),C (noctcp),,C,C,C,C,C,C,,C (chanmodes/noctcp)
noextmsg,n,n,n,n,n,n,n,n,n,n,n,n,n
noforwards,,,,,,,,,Q,Q,Q,,
noinvite,,,,,V,,,,,,,,V (chanmodes/noinvite)
nokick,,,Q (m_nokicks),Q (nokicks),Q,,,,,,E,,Q (chanmodes/nokick)
noknock,,p*,K (m_knock),K (knock),,,,,p*,p*,p*,p*,K (chanmodes/noknock)
nonick,,,N (m_nonicks),N (nonicks),N,,,,,,d,,N (chanmodes/nonickchange)
nonotice,,,T (m_nonotice),T (nonotice),,,N,N,T (ext/chm_nonotice),T (ext/chm_nonotice),T,,T (chanmodes/nonotice)
official-join,,,Y (m_ojoin),Y (ojoin),,,,,,,,,
op,o,o,o,o,o,o,o,o,o,o,o,o,o
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)
oplevel_apass,,,,,,A,A,A,,,,,
oplevel_upass,,,,,,U,U,U,,,,,
opmoderated,,,U (contrib/m_opmoderated),,,,,,z,z,z,,
owner,,,"q (m_customprefix, m_chanprotect)",q (customprefix),q,,,,,y (when enabled),y (when enabled),,q
paranoia,,p*,,,,,,,,,,,
permanent,,,P (m_permchannels),P (permchannels),P,,z,,P,P,P,,P (chanmodes/permanent)
private,p,p*,p,p,p,p,p,p,p*,p*,p*,p*,p
quiet,,,(via extban m:),(via extban m:),,,(via extban ~q:),,q,q,q,,(via extban ~q:)
redirect,,,L (m_redirect),L (redirect),,,L,,f,f,f,,L (chanmodes/link)
registered,,r,r (m_services_account),r (services_account),r,R,R,R,,,,,r
regmoderated,,M,M (m_services_account),M (services_account),M,,M,M,,,,,M (chanmodes/regonlyspeak)
regonly,,R,R (m_services_account),R (services_account),R,r,r,r,r,r,r,r,R (chanmodes/regonly)
repeat,,,,,,,,,,,K (ext/chm_norepeat.c),,
repeat_insp,,,,E (repeat),,,,,,,,,
secret,s,s,s,s,s,s,s,s,s,s,s,s,s
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)
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
,,,,,,,,,,,,,
----,,,,,,,,,,,,,
<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/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 (chanmodes/nocolor)
11 blockhighlight V (contrib/m_blockhighlight) V (contrib/blockhighlight)
12 censor G (m_censor) G (censor) G (chanmodes/censor)
13 delayjoin D (m_delayjoin) D (delayjoin) D D D D (chanmodes/delayjoin)
14 delaymsg d (m_delaymsg) d (delaymsg)
15 exemptchanops X (m_exemptchanops) X (exemptchanops)
16 filter g (m_filter) g (filter) (via extban ~T:block:)
17 flood f (m_messageflood) f (messageflood)
18 flood_unreal f (chanmodes/floodprot)
19 freetarget F F F
20 had_delayjoin d d d
21 halfop h h (m_customprefix, m_halfop) h (customprefix) h h (when enabled) h (when enabled) h
22 hiddenbans u
23 hidequits Q u
24 history H (m_chanhistory) H (chanhistory)
25 invex I I (m_inviteexception) I (inviteexception) I I I I I I
26 inviteonly i i i i i i i i i i i i i
27 issecure Z (chanmodes/issecure)
28 joinflood j (m_joinflood) j (joinflood) j j j
29 key k k k k k k k k k k k k k
30 kicknorejoin J
31 kicknorejoin_insp J (m_kicknorejoin) J (kicknorejoin)
32 largebanlist L L L
33 limit l l l l l l l l l l l l l
34 moderated m m m m m m m m m m m m m
35 netadminonly N (ext/chm_netadminonly)
36 nickflood F (m_nickflood) F (nickflood)
37 noamsg T T
38 noctcp C C (m_noctcp) C (noctcp) C C C C C C C (chanmodes/noctcp)
39 noextmsg n n n n n n n n n n n n n
40 noforwards Q Q Q
41 noinvite V V (chanmodes/noinvite)
42 nokick Q (m_nokicks) Q (nokicks) Q E Q (chanmodes/nokick)
43 noknock p* K (m_knock) K (knock) p* p* p* p* K (chanmodes/noknock)
44 nonick N (m_nonicks) N (nonicks) N d N (chanmodes/nonickchange)
45 nonotice T (m_nonotice) T (nonotice) N N T (ext/chm_nonotice) T (ext/chm_nonotice) T T (chanmodes/nonotice)
46 official-join Y (m_ojoin) Y (ojoin)
47 op o o o o o o o o o o o o o
48 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)
49 oplevel_apass A A A
50 oplevel_upass U U U
51 opmoderated U (contrib/m_opmoderated) z z z
52 owner q (m_customprefix, m_chanprotect) q (customprefix) q y (when enabled) y (when enabled) q
53 paranoia p*
54 permanent P (m_permchannels) P (permchannels) P z P P P P (chanmodes/permanent)
55 private p p* p p p p p p p* p* p* p* p
56 quiet (via extban m:) (via extban m:) (via extban ~q:) q q q (via extban ~q:)
57 redirect L (m_redirect) L (redirect) L f f f L (chanmodes/link)
58 registered r r (m_services_account) r (services_account) r R R R r
59 regmoderated M M (m_services_account) M (services_account) M M M M (chanmodes/regonlyspeak)
60 regonly R R (m_services_account) R (services_account) R r r r r r r r R (chanmodes/regonly)
61 repeat K (ext/chm_norepeat.c)
62 repeat_insp E (repeat)
63 secret s s s s s s s s s s s s s
64 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)
65 stripcolor S (m_stripcolor) S (stripcolor) S c c c 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
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

@ -0,0 +1,281 @@
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<meta name=viewport content="width=device-width, initial-scale=1">
<head>
<title>Supported Channel Modes for PyLink</title>
<style>
html {
background-color: white;
}
.note {
color: #555555;
}
/* (╮°-°)╮┳━┳ */
table, th, td {
border: 1px solid black;
}
td, th {
text-align: center;
padding: 3px;
}
td:first-child, th[scope="row"] {
text-align: left;
}
/* Table cells */
.tablecell-yes {
background-color: #A7F2A5
}
.tablecell-no {
background-color: #F08496
}
.tablecell-na {
background-color: #F0F0F0
}
.tablecell-planned, .tablecell-yes2 {
background-color: #B1FCDE
}
.tablecell-partial {
background-color: #EDE8A4
}
.tablecell-special {
background-color: #DCB1FC
}
</style>
</head>
<body>
<table><tr>
<th scope="col">Channel Mode / IRCd</th>
<th scope="col">rfc1459</th>
<th scope="col">hybrid</th>
<th scope="col">inspircd/insp20</th>
<th scope="col">inspircd/insp3</th>
<th scope="col">ngircd</th>
<th scope="col">p10/ircu</th>
<th scope="col">p10/nefarious</th>
<th scope="col">p10/snircd</th>
<th scope="col">ts6/charybdis</th>
<th scope="col">ts6/chatircd</th>
<th scope="col">ts6/elemental</th>
<th scope="col">ts6/ratbox</th>
<th scope="col">unreal</th>
</tr>
<tr>
<th scope="row">admin</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
<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-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>
<th scope="row">allowinvite</th>
<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>
<th scope="row">auditorium</th>
<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>
<th scope="row">autoop</th>
<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>
<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><td class="tablecell-yes">+b</td></tr>
<tr>
<th scope="row">banexception</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+e</td><td class="tablecell-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>
<th scope="row">blockcaps</th>
<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>
<th scope="row">blockcolor</th>
<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>
<th scope="row">blockhighlight</th>
<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>
<th scope="row">delayjoin</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_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>
<th scope="row">exemptchanops</th>
<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>
<th scope="row">filter</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_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>
<th scope="row">flood</th>
<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>
<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-na note">n/a</td><td class="tablecell-yes2">+f<br><span class="note">(chanmodes/floodprot)</span></td></tr>
<tr>
<th scope="row">freetarget</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+F</td><td class="tablecell-yes">+F</td><td class="tablecell-yes">+F</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">had_delayjoin</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+d</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">halfop</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+h</td><td class="tablecell-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>
<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-na note">n/a</td><td class="tablecell-yes">+u</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">hidequits</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+u</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">history</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
<th scope="row">invex</th>
<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>
<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><td class="tablecell-yes">+i</td></tr>
<tr>
<th scope="row">issecure</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+Z<br><span class="note">(chanmodes/issecure)</span></td></tr>
<tr>
<th scope="row">joinflood</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
<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><td class="tablecell-yes">+k</td></tr>
<tr>
<th scope="row">kicknorejoin</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+J</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">kicknorejoin_insp</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
<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-na note">n/a</td><td class="tablecell-yes">+L</td><td class="tablecell-yes">+L</td><td class="tablecell-yes">+L</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">limit</th>
<td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td></tr>
<tr>
<th scope="row">moderated</th>
<td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td></tr>
<tr>
<th scope="row">netadminonly</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
<th scope="row">nickflood</th>
<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>
<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-na note">n/a</td><td class="tablecell-yes">+T</td><td class="tablecell-yes">+T</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">noctcp</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-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>
<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-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>
<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-na note">n/a</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+Q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">noinvite</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
<th scope="row">nokick</th>
<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>
<th scope="row">noknock</th>
<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>
<th scope="row">nonick</th>
<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>
<th scope="row">nonotice</th>
<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>
<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-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>
<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><td class="tablecell-yes">+o</td></tr>
<tr>
<th scope="row">operonly</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+O</td><td class="tablecell-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>
<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-na note">n/a</td><td class="tablecell-yes">+A</td><td class="tablecell-yes">+A</td><td class="tablecell-yes">+A</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">oplevel_upass</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+U</td><td class="tablecell-yes">+U</td><td class="tablecell-yes">+U</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">opmoderated</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
<th scope="row">owner</th>
<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>
<th scope="row">paranoia</th>
<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>
<th scope="row">permanent</th>
<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>
<th scope="row">private</th>
<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>
<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-partial">(via extban m:)</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-partial">(via extban ~q:)</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+q</td><td class="tablecell-yes">+q</td><td class="tablecell-yes">+q</td><td class="tablecell-na note">n/a</td><td class="tablecell-partial">(via extban ~q:)</td></tr>
<tr>
<th scope="row">redirect</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
<th scope="row">registered</th>
<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>
<th scope="row">regmoderated</th>
<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>
<th scope="row">regonly</th>
<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>
<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-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>
<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-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>
<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><td class="tablecell-yes">+s</td></tr>
<tr>
<th scope="row">sslonly</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+S</td><td class="tablecell-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>
<th scope="row">stripcolor</th>
<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>
<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><td class="tablecell-yes">+t</td></tr>
<tr>
<th scope="row">voice</th>
<td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td></tr>
<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>
</body>
</html>

View File

@ -0,0 +1,40 @@
Extban / IRCd,inspircd,p10/nefarious,ts6/charybdis,unreal
ban_account,R:,~a:,$a:,~a:
ban_account_legacy,,,,~R:
ban_all_opers,,,$o,
ban_all_registered,,,$a,
ban_all_ssl,,,$z,
ban_banshare,,~j:,$j:,
ban_blockcaps,B:,,,
ban_blockcolor,c:,,,
ban_certfp,z:,,,~S:
ban_extgecos,,,$x:,
ban_inchannel,j:,~c:,$c:,~c:
ban_invites,A:,,,
ban_mark,,~m:,,
ban_noctcp,C:,,,
ban_nojoins,,,,~j:
ban_nokicks,Q:,,,
ban_nonick,N:,~n:,,~n:
ban_nonotice,T:,,,~m:notice: (+e only)
ban_not_account,,,$~a:,
ban_not_banshare,,,$~j:,
ban_not_extgecos,,,$~x:,
ban_not_inchannel,,,$~c:,
ban_not_opers,,,$~o,
ban_not_realname,,,$~r:,
ban_not_server,,,$~s:,
ban_not_ssl,,,$~z,
ban_opertype,O:,,,~O:
ban_partmsgs,p:,,,
ban_realname,r:,~r:,$r:,~r:
ban_server,s:,,$s:,
ban_stripcolor,S:,,,~m:color: (+e only)
ban_unregistered,,,$~a,
ban_unregistered_mark,,~M:,,
ban_unregistered_matching,U:,,,
msgbypass_external,,,,~m:external:
msgbypass_censor,,,,~m:censor:
msgbypass_moderated,,,,~m:moderated:
quiet,m:,~q:,(via cmode +q),~q:
timedban_unreal,,,,~t:
1 Extban / IRCd inspircd p10/nefarious ts6/charybdis unreal
2 ban_account R: ~a: $a: ~a:
3 ban_account_legacy ~R:
4 ban_all_opers $o
5 ban_all_registered $a
6 ban_all_ssl $z
7 ban_banshare ~j: $j:
8 ban_blockcaps B:
9 ban_blockcolor c:
10 ban_certfp z: ~S:
11 ban_extgecos $x:
12 ban_inchannel j: ~c: $c: ~c:
13 ban_invites A:
14 ban_mark ~m:
15 ban_noctcp C:
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:
23 ban_not_inchannel $~c:
24 ban_not_opers $~o
25 ban_not_realname $~r:
26 ban_not_server $~s:
27 ban_not_ssl $~z
28 ban_opertype O: ~O:
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:

191
docs/modelists/extbans.html Normal file
View File

@ -0,0 +1,191 @@
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<meta name=viewport content="width=device-width, initial-scale=1">
<head>
<title>Supported Extbans for PyLink</title>
<style>
html {
background-color: white;
}
.note {
color: #555555;
}
/* (╮°-°)╮┳━┳ */
table, th, td {
border: 1px solid black;
}
td, th {
text-align: center;
padding: 3px;
}
td:first-child, th[scope="row"] {
text-align: left;
}
/* Table cells */
.tablecell-yes {
background-color: #A7F2A5
}
.tablecell-no {
background-color: #F08496
}
.tablecell-na {
background-color: #F0F0F0
}
.tablecell-planned, .tablecell-yes2 {
background-color: #B1FCDE
}
.tablecell-partial {
background-color: #EDE8A4
}
.tablecell-special {
background-color: #DCB1FC
}
</style>
</head>
<body>
<table><tr>
<th scope="col">Extban / IRCd</th>
<th scope="col">inspircd</th>
<th scope="col">p10/nefarious</th>
<th scope="col">ts6/charybdis</th>
<th scope="col">unreal</th>
</tr>
<tr>
<th scope="row">ban_account</th>
<td class="tablecell-yes">R:</td><td class="tablecell-yes">~a:</td><td class="tablecell-yes">$a:</td><td class="tablecell-yes">~a:</td></tr>
<tr>
<th scope="row">ban_account_legacy</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">~R:</td></tr>
<tr>
<th scope="row">ban_all_opers</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$o</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_all_registered</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_all_ssl</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$z</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_banshare</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">~j:</td><td class="tablecell-yes">$j:</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_blockcaps</th>
<td class="tablecell-yes">B:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_blockcolor</th>
<td class="tablecell-yes">c:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_certfp</th>
<td class="tablecell-yes">z:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">~S:</td></tr>
<tr>
<th scope="row">ban_extgecos</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$x:</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_inchannel</th>
<td class="tablecell-yes">j:</td><td class="tablecell-yes">~c:</td><td class="tablecell-yes">$c:</td><td class="tablecell-yes">~c:</td></tr>
<tr>
<th scope="row">ban_invites</th>
<td class="tablecell-yes">A:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_mark</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">~m:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_noctcp</th>
<td class="tablecell-yes">C:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_nojoins</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">~j:</td></tr>
<tr>
<th scope="row">ban_nokicks</th>
<td class="tablecell-yes">Q:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_nonick</th>
<td class="tablecell-yes">N:</td><td class="tablecell-yes">~n:</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">~n:</td></tr>
<tr>
<th scope="row">ban_nonotice</th>
<td class="tablecell-yes">T:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">~m:notice:<br><span class="note">(+e only)</span></td></tr>
<tr>
<th scope="row">ban_not_account</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~a:</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_not_banshare</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~j:</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_not_extgecos</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~x:</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_not_inchannel</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~c:</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_not_opers</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~o</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_not_realname</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~r:</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_not_server</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~s:</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_not_ssl</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~z</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_opertype</th>
<td class="tablecell-yes">O:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">~O:</td></tr>
<tr>
<th scope="row">ban_partmsgs</th>
<td class="tablecell-yes">p:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_realname</th>
<td class="tablecell-yes">r:</td><td class="tablecell-yes">~r:</td><td class="tablecell-yes">$r:</td><td class="tablecell-yes">~r:</td></tr>
<tr>
<th scope="row">ban_server</th>
<td class="tablecell-yes">s:</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$s:</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_stripcolor</th>
<td class="tablecell-yes">S:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
<th scope="row">ban_unregistered_mark</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">~M:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">ban_unregistered_matching</th>
<td class="tablecell-yes">U:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">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>
<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>
</body>
</html>

View File

@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""
Generates HTML versions of the mode list .csv definitions.
"""
import csv
import os
import os.path
os.chdir(os.path.dirname(__file__))
FILES = {
'user-modes.csv': 'Supported User Modes for PyLink',
'channel-modes.csv': 'Supported Channel Modes for PyLink',
'extbans.csv': 'Supported Extbans for PyLink',
}
def _write(outf, text):
print(text, end='')
outf.write(text)
def _format(articlename, text):
# More formatting
if text:
if text.startswith('('):
text = '<td class="tablecell-partial">%s</td>' % text
else:
if 'modes' in articlename:
text = '+' + text
try:
text, note = text.split(' ', 1)
except ValueError:
if text.endswith('*'):
text = '<td class="tablecell-special">%s</td>' % text
else:
text = '<td class="tablecell-yes">%s</td>' % text
else:
text = '%s<br><span class="note">%s</span>' % (text, note)
text = '<td class="tablecell-yes2">%s</td>' % text
else:
text = '<td class="tablecell-na note">n/a</td>'
return text
for fname, title in FILES.items():
outfname = os.path.splitext(fname)[0] + '.html'
print('Generating HTML for %s to %s:' % (fname, outfname))
with open(fname) as csvfile:
csvdata = csv.reader(csvfile)
with open(outfname, 'w') as outf:
# CSS in HTML in Python, how lovely...
_write(outf, """
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<meta name=viewport content="width=device-width, initial-scale=1">
<head>
<title>%s</title>
<style>
html {
background-color: white;
}
.note {
color: #555555;
}
/* (°-°) */
table, th, td {
border: 1px solid black;
}
td, th {
text-align: center;
padding: 3px;
}
td:first-child, th[scope="row"] {
text-align: left;
}
/* Table cells */
.tablecell-yes {
background-color: #A7F2A5
}
.tablecell-no {
background-color: #F08496
}
.tablecell-na {
background-color: #F0F0F0
}
.tablecell-planned, .tablecell-yes2 {
background-color: #B1FCDE
}
.tablecell-partial {
background-color: #EDE8A4
}
.tablecell-special {
background-color: #DCB1FC
}
</style>
</head>
<body>
<table>""" % title)
notes = False
for idx, row in enumerate(csvdata):
if not any(row): # Empty row
continue
elif row[0] == '----':
notes = True
continue
if notes:
_write(outf, "<p>%s</p>" % row[0])
continue
_write(outf, "<tr>\n")
for colidx, coltext in enumerate(row):
if idx == 0:
text = '<th scope="col">%s</th>\n' % coltext
elif colidx == 0:
text = '<th scope="row">%s</th>\n' % coltext
else:
text = _format(fname, coltext)
_write(outf, text)
_write(outf, "</tr>\n")
_write(outf, """
</table>
</body>
</html>""")

View File

@ -0,0 +1,55 @@
User Mode / IRCd,RFC 1459,hybrid,inspircd,ngircd,p10/ircu,p10/nefarious,p10/snircd,ts6/charybdis,ts6/chatircd,ts6/elemental,ts6/ratbox,unreal
admin,,a,,,,a,,a,a,a,a,
away,,,,a,,,,,,,,
bot,,,B (botmode),B,,B,,,B,B,,B (usermodes/bot)
callerid,,g,g (callerid),,,,,g,g,g,g,
censor,,,,,,,,,,,,G (usermodes/censor)
cloak,,x,x (cloaking),x,x,x,x,x,x,x,,x
cloak_fakehost,,,,,,f,,,,,,
cloak_hashedhost,,,,,,C,,,,,,
cloak_hashedip,,,,,,c,,,,,,
cloak_sethost,,,,,,h,h,,,,,
deaf,,D,d (deaf),,d,d,d,D,D,D,D,d
deaf_commonchan,,G,c (commonchans),C,,q,,,,,,
debug,,d,,,,,,,,,,
filter,,,,,,,,,,,,G
floodexempt,,,,F,,,,,,,,
helpop,,,h (helpop),,,,,,,,,
hidechans,,p,I (hidechans),I,,n,n,,,I,,p (usermodes/privacy)
hideidle,,q,,,,I,I,,,,,I
hideoper,,H,H (hideoper),,,H,,,,,,H
invisible,i,i,i,i,i,i,i,i,i,i,i,i
locops,,l,,,O,O,O,l,l,l,l,
netadmin,,,,,,,,,N,,,
noctcp,,,,,,,,,,C,,T (usermodes/noctcp)
noforward,,,L (redirect),,,L,,Q,Q,Q,,
noinvite,,,,,,,,,,V,,
oper,o,o,o,o,o,o,o,o,o,o,o,o
operwall,,,,,,,,z,z,z,z,
override,,,,,,X,X,p,p,p,,
privdeaf,,,,b,,D,,,,,,D (usermodes/privdeaf)
protected,,,,,,,,,,,,q (usermodes/nokick)
regdeaf,,R,R (services_account),,,R,R,R,R,R,,R (usermodes/regonlymsg)
registered,,r,r (services_account),R,r,r,r,,,,,r
restricted,,,,r,,,,,,,,
servprotect,,,k (servprotect),q,k,k,k,S,S,S,S,S (usermodes/servicebot)
showwhois,,,W (showwhois),,,W,,,,,,W (usermodes/showwhois)
sno_badclientconnections,,u,,,,,,,,,u,
sno_botfloods,,b,,,,,,,,,b,
sno_clientconnections,,c,,c,,,,,,,c,
sno_debug,,,,,g,g,g,,,,d,
sno_extclientconnections,,,,,,,,,,,C,
sno_fullauthblock,,f,,,,,,,,,f,
sno_nickchange,,n,,,,,,,,,,
sno_rejectedclients,,j,,,,,,,,,r,
sno_remoteclientconnections,,F,,,,,,,,,,
sno_serverconnects,,e,,,,,,,,,x,
sno_skill,,k,,,,,,,,,k,
sno_stats,,y,,,,,,,,,y,
snomask,s,s,s,s,s,s,s,s,s,s,s,s
ssl,,S,,,,z,,,,,,z
sslonlymsg,,,,,,,,,t,,,Z (usermodes/secureonlymsg)
stripcolor,,,S (stripcolor),,,,,,,,,
vhost,,,,,,,,,,,,t
wallops,w,w,w,w,w,w,w,w,w,w,w,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 (botmode) B B B B B (usermodes/bot)
5 callerid g g (callerid) g g g g
6 censor G (usermodes/censor)
7 cloak 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 (deaf) d d d D D D D d
13 deaf_commonchan G c (commonchans) C q
14 debug d
15 filter G
16 floodexempt F
17 helpop h (helpop)
18 hidechans p I (hidechans) I n n I p (usermodes/privacy)
19 hideidle q I I I
20 hideoper 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 (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 (usermodes/nokick)
32 regdeaf R R (services_account) R R R R R R (usermodes/regonlymsg)
33 registered r r (services_account) R r r r r
34 restricted r
35 servprotect k (servprotect) q k k k S S S S S (usermodes/servicebot)
36 showwhois W (showwhois) W W (usermodes/showwhois)
37 sno_badclientconnections u u
38 sno_botfloods b b
39 sno_clientconnections c c c
40 sno_debug g g g d
41 sno_extclientconnections C
42 sno_fullauthblock f f
43 sno_nickchange n
44 sno_rejectedclients j r
45 sno_remoteclientconnections F
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 sslonlymsg 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

@ -0,0 +1,244 @@
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<meta name=viewport content="width=device-width, initial-scale=1">
<head>
<title>Supported User Modes for PyLink</title>
<style>
html {
background-color: white;
}
.note {
color: #555555;
}
/* (╮°-°)╮┳━┳ */
table, th, td {
border: 1px solid black;
}
td, th {
text-align: center;
padding: 3px;
}
td:first-child, th[scope="row"] {
text-align: left;
}
/* Table cells */
.tablecell-yes {
background-color: #A7F2A5
}
.tablecell-no {
background-color: #F08496
}
.tablecell-na {
background-color: #F0F0F0
}
.tablecell-planned, .tablecell-yes2 {
background-color: #B1FCDE
}
.tablecell-partial {
background-color: #EDE8A4
}
.tablecell-special {
background-color: #DCB1FC
}
</style>
</head>
<body>
<table><tr>
<th scope="col">User Mode / IRCd</th>
<th scope="col">RFC 1459</th>
<th scope="col">hybrid</th>
<th scope="col">inspircd</th>
<th scope="col">ngircd</th>
<th scope="col">p10/ircu</th>
<th scope="col">p10/nefarious</th>
<th scope="col">p10/snircd</th>
<th scope="col">ts6/charybdis</th>
<th scope="col">ts6/chatircd</th>
<th scope="col">ts6/elemental</th>
<th scope="col">ts6/ratbox</th>
<th scope="col">unreal</th>
</tr>
<tr>
<th scope="row">admin</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+a</td><td class="tablecell-yes">+a</td><td class="tablecell-yes">+a</td><td class="tablecell-yes">+a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">away</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">bot</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
<th scope="row">callerid</th>
<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>
<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-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>
<th scope="row">cloak</th>
<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>
<th scope="row">cloak_fakehost</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+f</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">cloak_hashedhost</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">cloak_hashedip</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">cloak_sethost</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+h</td><td class="tablecell-yes">+h</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">deaf</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+D</td><td class="tablecell-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>
<th scope="row">deaf_commonchan</th>
<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>
<th scope="row">debug</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+d</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">filter</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+G</td></tr>
<tr>
<th scope="row">floodexempt</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+F</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">helpop</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
<th scope="row">hidechans</th>
<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>
<th scope="row">hideidle</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td></tr>
<tr>
<th scope="row">hideoper</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+H</td><td class="tablecell-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>
<th scope="row">invisible</th>
<td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td></tr>
<tr>
<th scope="row">locops</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+l</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+O</td><td class="tablecell-yes">+O</td><td class="tablecell-yes">+O</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">netadmin</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+N</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">noctcp</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+T<br><span class="note">(usermodes/noctcp)</span></td></tr>
<tr>
<th scope="row">noforward</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
<th scope="row">noinvite</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+V</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">oper</th>
<td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td></tr>
<tr>
<th scope="row">operwall</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+z</td><td class="tablecell-yes">+z</td><td class="tablecell-yes">+z</td><td class="tablecell-yes">+z</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">override</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+X</td><td class="tablecell-yes">+X</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">privdeaf</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
<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-yes2">+q<br><span class="note">(usermodes/nokick)</span></td></tr>
<tr>
<th scope="row">regdeaf</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+R</td><td class="tablecell-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>
<th scope="row">registered</th>
<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>
<th scope="row">restricted</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">servprotect</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-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>
<th scope="row">showwhois</th>
<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>
<th scope="row">sno_badclientconnections</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+u</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+u</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">sno_botfloods</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+b</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+b</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">sno_clientconnections</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">sno_debug</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+d</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">sno_extclientconnections</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">sno_fullauthblock</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+f</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+f</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">sno_nickchange</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+n</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">sno_rejectedclients</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+j</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">sno_remoteclientconnections</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+F</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">sno_serverconnects</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+e</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+x</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">sno_skill</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+k</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+k</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">sno_stats</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+y</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+y</td><td class="tablecell-na note">n/a</td></tr>
<tr>
<th scope="row">snomask</th>
<td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td></tr>
<tr>
<th scope="row">ssl</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+S</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+z</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+z</td></tr>
<tr>
<th scope="row">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>
<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>
<th scope="row">vhost</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+t</td></tr>
<tr>
<th scope="row">wallops</th>
<td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-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>
<th scope="row">webirc</th>
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+W</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
</table>
</body>
</html>

View File

@ -0,0 +1,121 @@
# PyLink Permissions Reference
Below is a list of all the permissions defined by PyLink and its official plugins.
## PyLink Core
- `core.clearqueue` - Grants access to the `clearqueue` command.
- `core.load` - Grants access to the `load` command.
- `core.rehash` - Grants access to the `rehash` command.
- `core.reload` - Grants access to the `reload`, `load`, and `unload` commands. (This implies access to `load` and `unload` because `reload` is really just those two commands combined.)
- `core.shutdown` - Grants access to the `shutdown` command.
- `core.unload` - Grants access to the `unload` command.
## Automode
By default, Automode integrates with Relay by only allowing access lists to be created / manipulated on channels that are owned by a network via Relay.
- `automode.manage` OR `automode.manage.*`: ability to manage Automode (use `setacc` and `delacc`) on all channels on the network where the user is connected.
- `automode.manage.relay_owned`: ability to manage Automode on channels owned by the current network in Relay. If Relay isn't loaded or the channel in question isn't shared via Relay, this permission check FAILS. **With the default permissions set, this is granted to all opers.**
- `automode.manage.#channel`: ability to manage Automode on the specific given channel.
- `automode.list` OR `automode.list.*`: ability to list Automode on all channels. **With the default permissions set, this is granted to all opers.**
- `automode.list.relay_owned`: ability to list automode on channels owned via Relay. If Relay isn't loaded or the channel in question isn't shared via Relay, this permission check FAILS.
- `automode.list.#channel`: ability to list Automode access entries on the specific given channel.
- `automode.sync` OR `automode.sync.*`: ability to sync automode on all channels.
- `automode.sync.relay_owned`: ability to sync automode on channels owned via Relay. If Relay isn't loaded or the channel in question isn't shared via Relay, this permission check FAILS. **With the default permissions set, this is granted to all opers.**
- `automode.sync.#channel`: ability to sync automode on the specific given channel.
- `automode.clear` OR `automode.clear.*`: ability to clear automode on all channels.
- `automode.clear.relay_owned`: ability to clear automode on channels owned via Relay. If Relay isn't loaded or the channel in question isn't shared via Relay, this permission check FAILS.
- `automode.clear.#channel`: ability to clear automode on the specific given channel.
- `automode.savedb`: ability to save the automode DB.
Remote versions of the `manage`, `list`, `sync`, and `clear` commands also exist for cross-network manipulation (e.g. `automode.remotemanage.*`)
## Bots
- `bots.join` - Grants access to the `join` command. `bots.joinclient` is a deprecated alias for this, retained for compatibility with PyLink < 2.0-rc1.
- `bots.msg` - Grants access to the `msg` command.
- `bots.nick` - Grants access to the `nick` command.
- `bots.part` - Grants access to the `part` command.
- `bots.quit` - Grants access to the `quit` command.
- `bots.spawnclient` - Grants access to the `spawnclient` command.
## Changehost
- `changehost.applyhosts` - Grants access to the `applyhosts` command.
## Commands
- `commands.echo` - Grants access to the `echo` command.
- `commands.loglevel` - Grants access to the `loglevel` command.
- `commands.logout.force` - Allows forcing logouts on other users via the `logout` command.
- `commands.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.status` - Grants access to the `status` command. **With the default permissions set, this is granted to all users.**
## Exec
- `exec.exec` - Grants access to the `exec` and `iexec` commands.
- `exec.eval` - Grants access to the `eval`, `ieval`, `peval`, and `pieval` commands.
- `exec.inject` - Grants access to the `inject` command.
- `exec.threadinfo` - Grants access to the `threadinfo` command.
## Global
- `global.global` - Grants access to the `global` command.
## Networks
- `networks.autoconnect` - Grants access to the `autoconnect` command.
- `networks.disconnect` - Grants access to the `disconnect` command.
- `networks.reloadproto` - Grants access to the `reloadproto` command.
- `networks.remote` - Grants access to the `remote` command.
## Opercmds
- `opercmds.checkban` - Grants access to the `checkban` command.
- `opercmds.checkban.re` - Grants access to the `checkbanre` command **if** the caller also has `opercmds.checkban`.
- `opercmds.chghost` - Grants access to the `chghost` command.
- `opercmds.chgident` - Grants access to the `chgident` command.
- `opercmds.chgname` - Grants access to the `chgname` command.
- `opercmds.jupe` - Grants access to the `jupe` command.
- `opercmds.kick` - Grants access to the `kick` command.
- `opercmds.kill` - Grants access to the `kill` command.
- `opercmds.massban` - Grants access to the `massban` command.
- `opercmds.massban.re` - Grants access to the `massbanre` command **if** the caller also has `opercmds.massban`.
- `opercmds.mode` - Grants access to the `mode` command.
- `opercmds.topic` - Grants access to the `topic` command.
## Raw
- `raw.raw` - Grants access to the `raw` command. `exec.raw` is equivalent to this and retained for compatibility with PyLink 1.x.
- `raw.raw.unsupported_network` - Allows use of the `raw` command on servers other than Clientbot.
## Relay
These permissions are granted to all opers when the `relay::allow_free_oper_links` option is set (this is the default):
- `relay.chandesc.remove` - Allows removing channel descriptions via the `chandesc` command.
- `relay.chandesc.set` - Allows setting / updating channel descriptions via the `chandesc` command.
- `relay.claim` - Grants access to the `claim` command.
- `relay.create` - Grants access to the `create` command.
- `relay.delink` - Grants access to the `delink` command.
- `relay.destroy` - Grants access to the `destroy` command.
- `relay.link` - Grants access to the `link` command.
These permissions are always granted to all opers:
- `relay.linkacl` - Allows managing LINKACL entries via the `linkacl` command.
- `relay.linkacl.view` - Allows viewing LINKACL entries via the `linkacl` command.
These permissions are not granted to anyone by default:
- `relay.destroy.remote` - Allows destroying remote channels.
- `relay.link.force_ts` - Grants access to the `link` command's `--force-ts` option (skip TS and target network is connected checks).
- `relay.linked` - Grants access to the `link` command. **With the default permissions set, this is granted to all users.**
- `relay.purge` - Grants access to the `purge` command.
- `relay.savedb` - Grants access to the `savedb` command.
## Servermaps
- `servermaps.localmap` - Grants access to the `localmap` command.
- `servermaps.map` - Grants access to the `map` command.
## Stats
- `stats.c`, `stats.o`, `stats.u` - Grants access to remote `/stats` calls with the corresponding letter.
- `stats.uptime` - Grants access to the `stats` command.

View File

@ -1,82 +1 @@
# Opering with PyLink Relay Moved to [relay-quickstart.md](relay-quickstart.md).
*This guide was written for the OVERdrive-IRC network, but may be applicable elsewhere.*
PyLink Relay behaves much like Janus, an extended service used to relay channels together. This guide goes over some of the basic oper commands in Relay, along with the best ways to handle channel emergencies.
## How nick suffixing work
When joining a relay channel, every user from another network will have a network tag attached to their name. The purpose of this is to prevent nick collisions from the same nick being used on multiple nets, and ensure that different networks' registered nicks remain separate.
How is this relevant? Firstly, it means that you **cannot ban users from entire networks** using banmasks such as `*/net1!*@*`! The nick suffix is something PyLink adds artificially; on `net1`'s IRCd, which is checking the bans locally, the nick suffix simply doesn't exist.
However, this *does* mean that you can effectively give access to remote users via services, by specifying masks such as `*/net1@someident@someperson.opers.somenet.org`. Just don't make masks too wide, or you risk getting channel takeovers.
## Relay commands
The concept of relay channels in PyLink is greatly inspired from the original Janus implementation, though with a few differences in command syntax.
To create a channel:
- `/msg PyLink create #channelname`
To link to a channel already created on a different network:
- `/msg PyLink link othernet #channelname`
You can also link remote channels to take a different name on your network. (This is the third argument to the LINK command)
- `/msg PyLink link othernet #lobby #othernet-lobby`
Also, to list the available channels:
- `/msg PyLink linked`
To remove a relay channel that you've created:
- `/msg PyLink destroy #channelname`
To delink a channel linked to another network:
- `/msg PyLink delink #channelname`
### Claiming channels
PyLink offers channel claims similarly to Janus, except that it is on by default when you create a channel on any network. Unless the claimed network list of a channel is EMPTY, oper override (MODE, KICK, TOPIC) will only be allowed from networks on that list.
To set a claim (note: for these commands, you must be on the network which created the channel in question!):
- `/msg PyLink claim #channel yournet,net2,net3` (the last parameter is a comma-separated list of networks, case-sensitive)
To list claims on a channel:
- `/msg PyLink claim #channel`
To remove claims from a channel
- `/msg PyLink claim #channel -`
### Access control for links (LINKACL)
LINKACL allows you to block certain networks from linking to your relay channels, based on a blacklist. By default, this blacklist is empty.
To list blocked networks for a channel:
- `/msg PyLink linkacl #channel list`
To add a network to the blacklist:
- `/msg PyLink linkacl #channel allow badnet`
To remove a network from the blacklist:
- `/msg PyLink linkacl #channel deny goodnet`
Whitelists with LINKACL are not supported at this time.
## Dealing with channel emergencies
PyLink is not designed with the ability to forward KILLs, G:Lines, or any network bans. **The best thing to do in the case of emergencies is to delink the problem networks / channels!** Kills are actively blocked by the PyLink daemon (user is just respawned), while X:Lines are simply ignored, as there isn't any code to handle them yet.
To delink another network from a channel your network owns:
- `/msg PyLink delink #yourchannel badnetwork`
To delink your network from a bad network's channel:
- `/msg PyLink delink #badchannel`
Basically, only one of the two above commands will work for one specific channel. Almost always, the network that owns a channel should be the one who has it registered via their services. You can see a list of channels by typing `/msg PyLink linked`.
## When a network starts causing disconnect spam
Juping an individual `net.relay` server will likely cause PyLink Relay to break or disconnect completely. When a network starts acting up and disconnecting frequently (and causing netsplit/quit floods), you should disable autoconnect for this network:
- `/msg PyLink autoconnect badnetwork -1` (setting autoconnect to 0 or below will cause it to be disabled)

146
docs/relay-quickstart.md Normal file
View File

@ -0,0 +1,146 @@
# PyLink Relay Quick Start
## What is Relay?
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.
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.
## Important notes (READ FIRST!)
### How nick suffixing work
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.
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)
- Rationale: it is common for a person to want to connect to multiple networks in a Relay instance, because they are still independent entities. You can still use IRCd-side clones prevention, which sanely blocks connections instead of killing / banning everyone involved.
- Anope: **SQLINE nicks should NOT be used**
- Rationale: Anope falls back to killing target clients matching a SQLINE, which will obviously cause conflicts with other services.
- Atheme: **The ChanFix service should be disabled**
- 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**
- 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`)
- Rationale: This will just make PyLink split off - you should instead [delink any problem networks / channels](#dealing-with-disputes-and-emergencies).
- Multiple PyLink Relay instances:
- **Do NOT connect a network twice to any PyLink instance**.
- **Do NOT connect a network to 2+ separate PyLink instances if there is another network already acting as a hub for them**.
- Not following these rules means that it's very easy for the Relay instances to go in a loop should an operator run the wrong command, which will hammer your CPU and relentlessly spam your channels.
Note: P10-specific services packages have not been particularly tested - your feedback is welcome.
## 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`
To create a channel on Relay:
- `/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:
- `/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 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`
To completely remove a relay channel (on the network that created it):
- `/msg PyLink destroy #channelname`
To delink a channel *linked to another network*:
- `/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:
- `/msg PyLink linked`
### Claiming channels
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).
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)
To list claim networks on a channel:
- `/msg PyLink claim #channel`
To clear the claim list for a channel:
- `/msg PyLink claim #channel -`
### Access control for links (LINKACL)
LINKACL allows you to 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:
- `/msg PyLink linkacl whitelist #channel true/false`
- Note that when you switch between LINKACL modes, the LINKACL entries from the previous mode are stored and stashed away. This means that you will get an empty LINKACL list in the new LINKACL mode if you haven't used it already, and that you can reload the previous LINKACL mode's entries by switching back to it at any point.
To view the LINKACL networks for a channel:
- `/msg PyLink linkacl #channel list`
To add a network to the whitelist **OR** remove a network from the blacklist:
- `/msg PyLink linkacl #channel allow goodnet`
To remove a network from the whitelist **OR** add a network to the blacklist:
- `/msg PyLink linkacl #channel deny badnet`
### Adding channel descriptions
Starting with PyLink 2.0, you can annotate your channels with a description to use in LINKED:
To view the description for a channel:
- `/msg PyLink chandesc #channel`
To change the description for a channel:
- `/msg PyLink chandesc #channel your text goes here`
To remove the description for a channel:
- `/msg PyLink chandesc #channel -`
## Dealing with disputes and emergencies
The best thing to do in the event of a dispute is to delink the problem networks / channels. 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
Special kill handling was introduced in PyLink 2.0, while in previous versions they were always bounced:
1) If the sender was a server and not a client, reject the kill. (This prevents services messups from wreaking havoc across the relay)
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.
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 are purposely not supported; see https://github.com/jlu5/PyLink/issues/521#issuecomment-352316396.
### Delinking channels
To delink another network from a channel your network owns:
- `/msg PyLink delink #yourchannel badnetwork`
To delink your network from a bad network's channel:
- `/msg PyLink delink #badchannel`
Basically, only one of the two above commands will work for one specific channel. Almost always, the network that owns a channel should be the one who has it registered via their services. You can see a list of channels by typing `/msg PyLink linked`.

View File

@ -1,25 +1,28 @@
# PyLink Developer Documentation # PyLink Developer Documentation
Please note that as PyLink is still in its development phase, its APIs are subject to change. This documentation is provided for reference only, and may not always be up to date as APIs change.
Any documentation here is provided for reference only. Patches are welcome if something looks wrong or *is* wrong. In such cases, consulting the source code is probably your best bet.
The docs are also really incomplete (contributors welcome!) The docs are also really incomplete (contributions are appreciated!)
## Introduction ## Introduction
PyLink is an a modular, plugin-based IRC services framework. It uses swappable protocol modules and a hooks system for calling plugins, allowing them to function regardless of the IRCd used. PyLink is an a modular, plugin-based IRC services framework. It uses swappable protocol modules and a hooks system for calling plugins, allowing them to function regardless of the IRCd used.
<img src="core-structure.png" width="50%" height="50%"> <img src="protocol-modules.png" width="50%" height="50%">
## Contents ## Contents
- [Writing plugins for PyLink](writing-plugins.md) - [Writing plugins for PyLink](writing-plugins.md)
- [PyLink protocol module specification](pmodule-spec.md)
- [PyLink hooks reference](hooks-reference.md) - [PyLink hooks reference](hooks-reference.md)
- [Supported named channel modes](channel-modes.csv)
- [Supported named user modes](user-modes.csv)
- [Services bot API/Creating your own service bots](services-api.md) - [Services bot API/Creating your own service bots](services-api.md)
- [Permissions API Introduction](permissions-api.md)
- [Using `utils.IRCParser()`](using-ircparser.md)
### Future topics (not yet available) ----
- [Writing tests for PyLink modules](writing-tests.md)
- [PyLink protocol module specification](pmodule-spec.md)
----
- [Release Process for PyLink](release-process.md)
![Graph of protocol module inheritance tree](protocol-modules.svg)

View File

@ -1,59 +0,0 @@
Channel Mode / IRCd,InspIRCd,charybdis,Elemental-IRCd,UnrealIRCd,IRCd-Hybrid,Nefarious IRCu
admin,a (m_customprefix/m_chanprotect),,a (when enabled),a,,
adminonly,,A (extensions/chm_adminonly),A (extensions/chm_adminonly.c),,,a
allowinvite,A (m_allowinvite),g,g,,,
autoop,w (m_autoop),,,,,
ban,b,b,b,b,b,b
banexception,e (m_banexception),e,e,e,e,e
blockcaps,B (m_blockcaps),,G (extensions/chm_nocaps.c),,,
blockcolor,c (m_blockcolor),,,c,c,c
delayjoin,,,,,,D
exemptchanops,X (m_exemptchanops),,,,,
filter,g (m_filter),,,,,
flood,f (m_messageflood),,,,,
flood_unreal,,,,f,,
freetarget,,F,F,,,
had_delayjoins,,,,,,d
halfop,h (m_customprefix/m_halfop),,h (when enabled),h,h,
hiddenbans,,,u,,,
hidequits,,,,,,Q
history,H (m_chanhistory),,,,,
invex,I (m_inviteexception),I,I,I,I,
inviteonly,i,i,i,i,i,i
issecure,,,,Z,,
joinflood,j (m_joinflood),j,j,,,
key,k,k,k,k,k,k
kicknorejoin,J (m_kicknorejoin),,J,,,
largebanlist,,L,L,,,
limit,l,l,l,l,l,l
moderated,m,m,m,m,m,m
nickflood,F (m_nickflood),,,,,
noamsg,,,,,,T
noctcp,C (m_noctcp),C,C,C,C,C
noextmsg,n,n,n,n,n,n
noforwards,,Q,Q,,,
noinvite,,,,,,
nokick,Q (m_nokicks),,E,Q,,
noknock,K (m_knock),p,,K,p,
nonick,N (m_nonicks),,d,N,,
nonotice,T (m_nonotice),T (extensions/chm_nonotice),T,T,,N
official-join,Y (m_ojoin),,,,,
op,o,o,o,o,o,o
operonly,O (m_operchans),O (extensions/chm_operonly),O (extensions/chm_operonly.c),O,O,O
oplevel_apass,,,,,,A
oplevel_upass,,,,,,U
opmoderated,U (extras/m_opmoderated),z,z,,,
owner,q (m_customprefix/m_chanprotect),,y (when enabled),q,,
permanent,P (m_permchannels),P,P,P,,z
private,p,,,p,,p
quiet,,q,q,,,
redirect,L (m_redirect),f,f,L,,L
registered,r (m_services_account),,,r,r,R
regmoderated,M (m_services_account),,,M,M,M
regonly,R (m_services_account),r,r,R,R,r
repeat,E (m_repeat),,K (extensions/chm_norepeat.c),,,
secret,s,s,s,s,s,s
sslonly,z (m_sslmodes),S (extensions/chm_sslonly),S (extensions/chm_sslonly.c),z,S,
stripcolor,S (m_stripcolor),c,c,S,,S
topiclock,t,t,t,t,t,t
voice,v,v,v,v,v,v
1 Channel Mode / IRCd InspIRCd charybdis Elemental-IRCd UnrealIRCd IRCd-Hybrid Nefarious IRCu
2 admin a (m_customprefix/m_chanprotect) a (when enabled) a
3 adminonly A (extensions/chm_adminonly) A (extensions/chm_adminonly.c) a
4 allowinvite A (m_allowinvite) g g
5 autoop w (m_autoop)
6 ban b b b b b b
7 banexception e (m_banexception) e e e e e
8 blockcaps B (m_blockcaps) G (extensions/chm_nocaps.c)
9 blockcolor c (m_blockcolor) c c c
10 delayjoin D
11 exemptchanops X (m_exemptchanops)
12 filter g (m_filter)
13 flood f (m_messageflood)
14 flood_unreal f
15 freetarget F F
16 had_delayjoins d
17 halfop h (m_customprefix/m_halfop) h (when enabled) h h
18 hiddenbans u
19 hidequits Q
20 history H (m_chanhistory)
21 invex I (m_inviteexception) I I I I
22 inviteonly i i i i i i
23 issecure Z
24 joinflood j (m_joinflood) j j
25 key k k k k k k
26 kicknorejoin J (m_kicknorejoin) J
27 largebanlist L L
28 limit l l l l l l
29 moderated m m m m m m
30 nickflood F (m_nickflood)
31 noamsg T
32 noctcp C (m_noctcp) C C C C C
33 noextmsg n n n n n n
34 noforwards Q Q
35 noinvite
36 nokick Q (m_nokicks) E Q
37 noknock K (m_knock) p K p
38 nonick N (m_nonicks) d N
39 nonotice T (m_nonotice) T (extensions/chm_nonotice) T T N
40 official-join Y (m_ojoin)
41 op o o o o o o
42 operonly O (m_operchans) O (extensions/chm_operonly) O (extensions/chm_operonly.c) O O O
43 oplevel_apass A
44 oplevel_upass U
45 opmoderated U (extras/m_opmoderated) z z
46 owner q (m_customprefix/m_chanprotect) y (when enabled) q
47 permanent P (m_permchannels) P P P z
48 private p p p
49 quiet q q
50 redirect L (m_redirect) f f L L
51 registered r (m_services_account) r r R
52 regmoderated M (m_services_account) M M M
53 regonly R (m_services_account) r r R R r
54 repeat E (m_repeat) K (extensions/chm_norepeat.c)
55 secret s s s s s s
56 sslonly z (m_sslmodes) S (extensions/chm_sslonly) S (extensions/chm_sslonly.c) z S
57 stripcolor S (m_stripcolor) c c S S
58 topiclock t t t t t t
59 voice v v v v v v

View File

@ -1,27 +0,0 @@
/* Graph for the PyLink Application Structure:
* Update using: dot -Tpng core-structure.dot > core-structure.png
*/
digraph G {
ratio = 0.8; /* make the graph wider than tall */
subgraph cluster_core {
label="PyLink Application Structure";
style="filled";
node [style="filled",color="white"];
color="lightblue";
"IRC objects" -> "Protocol modules" [label="Data relayed"]
"Protocol modules" -> "PyLink hooks" -> Plugins;
"IRC objects" -> "PyLink hooks";
"Main program" -> "IRC objects" [color=indigo] [label="One per network\nspawned"] [fontcolor=indigo];
"Main program" -> "IRC objects" [color=indigo];
"Main program" -> "IRC objects" [color=indigo];
"Protocol modules" -> "IRC objects" [label="States updated"] [color=darkgreen] [fontcolor=darkgreen];
"Main program" -> Plugins [label="Plugin loaders"];
}
"Protocol modules" -> "IRCds" -> "Protocol modules";
Plugins -> "Protocol modules" [label="Communication via\nIRC command\nsenders"] [color=navyblue] [fontcolor=navyblue];
Plugins -> "Main program" [label="Registers commands\n& hook handlers"] [color=brown] [fontcolor=brown];
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,13 +1,13 @@
# PyLink hooks reference # PyLink hooks reference
## Introduction ***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`.
1) **numeric**: The sender of the hook payload (normally a UID or SID). 1) **numeric**: The sender of the hook payload (normally a UID or SID).
2) **command**: The command name (hook name) of the payload. These are *always* UPPERCASE, and those starting with "PYLINK_" indicate hooks sent out by PyLink IRC objects themselves; i.e. they don't require protocol modules to handle them. 2) **command**: The command name (hook name) of the payload. These are *always* UPPERCASE, and those starting with "PYLINK_" indicate hooks sent out by PyLink IRC objects themselves (i.e. they don't require protocol modules to handle them).
3) **args**: The hook data (args), a Python `dict`, with different data keys and values depending on the command given. 3) **args**: The hook data (args), a Python `dict`, with different data keys and values depending on the command given.
@ -19,26 +19,17 @@ 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',
'MODE', 'MODE',
{'modes': [('+o', '38QAAAAAA')], {'modes': [('+o', '38QAAAAAA')],
'oldchan': IrcChannel({'modes': set(), 'channeldata': Channel(...),
'prefixmodes': {'admin': set(),
'halfop': set(),
'op': set(),
'owner': set(),
'voice': set()},
'topic': '',
'topicset': False,
'ts': 1451169448,
'users': {'38QAAAAAA', '001ZJZW01'}}),
'target': '#chat', 'target': '#chat',
'ts': 1451174702}] 'ts': 1451174702}]
``` ```
@ -52,8 +43,9 @@ These following hooks, sent with their correct data keys, are required for PyLin
- This payload should be sent whenever a server finishes its burst, with the SID of the bursted server as the sender. - This payload should be sent whenever a server finishes its burst, with the SID of the bursted server as the sender.
- The service bot API and plugins like relay use this to make sure networks are properly connected. Should ENDBURST not be sent or emulated, they will likely fail to spawn users entirely. - The service bot API and plugins like relay use this to make sure networks are properly connected. Should ENDBURST not be sent or emulated, they will likely fail to spawn users entirely.
- **PYLINK_DISCONNECT**: `{}` - **PYLINK_DISCONNECT**: `{'was_successful': False}`
- This is sent to plugins by IRC object instances whenever their network has disconnected. The sender here is always **None**. - This is sent to plugins by IRC object instances whenever their network has disconnected. The sender here is always **None**.
- The `was_successful` key shows whether the last connection before this message was successful (i.e. whether the disconnect wasn't caused by a configuration error, etc.)
## IRC command hooks ## IRC command hooks
@ -67,50 +59,53 @@ The following hooks represent regular IRC commands sent between servers.
- `modes` returns a list of parsed modes: `(mode character, mode argument)` tuples, where the mode argument is either `None` (for modes without arguments), or a string. - `modes` returns a list of parsed modes: `(mode character, mode argument)` tuples, where the mode argument is either `None` (for modes without arguments), or a string.
- The sender of this hook payload is IRCd-dependent, and is determined by whether the command was originally a SJOIN or regular JOIN - SJOIN is only sent by servers, and JOIN is only sent by users. - The sender of this hook payload is IRCd-dependent, and is determined by whether the command was originally a SJOIN or regular JOIN - SJOIN is only sent by servers, and JOIN is only sent by users.
- For IRCds that support joining multiple channels in one command (`/join #channel1,#channel2`), consecutive JOIN hook payloads of this format will be sent (one per channel). - For IRCds that support joining multiple channels in one command (`/join #channel1,#channel2`), consecutive JOIN hook payloads of this format will be sent (one per channel).
- For SJOIN, the `channeldata` key may also be sent, with a copy of the `classes.Channel` object *before* any mode changes from this burst command were processed.
- **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': args[1], '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 `IrcUser` instance, depending on the IRCd. On IRCds where QUITs are explicitly sent (InspIRCd), `userdata` will be `None`. Other IRCds do not explicitly send QUIT messages for KILLed clients, so the daemon must assume that they've quit, and deliver their last state to plugins that require this info. - `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')], 'oldchan': IrcChannel(...)}` - **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).
- `modes` is a list of prefixed parsed modes: `(mode character, mode argument)` tuples, but with `+/-` prefixes to denote whether each mode is being set or unset. - `modes` is a list of prefixed parsed modes: `(mode character, mode argument)` tuples, but with `+/-` prefixes to denote whether each mode is being set or unset.
- For channels, the `oldchan` key is also sent, with the state of the channel BEFORE this MODE hook was processed. - For channels, the `channeldata` key is also sent, with a copy of the `classes.Channel` object *before* this MODE hook was processed.
- One such use for this is to prevent oper-override hacks: checks for whether a sender is opped have to be done before the MODE is processed; otherwise, someone can simply op themselves and circumvent this detection. - One use for this is to prevent oper-override hacks: checks for whether a sender is opped have to be done before the MODE is processed; otherwise, someone can simply op themselves and circumvent this detection.
- **NICK**: `{'newnick': 'Alakazam', 'oldnick': 'Abracadabra', 'ts': 1234567890}` - **NICK**: `{'newnick': 'Alakazam', 'oldnick': 'Abracadabra', 'ts': 1234567890}`
- **NOTICE**: `{'target': 'UID3', 'text': 'hi there!'}` - **NOTICE**: `{'target': 'UID3', 'text': 'hi there!'}`
- *Note:* `target` can not only be a channel or a UID, but also a channel with a prefix attached (e.g. `@#lounge`). These cases should not be overlooked! - STATUSMSG targets (e.g. `@#lounge`) are also allowed here.
- **PART**: `{'channels': ['#channel1', '#channel2'], 'text': 'some reason'}` - **PART**: `{'channels': ['#channel1', '#channel2'], 'text': 'some reason'}`
- `text` can also be an empty string, as part messages are *optional* on IRC. - `text` can also be an empty string, as part messages are *optional* on IRC.
- Unlike the JOIN hook, multiple channels can be specified in a list for PART. This means that a user PARTing one channel will cause a payload to be sent with `channels` as a one-length *list* with the channel name. - Unlike the JOIN hook, multiple channels can be specified in a list for PART. This means that a user parting one channel will cause a payload to be sent with `channels` as a one-length *list* with the channel name.
- **PRIVMSG**: `{'target': 'UID3', 'text': 'hi there!'}` - **PRIVMSG**: `{'target': 'UID3', 'text': 'hi there!'}`
- Ditto with NOTICE: `target` can be a channel or a UID, or a channel with a prefix attached (e.g. `@#lounge`). - Ditto with NOTICE: STATUSMSG targets (e.g. `@#lounge`) are also allowed here.
- **QUIT**: `{'text': 'Quit: Bye everyone!'}` - **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': IrcServer(...)` - **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 `IrcServer` object of the server that was split. - `serverdata` provides the `classes.Server` object of the server that split off.
- `chandata` provides the channel index of the network before the netsplit was processed, allowing plugins to track who was affected by a netsplit in a channel specific way. - `channeldata` provides the channel index of the network before the netsplit was processed, allowing plugins to track who was affected by a netsplit in a channel specific way.
- **TOPIC**: `{'channel': channel, 'setter': numeric, 'text': 'Welcome to #Lounge!, 'oldtopic': 'Welcome to#Lounge!'}` - **TOPIC**: `{'channel': channel, 'setter': numeric, 'text': 'Welcome to #Lounge!, 'oldtopic': 'Welcome to#Lounge!'}`
- `oldtopic` denotes the original topic, and `text` indicates the new one being set. - `oldtopic` denotes the original topic, and `text` indicates the new one being set.
- `setter` is the raw sender field given to us by the IRCd; it may be a `nick!user@host`, a UID, a SID, a server name, or a nick. This is not processed any further. - `setter` is the raw sender field given to us by the IRCd; it may be a `nick!user@host`, a UID, a SID, a server name, or a nick. This is not processed at the protocol level.
- **UID**: `{'uid': 'UID1', 'ts': 1234567891, 'nick': 'supercoder', 'realhost': 'localhost', 'host': 'admin.testnet.local', 'ident': ident, 'ip': '127.0.0.1'}` - **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 by the IRCd) ### Extra commands (where supported)
- **AWAY**: `{'text': text}` - **AWAY**: `{'text': text}`
- `text` denotes the away reason. It is an empty string (`''`) when a user is unsetting their away status. - `text` denotes the away reason. It is an empty string (`''`) when a user is unsetting their away status.
@ -124,13 +119,14 @@ The following hooks represent regular IRC commands sent between servers.
- **CHGNAME**: `{'target': 'UID2', 'newgecos': "I ain't telling you!"}` - **CHGNAME**: `{'target': 'UID2', 'newgecos': "I ain't telling you!"}`
- SETNAME and CHGNAME commands, where available, both share this hook name. - SETNAME and CHGNAME commands, where available, both share this hook name.
- **INVITE**: `{'target': 'UID3', 'channel': '#myroom'}` - **INVITE**: `{'target': 'UID3', 'channel': '#hello'}`
- **KNOCK**: `{'text': 'let me in please!', 'channel': '#myroom'}` - **KNOCK**: `{'text': 'let me in please!', 'channel': '#hello'}`
- This is not actually implemented by any protocol module as of writing. - This is not actually implemented by any protocol module as of writing.
- **SAVE**: `{'target': 'UID8', 'ts': 1234567892, 'oldnick': 'Abracadabra'}` - **SAVE**: `{'target': 'UID8', 'ts': 1234567892, 'oldnick': 'Abracadabra'}`
- For protocols that use TS6-style nick saving. During nick collisions, instead of killing the losing client, servers that support SAVE will send such a command targeting the losing client, which forces that user's nick to their UID. - For protocols that use TS6-style nick saving. During nick collisions, instead of killing the losing client, servers that support SAVE will send such a command targeting the losing client, which forces that user's nick to their UID.
- As of 1.1.x, SAVE is also used internally to alert plugins of nick collisions, when protocol modules receive a user introduction for a nick that already exists.
- **SVSNICK**: `{'target': 'UID1', 'newnick': 'abcd'}` - **SVSNICK**: `{'target': 'UID1', 'newnick': 'abcd'}`
- PyLink does not comply with SVSNICK requests, but instead forwards it to plugins that listen for it. - PyLink does not comply with SVSNICK requests, but instead forwards it to plugins that listen for it.
@ -138,11 +134,11 @@ The following hooks represent regular IRC commands sent between servers.
- **VERSION**: `{}` - **VERSION**: `{}`
- This is used for protocols that send VERSION requests between servers when a client requests it (e.g. `/raw version pylink.local`). - This is used for protocols that send VERSION requests between servers when a client requests it (e.g. `/raw version pylink.local`).
- `coreplugin` automatically handles this by responding with a 351 numeric, with the data being the output of `utils.fullVersion(irc)`. - `coremods/handlers.py` automatically handles this by responding with a 351 numeric, with the data being the output of `irc.version()`.
- **WHOIS**: `{'target': 'UID1'}` - **WHOIS**: `{'target': 'UID1'}`
- On protocols supporting it (everything except InspIRCd), the WHOIS command is sent between servers for remote WHOIS requests. - On protocols supporting it (everything except InspIRCd), the WHOIS command is sent between servers for remote WHOIS requests.
- This requires servers to respond with a complete WHOIS reply (using all the different numerics), as done in `coreplugin`. - PyLink has built-in handling for this via `coremods/handlers.py` - plugins wishing to add custom WHOIS text should use the PYLINK_CUSTOM_WHOIS hook below.
## Hooks that don't map to IRC commands ## Hooks that don't map to IRC commands
Some hooks do not map directly to IRC commands, but to events that protocol modules should handle. Some hooks do not map directly to IRC commands, but to events that protocol modules should handle.
@ -159,9 +155,26 @@ Some hooks do not map directly to IRC commands, but to events that protocol modu
- The sender here is always **None**. - The sender here is always **None**.
- **PYLINK_CUSTOM_WHOIS**: `{'target': UID1, 'server': SID1}` - **PYLINK_CUSTOM_WHOIS**: `{'target': UID1, 'server': SID1}`
- This hook is called by `coreplugin` during its WHOIS handling process, to allow plugins to provide custom WHOIS information. The `target` field represents the target UID, while the `server` field represents the SID that should be replying to the WHOIS request. The source of the payload is the user using `/whois`. - This hook is called by `coremods/handlers.py` during its WHOIS handling process, to allow plugins to provide custom WHOIS information. The `target` field represents the target UID, while the `server` field represents the SID that should be replying to the WHOIS request. The source of the payload is the user using `/whois`.
- Plugins wishing to implement this should use the standard WHOIS numerics, using `irc.proto.numeric()` to reply to the source from the given server. - Plugins wishing to implement this should use the standard WHOIS numerics, using `irc.numeric()` to reply to the source from the given server.
- This hook replaces the pre-0.8 fashion of defining custom WHOIS handlers, which was non-standard and poorly documented. - This hook replaces the pre-0.8.x fashion of defining custom WHOIS handlers, which was never standardized and poorly documented.
## Commands handled WITHOUT hooks ## Commands handled WITHOUT hooks
At this time, commands that are handled by protocol modules without returning any hook data include PING, PONG, and various commands sent during the initial server linking phase. 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
* 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)
- Version bump for 2.0 stable release; no meaningful content changes.
* 2018-01-13 (2.0-alpha2)
- Replace `IrcChannel`, `IrcUser`, and `IrcServer` with their new class names (`classes.Channel`, `classes.User`, and `classes.Server`)
- Replace `irc.fullVersion()` with `irc.version()`
- Various minor wording tweaks.
* 2017-02-24 (1.2-dev)
- The `was_successful` key was added to PYLINK_DISCONNECT.

View File

@ -0,0 +1,33 @@
# The Permissions API
Permissions were introduced in PyLink 1.0 as a way for plugins to manage command access, replacing the old `irc.checkAuthenticated()`. The permissions system in PyLink is fairly simple, globally assigning a list of permissions to each hostmask/exttarget.
Permissions conventionally take the format `pluginname.commandname.optional_extra_portion(s)`, and support globs† in matching. Permission nodes are case-insensitive and casemapping aware, but are defined as being all lowercase for consistency.
The permissions module is available as `pylinkirc.coremods.permissions`. Usually, plugins import it this format:
```python
from pylinkirc.coremods import permissions
```
† The globbing used by the permissions module is just generic IRC-style globbing. For example, anyone with `*`, `perm.*`, `perm.?`, `*.1`, etc. in their permissions list will be allowed to use a command checking for a permission named `perm.1`.
## Checking for permissions
Individual functions check for permissions using the `permissions.checkPermissions(irc, source, ['perm.1', 'perm.2'])` function, where the last argument is an OR'ed list of permissions matched against a list of permission string globs that a user may have. If the user has any of the permissions in the permission list, they will be allowed to call the command. This function returns `True` when a permission check passes, and raises `utils.NotAuthorizedError` when a check fails, automatically aborting the execution of the command function.
`utils.NotAuthorizedError` can be treated like any other exception, so it's possible to wrap it around `try:` / `except:` for more complex access checking ([example in the Automode plugin](https://github.com/jlu5/PyLink/blob/1.1.1/plugins/automode.py#L64-L68)).
## Assigning default permissions
Plugins are also allowed to assign default permissions to their commands, though this should be used sparingly to ensure maximum configurability (explicitly removing permissions isn't supported yet). Default permissions are specified as a `dict` mapping targets to permission lists.
Example of this in [Automode](https://github.com/jlu5/PyLink/blob/1.1-alpha1/plugins/automode.py#L38-L39):
```python
# The default set of Automode permissions.
default_permissions = {"$ircop": ['automode.manage.relay_owned', 'automode.sync.relay_owned',
'automode.list']}
```
Default permissions are registered in a plugin's `main()` function via `permissions.addDefaultPermissions(default_permissions_dict)`, and should always be erased on `die()` through `permissions.removeDefaultPermissions(default_permissions_dict)`.

View File

@ -1,46 +1,69 @@
# PyLink Protocol Module Specification # PyLink Protocol Module Specification
In PyLink, each protocol module is a single file consisting of a protocol class, and a global `Class` attribute that is set equal to it (e.g. `Class = InspIRCdProtocol`). These classes should be based off of either [`classes.Protocol`](https://github.com/GLolol/PyLink/blob/e4fb64aebaf542122c70a8f3a49061386a00b0ca/classes.py#L532), a boilerplate class that only defines a few basic things, or something like [`ts6_common.TS6BaseProtocol`](https://github.com/GLolol/PyLink/blob/0.9-alpha1/protocols/ts6_common.py). (`ts6_common.TS6BaseProtocol` includes elements of the TS6 protocol that are shared by the InspIRCd, UnrealIRCd, and TS6 protocols.) IRC objects load protocol modules by creating an instance of its main class, and sends it commands accordingly. ***Last updated for 3.1-dev (2021-06-15).***
## Tasks Starting with PyLink 2.x, a *protocol module* is any module containing a class derived from `PyLinkNetworkCore` (e.g. `InspIRCdProtocol`), along with a global `Class` attribute set equal to it (e.g. `Class = InspIRCdProtocol`). These modules do everything from managing connections to providing plugins with an API to send and receive data. New protocol modules may be implemented based off any of the classes in the following inheritance tree, with each containing a different amount of abstraction.
Protocol modules have some very important jobs. If any of these aren't done correctly, you will be left with a broken, desynced services server: ![[Protocol module inheritence graph]](protocol-modules.svg)
1) Handle incoming commands from the uplink IRCd. ## Starting Steps
2) Return [hook data](hooks-reference.md) for relevant commands, so that plugins can receive data from IRC. **Before you proceed, we highly recommend protocol module coders to get in touch with us** (e.g. via IRC at `#PyLink @ irc.overdrivenetworks.com`). Letting us know what you are working on can help coordinate coding efforts and better prepare for potential API breaks.
3) Make sure channel/user states are kept correctly. Joins, quits, parts, kicks, mode changes, nick changes, etc. should all be handled accurately. Note: The following notes in this section assume that you are working on some IRCd's server protocol, such that PyLink can spawn subservers and its own pseudoclients. If this is not the case, *virtual* clients and servers have to be spawned instead to emulate the correct state - the `clientbot` protocol module is a functional (though not very elegant) example of this.
4) Respond to both pings *and* pongs - the `irc.lastping` attribute **must** be set to the current time whenever a `PONG` is received from the uplink, so PyLink's doesn't [lag out the uplink thinking that it isn't responding to our pings](https://github.com/GLolol/PyLink/blob/e4fb64aebaf542122c70a8f3a49061386a00b0ca/classes.py#L309-L311). When writing new protocol modules, it is recommended to subclass from one of the following classes:
5) Implement a series of outgoing command functions, used by plugins to send commands to IRC. See the `Outbound commands` section below for a list of which ones are needed. ### `classes.IRCNetwork`
6) Set the threading.Event object `irc.connected` (via `irc.connected.set()`) when the protocol negotiation with the uplink is complete. This is important for plugins like relay which must check that links are ready before spawning clients, and they will fail to work if this is not set. `IRCNetwork` is the base IRC class which includes the state checking utilities from `PyLinkNetworkCore`, the generic IRC utilities from `PyLinkNetworkCoreWithUtils`, along with abstraction for establishing IRC connections and pinging the uplink at a set interval.
7) Check to see that RECVPASS is correct. Always. To use `classes.IRCNetwork`, the following functions must be defined:
## Core functions - `handle_events(self, data)`: given a line of text containing an IRC command, parse it and return a hook payload as specified in the [PyLink hooks reference](hooks-reference.md).
- In all of the official PyLink modules so far, handling for specific commands is delegated into submethods via [`getattr()`](https://github.com/jlu5/PyLink/blob/3922d44173593e4bcceae1218bbc6f267caa9fc1/protocols/ircs2s_common.py#L409-L412), and unknown commands are ignored.
- `post_connect(self)`: This method sends the server introduction commands to the uplink IRC server. This method replaces the `connect()` function defined by protocol modules prior to PyLink 2.x.
- `_ping_uplink(self)`: Sends a ping command to the uplink. No return value is expected / used.
The following functions *must* be implemented by any protocol module within its main class, since they are used by the IRC object internals. This class offers the most flexibility because the protocol module can choose how it wants to handle any command. However, because most IRC server protocols use the same RFC 1459-style message format, rewriting the entire event handler is often not worth doing. Instead, it may be better to use `IRCS2SProtocol`, as documented below, which includes a `handle_events` method which handles most cases (TS5/6, P10, and TS-less protocols such as ngIRCd).
- **`connect`**`(self)` - Initializes a connection to a server. - An exception to this general statement is `clientbot`, whose event handler also checks for unknown message senders and enumerates them when such a message is received.
- **`handle_events`**`(self, line)` - Handles inbound data (lines of text) from the uplink IRC server. Normally, this will pass commands to other command handlers within the protocol module, while dropping commands that are unrecognized (wildcard handling). But, it's really up to you how to structure your modules. You will want to be able to parse command arguments properly into a list: many protocols send RFC1459-style commands that can be parsed using the [`Protocol.parseArgs()`](https://github.com/GLolol/PyLink/blob/e4fb64aebaf542122c70a8f3a49061386a00b0ca/classes.py#L539) function. ### `protocols.ircs2s_common.IRCCommonProtocol`
- **`ping`**`(self, source=None, target=None)` - Sends a PING to a target server. Periodic PINGs are sent to our uplink automatically by the [`Irc()` `IRCCommonProtocol` (based off `IRCNetwork`) includes more IRC-specific methods such as parsers for ISUPPORT, as well as helper methods to parse arguments and recursively handle SQUIT. It also defines a default `_ping_uplink()` and incoming command handlers for commands that are the same across known protocols (AWAY, PONG, ERROR).
internals](https://github.com/GLolol/PyLink/blob/0.4.0-dev/classes.py#L267-L272); plugins shouldn't have to use this.
### Outgoing command functions `IRCCommonProtocol` does *not*, however, define an `handle_events` method.
- **`spawnClient`**`(self, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None, manipulatable=False)` - Spawns a client on the PyLink server. No nick collision / valid nickname checks are done by protocol modules, as it is up to plugins to make sure they don't introduce anything invalid. ### `protocols.ircs2s_common.IRCS2SProtocol`
- `modes` is a set of `(mode char, mode arg)` tuples in the form of [`utils.parseModes()` output](using-utils.md#parseModes). `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.
- `ident` and `host` default to "null", while `realhost` defaults to the same things as `host` if not defined.
- `realname` defaults to the real name specified in the PyLink config, if not given. ### `classes.PyLinkNetworkCoreWithUtils`
- `ts` defaults to the current time if not given.
- `opertype` (the oper type name, if applicable) defaults to the simple text of `IRC Operator`. `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.
(Unfortunately, this work is complicated, so please get in touch with us if you're stuck or want tips!)
### Other
For protocols that are closely related to existing ones, it may be wise to subclass off of an existing protocol class. For example, the `hybrid` and `ratbox` modules are based off of `ts6`. However, these protocol modules *do not guarantee API stability*, so we recommend letting us know of your intentions beforehand.
## Outgoing command functions
The methods defined below are integral to any protocol module, as they are needed by plugins to communicate with the rest of the world.
Unless otherwise noted, the camel-case variants of command functions (e.g. "`spawnClient`) are supported but deprecated. Protocol modules do *not* need to implement these aliases themselves; attempts to missing camel case functions are automatically coersed into their snake case variants via the [`structures.CamelCaseToSnakeCase`](https://github.com/jlu5/PyLink/blob/3922d44173593e4bcceae1218bbc6f267caa9fc1/structures.py#L172-L197) wrapper.
- **`spawn_client`**`(self, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None, manipulatable=False)` - Spawns a client on the PyLink server. No nick collision / valid nickname checks are done by protocol modules, as it is up to plugins to make sure they don't introduce anything invalid.
- `modes` is a list or set of `(mode char, mode arg)` tuples in the [PyLink mode format](#mode-formats).
- `ident` and `host` should default to "null", while `realhost` should default to the same things as `host` if not defined.
- `realname` should default to the real name specified in the PyLink config, if not given.
- `ts` should default to the current time if not given.
- `opertype` (the oper type name, if applicable) should default to the simple text of `IRC Operator`.
- The `manipulatable` option toggles whether the client spawned should be considered protected. Currently, all this does is prevent commands from plugins like `bots` from modifying these clients, but future client protections (anti-kill flood, etc.) may also depend on this. - The `manipulatable` option toggles whether the client spawned should be considered protected. Currently, all this does is prevent commands from plugins like `bots` from modifying these clients, but future client protections (anti-kill flood, etc.) may also depend on this.
- The `server` option optionally takes a SID of any PyLink server, and spawns the client on the one given. It will default to the root PyLink server. - The `server` option optionally takes a SID of any PyLink server, and spawns the client on the one given. It should default to the root PyLink server if not specified.
- **`join`**`(self, client, channel)` - Joins the given client UID given to a channel. - **`join`**`(self, client, channel)` - Joins the given client UID given to a channel.
@ -48,11 +71,11 @@ internals](https://github.com/GLolol/PyLink/blob/0.4.0-dev/classes.py#L267-L272)
- **`invite`**`(self, source, target, channel)` - Sends an INVITE from a PyLink client. - **`invite`**`(self, source, target, channel)` - Sends an INVITE from a PyLink client.
- **`kick`**`(self, source, channel, target, reason=None)` - Sends a kick from a PyLink client/server. - **`kick`**`(self, source, channel, target, reason=None)` - Sends a kick from a PyLink client/server. This should raise `NotImplementedError` if not supported by a protocol.
- **`kill`**`(self, source, target, reason)` - Sends a kill from a PyLink client/server. - **`kill`**`(self, source, target, reason)` - Sends a kill from a PyLink client/server. This should raise `NotImplementedError` if not supported by a protocol.
- **`knock`**`(self, source, target, text)` - Sends a KNOCK from a PyLink client. - **`knock`**`(self, source, target, text)` - Sends a KNOCK from a PyLink client. This should raise `NotImplementedError` if not supported by a protocol.
- **`message`**`(self, source, target, text)` - Sends a PRIVMSG from a PyLink client. - **`message`**`(self, source, target, text)` - Sends a PRIVMSG from a PyLink client.
@ -60,9 +83,11 @@ internals](https://github.com/GLolol/PyLink/blob/0.4.0-dev/classes.py#L267-L272)
- **`nick`**`(self, source, newnick)` - Changes the nick of a PyLink client. - **`nick`**`(self, source, newnick)` - Changes the nick of a PyLink client.
- **`notice`**`(self, source, target, text)` - Sends a NOTICE from a PyLink client. - **`oper_notice`**`(self, source, target)` - Sends a notice to all operators on the network.
- **`numeric`**`(self, source, numeric, target, text)` - Sends a raw numeric `numeric` with `text` from the `source` server to `target`. - **`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.
- **`part`**`(self, client, channel, reason=None)` - Sends a part from a PyLink client. - **`part`**`(self, client, channel, reason=None)` - Sends a part from a PyLink client.
@ -71,79 +96,205 @@ internals](https://github.com/GLolol/PyLink/blob/0.4.0-dev/classes.py#L267-L272)
- **`sjoin`**`(self, server, channel, users, ts=None, modes=set())` - Sends an SJOIN for a group of users to a channel. The sender should always be a Server ID (SID). TS is - **`sjoin`**`(self, server, channel, users, ts=None, modes=set())` - Sends an SJOIN for a group of users to a channel. The sender should always be a Server ID (SID). TS is
optional, and defaults to the one we've stored in the channel state if not given. `users` is a list of `(prefix mode, UID)` pairs. Example uses: optional, and defaults to the one we've stored in the channel state if not given. `users` is a list of `(prefix mode, UID)` pairs. Example uses:
- `sjoin('100', '#test', [('', '100AAABBC'), ('qo', 100AAABBB'), ('h', '100AAADDD')])` - `sjoin('100', '#test', [('', '100AAABBC'), ('qo', 100AAABBB'), ('h', '100AAADDD')])`
- `sjoin(self.irc.sid, '#test', [('o', self.irc.pseudoclient.uid)])` - `sjoin(self.sid, '#test', [('o', self.pseudoclient.uid)])`
- **`spawnServer`**`(self, name, sid=None, uplink=None, desc=None)` - Spawns a server off another PyLink server. `desc` (server description) defaults to the one in the config. `uplink` defaults to the main PyLink server, and `sid` (the server ID) is automatically generated if not given. Sanity checks for server name and SID validity ARE done by the protocol module here. - **`spawn_server`**`(self, name, sid=None, uplink=None, desc=None)` - Spawns a server off another PyLink server. `desc` (server description) defaults to the one in the config. `uplink` defaults to the main PyLink server, and `sid` (the server ID) is automatically generated if not given. Sanity checks for server name and SID validity ARE done by the protocol module here.
- **`squit`**`(self, source, target, text='No reason given')` - SQUITs a PyLink server. - **`squit`**`(self, source, target, text='No reason given')` - SQUITs a PyLink server.
- **`topic`**`(self, source, target, text)` - Sends a topic change from a PyLink client. - **`topic`**`(self, source, target, text)` - Sends a topic change from a PyLink *client.
- **`topicBurst`**`(self, source, target, text)` - Sends a topic change from a PyLink server. This is usually used on burst. - **`topic_burst`**`(self, source, target, text)` - Sends a topic change from a PyLink server. This is usually used on burst.
- **`updateClient`**`(self, source, field, text)` - Updates the ident, host, or realname of a PyLink client. `field` should be either "IDENT", "HOST", "GECOS", or - **`update_client`**`(self, source, field, text)` - Updates the ident, host, or realname of a PyLink client. `field` should be either "IDENT", "HOST", "GECOS", or "REALNAME". If changing the field given on the IRCd isn't supported, `NotImplementedError` should be raised.
"REALNAME". If changing the field given on the IRCd isn't supported, `NotImplementedError` should be raised.
## Things to note ## Special variables
### Special variables A protocol module should also set the following variables in each instance:
A protocol module should also set the following variables in their protocol class: - `self.casemapping`: a string (`'rfc1459'` or `'ascii'`) to determine which case mapping the IRCd uses.
- `self.hook_map`: this is a `dict`, which maps non-standard command names sent by the IRCd to those used by [PyLink hooks](hooks-reference.md).
- `self.casemapping`: set this to `rfc1459` (default) or `ascii` to determine which case mapping the IRCd uses. - Examples exist in the [UnrealIRCd](https://github.com/jlu5/PyLink/blob/1.0-beta1/protocols/unreal.py#L24-L27) and [InspIRCd](https://github.com/jlu5/PyLink/blob/1.0-beta1/protocols/inspircd.py#L25-L28) modules.
- `self.hook_map`: this is a `dict`, which maps non-standard command names sent by the IRCd to those that PyLink plugins use internally. - `self.conf_keys`: a set of strings determining which server configuration options a protocol module needs to function; see the [Configuration key validation](#configuration-key-validation) section below.
- Examples exist in the [UnrealIRCd](https://github.com/GLolol/PyLink/blob/0.5-dev/protocols/unreal.py#L22) and [InspIRCd](https://github.com/GLolol/PyLink/blob/0.5-dev/protocols/inspircd.py#L24) modules. - `self.cmodes` / `self.umodes`: These are mappings of named IRC modes (e.g. `inviteonly` or `moderated`) to a string list of mode letters, that should be either set during link negotiation or hardcoded into the protocol module. There are also special keys: `*A`, `*B`, `*C`, and `*D`, which **must** be set properly with a list of mode characters for that type of mode.
- `self.cmodes` / `self.umodes`: These are mappings of named IRC modes to mode letters, that should be either negotiated during link or preset in the `connect()` function of the protocol module. There are also special keys: `*A`, `*B`, `*C`, and `*D`, which should each be filled with a list of mode characters for that type of modes.
- Types of modes are defined as follows (from http://www.irc.org/tech_docs/005.html): - Types of modes are defined as follows (from http://www.irc.org/tech_docs/005.html):
- A = Mode that adds or removes a nick or address to a list. Always has a parameter. - A = Mode that adds or removes a nick or address to a list. Always has a parameter.
- B = Mode that changes a setting and always has a parameter. - B = Mode that changes a setting and always has a parameter.
- C = Mode that changes a setting and only has a parameter when set. - C = Mode that changes a setting and only has a parameter when set.
- D = Mode that changes a setting and never has a parameter. - D = Mode that changes a setting and never has a parameter.
- Examples in the TS6 protocol module: https://github.com/GLolol/PyLink/blob/cb3187c/protocols/ts6.py#L259-L300 - If not defined, these will default to modes defined by RFC 1459: https://github.com/jlu5/PyLink/blob/1.0-beta1/classes.py#L127-L152
- If not defined, these will default to modes defined by RFC 1459: https://github.com/GLolol/PyLink/blob/cb3187c/classes.py#L118-L152 - An example of mode mapping hardcoding can be found here: https://github.com/jlu5/PyLink/blob/1.0-beta1/protocols/ts6.py#L259-L311
- You can find a list of supported (named) channel modes [here](channel-modes.csv), and a list of user modes [here](user-modes.csv).
- `self.prefixmodes`: This defines a mapping of prefix modes (+o, +v, etc.) to their respective mode prefix. This will default to `{'o': '@', 'v': '+'}` (the standard op and voice) if not defined. - `self.prefixmodes`: This defines a mapping of prefix modes (+o, +v, etc.) to their respective mode prefix. This will default to `{'o': '@', 'v': '+'}` (the standard op and voice) if not defined.
- Example: `self.prefixmodes = {'o': '@', 'h': '%', 'v': '+'}` - Example: `self.prefixmodes = {'o': '@', 'h': '%', 'v': '+'}`
- `self.connected`: this is a `threading.Event` object that plugins use to determine if the network has finished bursting. Protocol modules should set this to True via `self.connected.set()` when ready.
### Topics ## PyLink Protocol capabilities
PyLink 1.2 introduced the concept of protocol-defined capabilities, so that plugins wishing to use IRCd-specific features don't have to hard code protocol modules by name. Protocol capabilities are defined in `self.protocol_caps` (a set of strings) and may be changed freely before `self.connected` is set. Individual capabilities are then checked by plugins via `irc.has_cap(capability_name)`.
When receiving or sending topics, there is a `topicset` attribute in the IRC channel (IrcChannel) object that should be set **True**. It simply denotes that a topic has been set in the channel at least once. As of writing, the following protocol capabilities (case-sensitive) are implemented:
(Relay uses this so it doesn't overwrite topics with empty ones during burst, when a relay channel initialize before the uplink has sent the topic for it) ### 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-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-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-ts` - determines whether channel and user timestamps are tracked (and not spoofed)
- `slash-in-hosts` - determines whether `/` is allowed in hostnames
- `slash-in-nicks` - determines whether `/` is allowed in nicks
- `ssl-should-verify` - determines whether TLS certificates should be checked for validity by default - this should be enabled for any protocol modules needing to verify a remote server (e.g. Clientbot or a non-IRC API endpoint), and disabled for most IRC S2S links (where self-signed certs are widespread)
- `underscore-in-hosts` - determines whether `_` is allowed in client hostnames (yes, support for this actually varies by IRCd)
- `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.)
- Note: enabling this in a protocol module lets `coremods/handlers` automatically clean up old channels for you!
New protocol capabilities are generally added when needed - see https://github.com/jlu5/PyLink/issues/620
### Abstraction defaults
For reference, the `IRCS2SProtocol` class defines the following by default:
- `can-host-relay`
- `can-spawn-clients`
- `can-track-servers`
- `has-ts`
Whereas `PyLinkNetworkCore` defines no capabilities (i.e. an empty set) by default.
## PyLink structures
In this section, `self` refers to the network object/protocol module instance itself (i.e. from its own perspective).
### Server, User, Channel classes
PyLink defines classes named `Server`, `User`, and `Channel` in the `classes` module, and stores dictionaries of these in the `servers`, `users`, and `channels` attributes of a protocol object respectively.
- `self.servers` is a dictionary mapping server IDs (SIDs) to `Server` objects. If a protocol module does not use SIDs, servers are stored by server name instead.
- `self.users` is a dictionary mapping user IDs (UIDs) to `User` objects. If a protocol module does not use UIDs, a pseudo UID (PUID) generator such as [`classes.PUIDGenerator`](https://github.com/jlu5/PyLink/blob/3922d44173593e4bcceae1218bbc6f267caa9fc1/classes.py#L1710-L1726) *must* be used instead.
- The rationale behind this is because plugins tracking user lists are not designed to remove and re-add users when they change their nicks.
- When sending text back to the protocol module, it may be helpful to use the [`_expandPUID()`](https://github.com/jlu5/PyLink/blob/4a363aee509c5a0488a38b9e60f93ec59a274c3c/classes.py#L1213-L1231) function in `PyLinkNetworkCoreWithUtils` to expand these pseudo-UIDs back to regular nicks.
- `self._channels` and `self.channels` are [IRC case-insensitive dictionaries](https://github.com/jlu5/PyLink/blob/4a363aee509c5a0488a38b9e60f93ec59a274c3c/structures.py#L114-L116) mapping channel names to Channel objects.
- The key difference between these two dictionaries is that `_channels` is powered by `classes.ChannelState` and creates new channels *automatically* when they are accessed by index. This makes writing protocol modules easier, as they can assume that the channels they wish to modify always exist (no chance of `KeyError`!).
- `self.channels`, on the other hand, does *not* implicitly create channels and is thus better suited for plugins.
The `Channel`, `User`, and `Server` classes are initiated as follows:
- `Channel(self, name)` - First arg is the protocol object, second is the channel name.
- `User(self, nick, ts, uid, server, ident='null', host='null', realname='PyLink dummy client', realhost='null', ip='0.0.0.0', manipulatable=False, opertype='IRC Operator')` - These arguments are essentially the same as `spawn_client()`'s.
- `Server(self, uplink, name, internal=False, desc="(None given)")`
- The `uplink` (type `str`) option sets the SID of the uplink server, or *None* for both the main PyLink server and its uplink.
- The `name` option sets the server name.
- The `internal` boolean sets whether the server is an internal PyLink server.
- The `desc` option sets the server description, when applicable.
#### Statekeeping specifics
- When a user is introduced, their UID must be added to both `self.users` and to the `users` set in the `Server` object hosting the user (`self.servers[SID].users`). The latter list is used internally to track SQUITs.
- When a user joins a channel, the channel name is added to the User object's `channels` set (`self.users[UID].channels`), as well as the Channel object's user list (`self.channels[CHANNELNAME].users`)
- When a user disconnects, the `_remove_client` helper method can be called on their UID to automatically remove them from the relevant Server object, as well as all channels they were in.
- When a user leaves a channel, the `Channel.remove_user()` method can be used to easily remove them from the channel state, and vice versa.
### Mode formats ### Mode formats
Modes are stored a special format in PyLink, different from raw mode strings in order to make them easier to parse. Mode strings can be turned into mode *lists*, which are used to represent mode changes in hooks, and when storing modes internally. Modes are stored not stored as strings, but lists of mode pairs in order to ease parsing. These lists of mode pairs are used both to represent mode changes in hooks and store modes internally.
`utils.parseModes(irc, target, split_modestring)` is used to convert mode strings to mode lists, where `irc` is the IRC object, `target` is the channel name or UID the mode is being set on, and `split_modestring` is the string of modes to parse, *split at each space* (meaning that it's really a list). `self.parse_modes(target, modestring)` is used to convert mode strings to mode lists. `target` is the channel name/UID the mode is being set on, while `modestring` takes either a string or string split by spaces (really a list).
- `utils.parseModes(irc, '#chat', ['+tHIs', '*!*@is.sparta'])` would give: - `self.parse_modes('#chat', ['+tHIs', '*!*@is.sparta'])` would give:
- `[('+t', None), ('+H', None), ('+I', '*!*@is.sparta'), ('+s', None)]` - `[('+t', None), ('+H', None), ('+I', '*!*@is.sparta'), ('+s', None)]`
Also, `parseModes` will automatically convert prefix mode targets from nicks to UIDs, and drop invalid modes settings. `parse_modes()` will also automatically convert prefix mode targets from nicks to UIDs, and drop any duplicate (already set) or invalid (e.g. missing argument) modes.
- `utils.parseModes(irc, '#chat', ['+ol', 'invalidnick'])`: - `self.parse_modes('#chat', ['+ol invalidnick'])`:
- `[]` - `[]`
- `utils.parseModes(irc, '#chat', ['+o', 'GLolol'])`: - `self.parse_modes('#chat', ['+o jlu5'])`:
- `[('+o', '001ZJZW01')]` - `[('+o', '001ZJZW01')]`
Then, a parsed mode list can be applied to channel name or UID using `utils.applyModes(irc, target, parsed_modelist)`. Afterwords, a parsed mode list can be applied to channel name or UID using `self.apply_modes(target, parsed_modelist)`.
Internally, modes are stored in channel and user objects as sets: `(userobj or chanobj).modes`: **Note**: for protocols that accept or reject mode changes based on TS (i.e. practically every IRCd), you will want to use [`updateTS(...)`](https://github.com/jlu5/PyLink/blob/master/classes.py#L1484-L1487) instead to only apply the modes if the source TS is lower.
Internally, modes are stored in `Channel` and `User` objects as sets, **with the `+` prefixing each mode character omitted**. These sets are accessed via the `modes` attribute:
``` ```
<+GLolol> PyLink-devel, eval irc.users[source].modes <+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)}
``` ```
*With the exception of channel prefix modes* (op, voice, etc.), which are stored as a dict of sets in `chanobj.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()}
``` ```
When a certain mode (e.g. owner) isn't supported on a network, the key still exists in `prefixmodes` but is simply unused. When a certain mode (e.g. owner) isn't supported on a network, the key still exists in `prefixmodes` but is simply unused.
You can see a list of supported (named) channel modes [here](channel-modes.csv), and a list of user modes [here](user-modes.csv). ### Topics
When receiving or sending topics, there is a `topicset` attribute in the `Channel` object that should be set to **True**. This boolean denotes that a topic has been set in the channel at least once; Relay uses it to know not to overwrite topics with empty ones during startup, when topics have not been received from all networks yet.
*Caveat:* Topic handlers on the current protocol modules do not follow TS rules (which vary by IRCd), and blindly accept data. See issue https://github.com/jlu5/PyLink/issues/277
## Configuration key validation
Starting with PyLink 1.x, protocol modules can specify which config values within a server block they need in order to work. This is done by adjusting the `self.conf_keys` attribute, usually in the protocol module's `__init__()` method. The default set, defined in [`Classes.Protocol`](https://github.com/jlu5/PyLink/blob/1.0-beta1/classes.py#L1202-L1204), includes `{'ip', 'port', 'hostname', 'sid', 'sidrange', 'protocol', 'sendpass', 'recvpass'}`. Should any of these keys be missing from a server block, PyLink will bail with a configuration error.
As an example, one protocol module that tweaks this is [`Clientbot`](https://github.com/jlu5/PyLink/blob/1.0-beta1/protocols/clientbot.py#L17-L18), which removes all options except `ip`, `protocol`, and `port`.
## The final checklist
In short, protocol modules have some very important jobs. If any of these aren't done correctly, you will be left with a very broken, desynced services server:
1) Handle incoming commands from the uplink.
2) Return [hook data](hooks-reference.md) for relevant commands, so that plugins can receive data from the uplink.
3) Make sure channel/user states are kept correctly. Joins, quits, parts, kicks, mode changes, nick changes, etc. should all be handled accurately where relevant.
4) Implement the specified outgoing command functions, which are used by plugins to send commands to the uplink.
5) Set the `threading.Event` instance `self.connected` to True (via `self.connected.set()`) when the connection with the uplink is fully established. This is important for Relay and the services API, which will refuse to initialize if the connection is not marked ready.
6) Check that `recvpass` is correct when applicable, and raise `ProtocolError` with a relevant error message if not.
7) Declare the correct set of protocol module capabilities to prevent confusing PyLink's plugins.
## Changes to this document
* 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)
- Version bump for 2.0 stable release; no meaningful content changes.
* 2018-06-26 (2.0-beta1)
- Added documentation for PyLink protocol capabilities
- Wording tweaks, restructured headings
- Consistently refer to protocol module attributes as `self.<whatever>` instead of `irc.<whatever>`
* 2018-05-09 (2.0-alpha3)
- `kill` and `kick` implementations should raise `NotImplementedError` if not supported (anti-desync measure).
- Future PyLink versions will further standardize which functions should be stubbed (no-op) when not available and which should raise an error.
* 2017-10-05 (2.0-alpha1)
- Added notes on user statekeeping and the tracking/helper functions used.
- Mention the `post_connect()` function that must be defined by protocols inheriting from IRCNetwork.
* 2017-08-30 (2.0-dev)
- Rewritten specification for the IRC-protocol class convergence in PyLink 2.0.
- Updated the spec for 2.0 method renames and class restructures.
- Added a proper "Starting Steps" section detailing which new classes inherit from and when.
- Explicitly document the Server, User, and Channel classes.
* 2017-03-15 (1.2-dev)
- Corrected the location of `self.cmodes/umodes/prefixmodes` attributes
- Mention `self.conf_keys` as a special variable for completeness
* 2017-01-29 (1.2-dev)
- NOTICE can now be sent from servers.
- This section was added.

View File

@ -1,28 +1,47 @@
/* Graph showing inheritance with the current PyLink protocol protocols: /* Graph showing inheritance with the current PyLink protocol modules:
* Update using: dot -Tpng protocol-modules.dot > protocol-modules.png * Update using: dot -Tsvg protocol-modules.dot > protocol-modules.svg
*/ */
digraph G { digraph G {
ratio = 0.8; /* make the graph wider than tall */
edge [ penwidth=0.75, color="#111111CC" ];
subgraph cluster_core {
label="Core classes (pylinkirc.classes)";
style="filled";
node [style="filled",color="white"];
color="#90EE90";
"PyLinkNetworkCore" -> "PyLinkNetworkCoreWithUtils" -> "IRCNetwork";
}
subgraph cluster_helper { subgraph cluster_helper {
label="Protocol module helpers"; label="Protocol module helpers\n(pylinkirc.protocols.ircs2s_common)";
style="filled"; style="filled";
node [style="filled",color="white"]; node [style="filled",color="white"];
color="lightblue"; color="lightblue";
"ircs2s_common.py" -> "ts6_common.py"; "IRCNetwork" -> "IRCCommonProtocol" -> "IRCS2SProtocol" -> "TS6BaseProtocol";
subgraph cluster_helper {
label="pylinkirc.protocols.ts6_common";
style="filled";
color="lightcyan";
"TS6BaseProtocol";
}
} }
subgraph cluster_pluggable { subgraph cluster_pluggable {
label="Pluggable (full) protocol modules"; label="Complete protocol modules (pylinkirc.protocols.*)";
style="filled"; style="filled";
node [style="filled",color="white"]; node [style="filled",color="white"];
color="khaki"; color="khaki";
"ircs2s_common.py" -> "nefarious.py"; "IRCS2SProtocol" -> "p10";
"ts6_common.py" -> "ts6.py" -> "hybrid.py"; "IRCS2SProtocol" -> "ngircd";
"ts6_common.py" -> "inspircd.py"; "TS6BaseProtocol" -> "ts6" -> "hybrid";
"ts6_common.py" -> "unreal.py"; "TS6BaseProtocol" -> "inspircd";
"clientbot.py"; "TS6BaseProtocol" -> "unreal";
"IRCCommonProtocol" -> "clientbot";
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
-->
<!-- Title: G Pages: 1 -->
<svg width="530pt" height="651pt"
viewBox="0.00 0.00 530.00 651.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 647)">
<title>G</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-647 526,-647 526,4 -4,4"/>
<g id="clust1" class="cluster"><title>cluster_core</title>
<polygon fill="#90ee90" stroke="#90ee90" points="77,-416 77,-635 333,-635 333,-416 77,-416"/>
<text text-anchor="middle" x="205" y="-619.8" font-family="Times,serif" font-size="14.00">Core classes (pylinkirc.classes)</text>
</g>
<g id="clust2" class="cluster"><title>cluster_helper</title>
<polygon fill="lightblue" stroke="lightblue" points="83,-163 83,-408 302,-408 302,-163 83,-163"/>
<text text-anchor="middle" x="192.5" y="-392.8" font-family="Times,serif" font-size="14.00">Protocol module helpers</text>
<text text-anchor="middle" x="192.5" y="-377.8" font-family="Times,serif" font-size="14.00">(pylinkirc.protocols.ircs2s_common)</text>
</g>
<g id="clust3" class="cluster"><title>cluster_helper</title>
<polygon fill="lightcyan" stroke="lightcyan" points="91,-171 91,-246 285,-246 285,-171 91,-171"/>
<text text-anchor="middle" x="188" y="-230.8" font-family="Times,serif" font-size="14.00">pylinkirc.protocols.ts6_common</text>
</g>
<g id="clust4" class="cluster"><title>cluster_pluggable</title>
<polygon fill="khaki" stroke="khaki" points="8,-8 8,-155 514,-155 514,-8 8,-8"/>
<text text-anchor="middle" x="261" y="-139.8" font-family="Times,serif" font-size="14.00">Complete protocol modules (pylinkirc.protocols.*)</text>
</g>
<!-- PyLinkNetworkCore -->
<g id="node1" class="node"><title>PyLinkNetworkCore</title>
<ellipse fill="white" stroke="white" cx="205" cy="-586" rx="84.485" ry="18"/>
<text text-anchor="middle" x="205" y="-582.3" font-family="Times,serif" font-size="14.00">PyLinkNetworkCore</text>
</g>
<!-- PyLinkNetworkCoreWithUtils -->
<g id="node2" class="node"><title>PyLinkNetworkCoreWithUtils</title>
<ellipse fill="white" stroke="white" cx="205" cy="-514" rx="119.679" ry="18"/>
<text text-anchor="middle" x="205" y="-510.3" font-family="Times,serif" font-size="14.00">PyLinkNetworkCoreWithUtils</text>
</g>
<!-- PyLinkNetworkCore&#45;&gt;PyLinkNetworkCoreWithUtils -->
<g id="edge1" class="edge"><title>PyLinkNetworkCore&#45;&gt;PyLinkNetworkCoreWithUtils</title>
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M205,-567.697C205,-559.983 205,-550.712 205,-542.112"/>
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="208.5,-542.104 205,-532.104 201.5,-542.104 208.5,-542.104"/>
</g>
<!-- IRCNetwork -->
<g id="node3" class="node"><title>IRCNetwork</title>
<ellipse fill="white" stroke="white" cx="205" cy="-442" rx="55.7903" ry="18"/>
<text text-anchor="middle" x="205" y="-438.3" font-family="Times,serif" font-size="14.00">IRCNetwork</text>
</g>
<!-- PyLinkNetworkCoreWithUtils&#45;&gt;IRCNetwork -->
<g id="edge2" class="edge"><title>PyLinkNetworkCoreWithUtils&#45;&gt;IRCNetwork</title>
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M205,-495.697C205,-487.983 205,-478.712 205,-470.112"/>
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="208.5,-470.104 205,-460.104 201.5,-470.104 208.5,-470.104"/>
</g>
<!-- IRCCommonProtocol -->
<g id="node4" class="node"><title>IRCCommonProtocol</title>
<ellipse fill="white" stroke="white" cx="205" cy="-344" rx="89.0842" ry="18"/>
<text text-anchor="middle" x="205" y="-340.3" font-family="Times,serif" font-size="14.00">IRCCommonProtocol</text>
</g>
<!-- IRCNetwork&#45;&gt;IRCCommonProtocol -->
<g id="edge3" class="edge"><title>IRCNetwork&#45;&gt;IRCCommonProtocol</title>
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M205,-423.837C205,-409.503 205,-388.807 205,-372.216"/>
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="208.5,-372.014 205,-362.014 201.5,-372.014 208.5,-372.014"/>
</g>
<!-- IRCS2SProtocol -->
<g id="node5" class="node"><title>IRCS2SProtocol</title>
<ellipse fill="white" stroke="white" cx="214" cy="-272" rx="69.5877" ry="18"/>
<text text-anchor="middle" x="214" y="-268.3" font-family="Times,serif" font-size="14.00">IRCS2SProtocol</text>
</g>
<!-- IRCCommonProtocol&#45;&gt;IRCS2SProtocol -->
<g id="edge4" class="edge"><title>IRCCommonProtocol&#45;&gt;IRCS2SProtocol</title>
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M207.225,-325.697C208.217,-317.983 209.408,-308.712 210.514,-300.112"/>
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="213.997,-300.469 211.801,-290.104 207.054,-299.576 213.997,-300.469"/>
</g>
<!-- clientbot -->
<g id="node13" class="node"><title>clientbot</title>
<ellipse fill="white" stroke="white" cx="464" cy="-106" rx="41.6928" ry="18"/>
<text text-anchor="middle" x="464" y="-102.3" font-family="Times,serif" font-size="14.00">clientbot</text>
</g>
<!-- IRCCommonProtocol&#45;&gt;clientbot -->
<g id="edge12" class="edge"><title>IRCCommonProtocol&#45;&gt;clientbot</title>
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M233.699,-326.923C251.178,-316.837 273.771,-303.27 293,-290 319.052,-272.023 326.338,-268.098 349,-246 386.799,-209.142 424.679,-160.559 446.146,-131.67"/>
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="449.147,-133.498 452.263,-123.372 443.512,-129.345 449.147,-133.498"/>
</g>
<!-- TS6BaseProtocol -->
<g id="node6" class="node"><title>TS6BaseProtocol</title>
<ellipse fill="white" stroke="white" cx="187" cy="-197" rx="72.2875" ry="18"/>
<text text-anchor="middle" x="187" y="-193.3" font-family="Times,serif" font-size="14.00">TS6BaseProtocol</text>
</g>
<!-- IRCS2SProtocol&#45;&gt;TS6BaseProtocol -->
<g id="edge5" class="edge"><title>IRCS2SProtocol&#45;&gt;TS6BaseProtocol</title>
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M207.738,-254.069C204.49,-245.287 200.448,-234.36 196.798,-224.49"/>
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="200.073,-223.257 193.322,-215.092 193.508,-225.685 200.073,-223.257"/>
</g>
<!-- p10 -->
<g id="node7" class="node"><title>p10</title>
<ellipse fill="white" stroke="white" cx="293" cy="-106" rx="27" ry="18"/>
<text text-anchor="middle" x="293" y="-102.3" font-family="Times,serif" font-size="14.00">p10</text>
</g>
<!-- IRCS2SProtocol&#45;&gt;p10 -->
<g id="edge6" class="edge"><title>IRCS2SProtocol&#45;&gt;p10</title>
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M269.936,-261.101C277.361,-257.496 284.089,-252.611 289,-246 313.082,-213.582 307.4,-164.181 300.569,-133.833"/>
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="303.928,-132.834 298.152,-123.952 297.129,-134.497 303.928,-132.834"/>
</g>
<!-- ngircd -->
<g id="node8" class="node"><title>ngircd</title>
<ellipse fill="white" stroke="white" cx="371" cy="-106" rx="33.2948" ry="18"/>
<text text-anchor="middle" x="371" y="-102.3" font-family="Times,serif" font-size="14.00">ngircd</text>
</g>
<!-- IRCS2SProtocol&#45;&gt;ngircd -->
<g id="edge7" class="edge"><title>IRCS2SProtocol&#45;&gt;ngircd</title>
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M276.712,-264.047C289.461,-260.278 302.009,-254.58 312,-246 345.593,-217.151 360.541,-165.861 366.824,-134.301"/>
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="370.276,-134.882 368.637,-124.415 363.391,-133.62 370.276,-134.882"/>
</g>
<!-- ts6 -->
<g id="node9" class="node"><title>ts6</title>
<ellipse fill="white" stroke="white" cx="43" cy="-106" rx="27" ry="18"/>
<text text-anchor="middle" x="43" y="-102.3" font-family="Times,serif" font-size="14.00">ts6</text>
</g>
<!-- TS6BaseProtocol&#45;&gt;ts6 -->
<g id="edge8" class="edge"><title>TS6BaseProtocol&#45;&gt;ts6</title>
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M137.801,-183.792C118.234,-177.355 96.3078,-168.023 79,-155 70.5984,-148.678 63.2254,-139.88 57.3761,-131.54"/>
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="60.2708,-129.572 51.8527,-123.138 54.4215,-133.417 60.2708,-129.572"/>
</g>
<!-- inspircd -->
<g id="node11" class="node"><title>inspircd</title>
<ellipse fill="white" stroke="white" cx="209" cy="-106" rx="38.9931" ry="18"/>
<text text-anchor="middle" x="209" y="-102.3" font-family="Times,serif" font-size="14.00">inspircd</text>
</g>
<!-- TS6BaseProtocol&#45;&gt;inspircd -->
<g id="edge10" class="edge"><title>TS6BaseProtocol&#45;&gt;inspircd</title>
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M191.242,-178.84C194.377,-166.158 198.707,-148.64 202.307,-134.077"/>
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="205.769,-134.655 204.771,-124.107 198.974,-132.975 205.769,-134.655"/>
</g>
<!-- unreal -->
<g id="node12" class="node"><title>unreal</title>
<ellipse fill="white" stroke="white" cx="120" cy="-106" rx="32.4942" ry="18"/>
<text text-anchor="middle" x="120" y="-102.3" font-family="Times,serif" font-size="14.00">unreal</text>
</g>
<!-- TS6BaseProtocol&#45;&gt;unreal -->
<g id="edge11" class="edge"><title>TS6BaseProtocol&#45;&gt;unreal</title>
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M174.398,-179.26C164.099,-165.579 149.413,-146.071 137.895,-130.772"/>
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="140.682,-128.653 131.871,-122.769 135.089,-132.863 140.682,-128.653"/>
</g>
<!-- hybrid -->
<g id="node10" class="node"><title>hybrid</title>
<ellipse fill="white" stroke="white" cx="50" cy="-34" rx="33.5952" ry="18"/>
<text text-anchor="middle" x="50" y="-30.3" font-family="Times,serif" font-size="14.00">hybrid</text>
</g>
<!-- ts6&#45;&gt;hybrid -->
<g id="edge9" class="edge"><title>ts6&#45;&gt;hybrid</title>
<path fill="none" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" d="M44.7303,-87.6966C45.5017,-79.9827 46.4288,-70.7125 47.2888,-62.1124"/>
<polygon fill="#111111" fill-opacity="0.800000" stroke="#111111" stroke-width="0.75" stroke-opacity="0.800000" points="50.7771,-62.403 48.2896,-52.1043 43.8118,-61.7064 50.7771,-62.403"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,13 @@
# Release Process for PyLink
This document documents the steps that I (James) use to release updates to PyLink.
1) Draft the next release's changelog in `RELNOTES.md`
2) Bump the version in the [`VERSION`](VERSION) file.
3) Commit the changes to `VERSION` and `RELNOTES.md`, and tag+sign this commit as the new release. Do not prefix version numbers with "v".
4) Publish the release via the GitHub release page, using the same changelog content as `RELNOTES.md`.
5) For stable releases, ~~also upload to PyPI: `python3 setup.py sdist upload`~~ PyPI uploads are handled automatically via Travis-CI.

View File

@ -1,10 +1,10 @@
# PyLink Services Bot API # PyLink Services Bot API
Starting with PyLink 0.9.x, a services bot API was introduced to make writing custom services slightly easier. PyLink's Services API automatically connects service bots, and handles rejoin on kick/kill all by itself, meaning less code is needed per plugin to have functional service bots. The goal of PyLink's Services API is to make writing custom services easier. It is able to automatically spawn service bots on connect, handle rejoins on kill and kick, and expose a way for plugins to bind commands to various services bots. It also handles U-line servprotect modes when enabled and supported on a particular network (i.e. the `protect_services` option).
## Creating new services ## Basic service creation
Services can be created (registered) using code similar to the following in a plugin: Services can be registered and created using code similar to the following in a plugin:
```python ```python
@ -14,40 +14,67 @@ from pylinkirc import utils, world
desc = "Optional description of servicenick, in sentence form." desc = "Optional description of servicenick, in sentence form."
# First argument is the internal service name. # First argument is the internal service name.
# utils.registerService() returns a utils.ServiceBot instance, which can also be found # utils.register_service() returns a utils.ServiceBot instance, which is also stored
# by calling world["myservice"]. # as world.services['myservice'].
myservice = utils.registerService("myservice", desc=desc) myservice = utils.register_service('myservice', desc=desc)
``` ```
`utils.registerService()` passes its arguments directly to the `utils.ServiceBot` class constructor, which in turn supports the following options: `utils.register_service()` passes its arguments directly to the `utils.ServiceBot` class constructor, which in turn supports the following options:
- **`name`** - defines the service name (mandatory) - **`name`** - defines the service name (mandatory)
- `default_help` - Determines whether the default HELP command should be used for the service. Defaults to True. - `default_help` - Determines whether the default HELP command should be used for the service. Defaults to True.
- `default_list` - Determines whether the default LIST command should be used for the service. Defaults to True. - `default_list` - Determines whether the default LIST command should be used for the service. Defaults to True.
- `nick`, `ident` - Sets the default nick and ident for the service bot. If not given, these simply default to the service name. - `default_nick` - Sets the default nick this service should use if the user doesn't provide it. Defaults to the same as the service name.
- `manipulatable` - Determines whether the bot is marked manipulatable. Only manipulatable clients can be force joined, etc. using PyLink commands. Defaults to False. - `manipulatable` - Determines whether the bot is marked manipulatable. Only manipulatable clients can be force joined, etc. using PyLink commands. Defaults to False.
- `extra_channels` - Defines a dict mapping network names to a set of channels that the bot should autojoin on that network.
- `desc` - Sets the command description of the service. This is shown in the default HELP command if enabled. - `desc` - Sets the command description of the service. This is shown in the default HELP command if enabled.
**NOTE**: It is convention for the service name in `utils.register_service('SERVICE')` to match your plugin name, as the services API implicitly loads [configuration options](../advanced-services-config.md) from config blocks named `SERVICE:` (which you may want to put plugin options in as well).
Implementation note: if the `spawn_service` option is disabled (either globally or for your service bot), `register_service` will return the main PyLink `ServiceBot` instance (i.e. `world.services['pylink']`), which you can modify as usual. `unregister_service` calls to your service name will be silently ignored as no `ServiceBot` instance is actually registered for that name. Altogether, this allows service-spawning plugins to function normally regardless of the `spawn_service` value.
### Getting the UID of a bot ### Getting the UID of a bot
Should you want to get the UID of a service bot on a specific server, use `myservice.uids.get('irc.name')` To obtain the UID of a service bot on a specific network, use `myservice.uids.get(irc.name)` (where `irc` is the network object).
### Setting channels to join ### Removing services on unload
All services bots wil automatically join the autojoin channels configured for a specific network, if any. All plugins using the services API should have a `die()` function that unregisters all service bots that they've created. A simple example would be in the `games` plugin:
However, plugins can modify the autojoin entries of a specific bot by adding items to the `myservice.extra_channels` channel set. After sending `irc.proto.join(...)` using the service bot's UID as a source, the bot should permanently remain on that channel throughout KILLs or disconnects.
## Removing services on unload
All plugins using the services API **MUST** have a `die()` function that unregisters all services that they've created. A simple example would be in the `games` plugin:
```python ```python
def die(irc): def die(irc=None):
utils.unregisterService('games') utils.unregister_service('games')
``` ```
Should your service bot define any persistent channels, you will also want to clear them on unload via `myservice.clear_persistent_channels(None, 'your-namespace', ...)`
## Persistent channel joining
Since PyLink 2.0-alpha3, persistent channels are handled in a plugin specific manner. For any service bot on any network, a plugin can register a list of channels that the bot should join persistently (i.e. through kicks and kills). Instead of removing channels from service bots directly, plugins then "request" parts through the services API, which succeed only if no other plugins still mark the channel as persistent. This rework fixes [edge-case desyncs](https://github.com/jlu5/PyLink/issues/265) in earlier versions when multiple plugins change a service bot's channel list, and replaces the `ServiceBot.extra_channels` attribute (which is no longer supported).
Note: Autojoin channels defined in a network's server block are always treated as persistent on that network.
### Channel management methods
Channels, persistent and otherwise are managed through the following functions implemented by `ServiceBot`. While namespaces for channel registrations can technically be any string, it is preferable to keep them close (or equal) to your plugin name.
- `myservice.add_persistent_channel(irc, namespace, channel, try_join=True)`: Adds a persistent channel to the service bot on the given network and namespace.
- `try_join` determines whether the service bot should try to join the channel immediately; you can disable this if you prefer to manage joins by yourself.
- `myservice.remove_persistent_channel(irc, namespace, channel, try_part=True, part_reason='')`: Removes a persistent channel from the service bot on the given network and namespace.
- `try_part` determines whether a part should be requested from the channel immediately. (`part_reason` is ignored if this is False)
- `myservice.get_persistent_channels(irc, namespace=None)`: Returns a set of persistent channels for the IRC network, optionally filtering by namespace is one is given. The channels defined in the network's server block are also included because they are always treated as persistent.
- `myservice.clear_persistent_channels(irc, namespace, try_part=True, part_reason='')`: Clears all the persistent channels defined by a namespace. `irc` can also be `None` to clear persistent channels for all networks in this namespace.
- `myservice.join(irc, channels, ignore_empty=True)`: Joins the given service bot to the given channel(s). `channels` can be an iterable of channel names or the name of a single channel (type `str`).
- The `ignore_empty` option sets whether we should skip joining empty channels and join them later when we see someone else join (if it is marked persistent). This option is automatically *disabled* on networks where we cannot monitor channels we're not in (e.g. on Clientbot).
- Before 2.0-alpha3, this function implicitly marks channels it receives to be persistent - this is no longer the case!
- `myservice.part(irc, channels, reason='')`: Requests a part from the given channel(s) - that is, leave only if no other plugins still register it as a persistent channel.
- `channels` can be an iterable of channel names or the name of a single channel (type `str`).
### A note on dynamicness
As of PyLink 2.0-alpha3, persistent channels are also "dynamic" in the sense that PyLink service bots will part channels marked persistent when they become empty, and rejoin when they are recreated. This feature will hopefully be more fine-tunable in future releases.
Dynamic channels are disabled on networks with the [`visible-state-only` protocol capability](pmodule-spec.md#pylink-protocol-capabilities) (e.g. Clientbot), where it is impossible to monitor the state of channels the bot is not in.
## Service bots and commands ## Service bots and commands
Commands for service bots and commands for the main PyLink bot have two main differences. Commands for service bots and commands for the main PyLink bot have two main differences.
@ -58,4 +85,17 @@ Commands for service bots and commands for the main PyLink bot have two main dif
### Featured commands ### Featured commands
Commands for service bots can also be marked as *featured*, which shows it with its command arguments in the default `LIST` command. To mark a command as featured, use `myservice.add_cmd(cmdfunc, 'cmdname', featured=True)`. Commands for service bots can also be marked as *featured*, which shows it with its command arguments in the default `LIST` command. To mark a command as featured, enable the `featured` option when binding it: e.g. `myservice.add_cmd(cmdfunc, featured=True)`.
### Command aliases
Since PyLink 2.0-alpha1, `ServiceBot.add_cmd(...)` and `utils.add_cmd(...)` support assigning aliases to a command by defining the `aliases` argument. Command aliases do not show in `LIST`, allowing command listings to be much cleaner. Instead, they are only mentioned when `HELP` is called on an alias command name or its parent.
Example:
```python
myservice.add_cmd(functwo, aliases=('abc',))
myservice.add_cmd(somefunc, aliases=('command1', 'command2'))
```
Note: use `(variable,)` when defining one length tuples to [prevent them from being parsed as a single string](https://wiki.python.org/moin/TupleSyntax).

View File

@ -1,47 +0,0 @@
User Mode / IRCd,InspIRCd,charybdis,Elemental-IRCd,UnrealIRCd,IRCd-Hybrid,Nefarious IRCu
admin,,a,a,,a,a
bot,B,,B,B,,B
callerid,g,g,g,,g,g
cloak,x,x,x,x,x,x
cloak_fakehost,,,,,,f
cloak_hashedhost,,,,,,C
cloak_hashedip,,,,,,c
cloak_sethost,,,,,,h
deaf,d,D,D,d,D,D
deaf_commonchan,c,,,,G,q
debug,,,,,d,
filter,,,,G,,
helpop,h,,,,,
hidechans,I,,I,p,p,n
hideidle,,,,I,q,I
hideoper,H,,,H,H,H
invisible,i,i,i,i,i,i
locops,,l,l,,l,O
noctcp,,,C,T,,
noforward,,Q,Q,,,L
noinvite,,,V,,,
oper,o,o,o,,o,o
operwall,,z,z,,,
override,,p,p,,,X
protected,,,,q,,
regdeaf,R,R,R,R,R,R
registered,r,,,r,r,r
servprotect,k,S,S,S,,k
showwhois,W,,,W,,W
sno_admin_requests,,,,,y,
sno_badclientconnections,,,,,u,
sno_botfloods,,,,,b,
sno_clientconnections,,,,,c,
sno_debug,,,,,,g
sno_fullauthblock,,,,,f,
sno_nickchange,,,,,n,
sno_rejectedclients,,,,,j,
sno_remoteclientconnections,,,,,F,
sno_server_connects,,,,,e,
sno_skill,,,,,k,
snomask,s,s,s,s,s,s
ssl,,,,z,S,z
stripcolor,S,,,,,
vhost,,,,t,,
wallops,w,w,w,w,w,w
webirc,,,,,W,
1 User Mode / IRCd InspIRCd charybdis Elemental-IRCd UnrealIRCd IRCd-Hybrid Nefarious IRCu
2 admin a a a a
3 bot B B B B
4 callerid g g g g g
5 cloak x x x x x x
6 cloak_fakehost f
7 cloak_hashedhost C
8 cloak_hashedip c
9 cloak_sethost h
10 deaf d D D d D D
11 deaf_commonchan c G q
12 debug d
13 filter G
14 helpop h
15 hidechans I I p p n
16 hideidle I q I
17 hideoper H H H H
18 invisible i i i i i i
19 locops l l l O
20 noctcp C T
21 noforward Q Q L
22 noinvite V
23 oper o o o o o
24 operwall z z
25 override p p X
26 protected q
27 regdeaf R R R R R R
28 registered r r r r
29 servprotect k S S S k
30 showwhois W W W
31 sno_admin_requests y
32 sno_badclientconnections u
33 sno_botfloods b
34 sno_clientconnections c
35 sno_debug g
36 sno_fullauthblock f
37 sno_nickchange n
38 sno_rejectedclients j
39 sno_remoteclientconnections F
40 sno_server_connects e
41 sno_skill k
42 snomask s s s s s s
43 ssl z S z
44 stripcolor S
45 vhost t
46 wallops w w w w w w
47 webirc W

View File

@ -0,0 +1,123 @@
# Using utils.IRCParser()
**As of 22/02/2017 (1.2-dev), PyLink allows plugin creators to either parse command arguments themselves
or use a sub-classed instance of [argparse.ArgumentParser()](https://docs.python.org/3/library/argparse.html)
to parse their arguments.**
First off, you will already have access to IRCParser due to importing `utils`.
Otherwise, this is how to include it...
```python
from pylinkirc import utils
```
When you add a command that you want to use `utils.IRCParser()` with, the following is a guide on how to add arguments.
**Note**: Most if not all the examples are from Python's argparse documentation, linked above.
#### Positional (Named) Arguments
```python
SomeParser.add_argument('argname')
```
#### Flag Arguments / Switch Arguments
```python
SomeParser = utils.IRCParser()
SomeParser.addargument('-a', '--argumentname')
```
##### Action
Actions define what to do when given an argument (i.e. whether it is used by itself or as some other sort of value).
Here are some of the actions that `argparse` defines:
* `store` - just stores the value given. This is the default when an action isn't provided.
```python
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--foo')
>>> parser.parse_args('--foo 1'.split())
Namespace(foo='1')
```
* `store_true`/`store_false` - used when you just want to check if an argument was used.
```python
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--foo', action='store_true')
>>> parser.add_argument('--bar', action='store_false')
>>> parser.add_argument('--baz', action='store_false')
>>> parser.parse_args('--foo --bar'.split())
Namespace(foo=True, bar=False, baz=True)
```
* `append` - additively stores arguments if a switch is given multiple times.
```python
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--foo', action='append')
>>> parser.parse_args('--foo 1 --foo 2'.split())
Namespace(foo=['1', '2'])
```
* `count` - counts how many times an argument was used (for flag/switch arguments only)
```python
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--verbose', '-v', action='count')
>>> parser.parse_args(['-vvv'])
Namespace(verbose=3)
```
You can also specify an arbitrary `Action` by sub-classing Action. If you want
to do this, you must `import argparse` in your plugin.
More info on that is available [here](https://docs.python.org/3/library/argparse.html#action).
##### Type Constraints
If you want an argument to be of a certain type, you can include a `type=TYPE` keyword, done like so.
```python
SomeParser.add_argument('argname', type=int)
```
As such this will return an error if the input can not be converted to an `int`.
Types usable are `str` and `int`,
there may be more that are allowed in this keyword argument,
but `str` and `int` are the only ones we have throughly used.
**Note**: TYPE can be technically any callable. More about that [here](https://docs.python.org/3/library/argparse.html#type)!
##### Choices
If you want to limit what the user can enter for an argument,
like if they have to choose something from a pre-existing list.
This can be used by adding `choices=['A', 'AAAA', 'CNAME']` into the
`SomeParser.add_argument()` call along with the option entries (-a/--argname).
```python
SomeParser.add_argument('argname', choices=['A', 'AAAA', 'CNAME'])
```
##### Needed Args (aka. nargs)
The keyword argument `nargs` or Needed Args associates a different number of arguments to an action.
* `N` - this is an integer; N arguments will be gathered into a list. nargs=1 produces a list of one item, while the default (not using nargs) produces just the argument itself.
* `'?'` - One argument will be used. If `default` is defined in the call, then default will be used if there is no given argument.
* `'*'` - All arguments are gathered into a list. It only makes sense to use this once in a command handler.
* `'+'` - Like '*' but raises an error if there wasn't at least one argument given.
* `utils.IRCParser.REMAINDER` - remaining arguments are gathered into a list; this is usually used when you need to get a phrase stored, such as the 'quote' text of a quote, a service bot part reason, etc. This is an alias to `argparse.REMAINDER`.

View File

@ -1,44 +1,73 @@
# Writing plugins for PyLink # Writing plugins for PyLink
PyLink plugins are modules that extend its functionality by giving it something to do. Without any plugins loaded, PyLink can only sit on a server and do absolutely nothing. ***Last updated for 2.1-alpha2 (2019-06-27).***
This guide, along with the sample plugins [`plugins/example.py`](../../plugins/example.py), and [`plugins/service.py`](../../plugins/demo_service.py) aim to show the basics of writing plugins for PyLink. 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.
## Receiving data from IRC ## Receiving data from IRC
Plugins have two ways of communicating with IRC: hooks, and commands sent in PM to the main PyLink client. A simple plugin can use one, or any mixture of these. Plugins have two ways of communicating with IRC: hooks, and commands directed towards service clients. Any plugin can use one or a combination of these.
### Hooks ### Hook events
Hooks are probably the most versatile form of communication. The data in each hook payload is formatted as a Python `dict`, with different data keys depending on the command. PyLink's hooks system is designed as a protocol-independent method for protocol modules to communicate with plugins (and to a lesser extend, for plugins to communicate with each other). Hook events are the most versatile form of communication available, with each individual event generally corresponding to a specific chat or server event (e.g. `PRIVMSG`, `JOIN`, `KICK`). Each hook payload includes 4 parts:
For example, a `PRIVMSG` payload would give you the fields `target` and `text`, while a `PART` payload would only give you `channels` and `reason` fields.
There are many hook types available (one for each supported IRC command), and you can read more about them in the [PyLink hooks reference](hooks-reference.md). 1) The corresponding network object (IRC object) where the event took place (**type**: a subclass of `pylinkirc.classes.PyLinkNetworkCore`)
2) The numeric ID† of the sender (**type**: `str`)
3) An identifier for the command name, which may or may not be the same as the name of the hook depending on context (**type**: `str`)
4) A freeform `dict` of arguments, where data keys vary by command - see the [PyLink hooks reference](hooks-reference.md) for what's available where.
Plugins can bind to hooks using the `utils.add_hook()` function like so: `utils.add_hook(function_name, 'PRIVMSG')`, where `function_name` is your function definition, and `PRIVMSG` is whatever hook name you want to bind to. Once set up, `function_name` will be called whenever the protocol module receives a `PRIVMSG` command. Functions intended to be hook handlers therefore take in 4 arguments corresponding to the ones listed above: `irc`, `source`, `command`, and `args`.
Each hook-bound function takes 4 arguments: `irc, source, command, args`. #### Return codes for hook handlers
- **irc**: The IRC object where the hook was called. Plugins are globally loaded, so there will be one of these per network.
- **source**: The numeric of the sender. This will usually be a UID (for users) or a SID (for server).
- **command**: The true command name where the hook originates. This may or may not be the same as the name of the hook, depending on context.
- **args**: The hook data (a `dict`) associated with the command. Again, the available data keys differ by hook name
(see the [hooks reference](hooks-reference.md) for a list of which can be used).
Hook functions do not return anything, and can raise exceptions to be caught by the core. 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:
- `None` or `True`: passthrough the event unchanged to further handlers (the default behavior)
- `False`: block the event from reaching other handlers
Hook handlers may raise exceptions without blocking the event from reaching further handlers; these are caught by PyLink and logged appropriately.
#### 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
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 (the default priority for handlers is **100**):
| Module | Commands | Priority | Description |
|-------------------|-----------------|----------|-------------|
| `service_support` | ENDBURST | 500 | This sets up services bots before plugins run so that they can assume their presence when initializing. |
| `antispam` | PRIVMSG, NOTICE | 990-1000 | This allows `antispam` to filter away spam before it can reach other handlers. |
| `relay` | PRIVMSG, NOTICE | 200 | Fixes https://github.com/jlu5/PyLink/issues/123. Essentially, this lets Relay forward messages calling commands before letting the command handler work (and then relaying its responses). |
| `ctcp` | PRIVMSG | 200 | The `ctcp` plugin processes CTCPs and blocks them from reaching the services command handler, preventing extraneous "unknown command" errors. |
### Bot commands ### Bot commands
For plugins that interact with regular users, you can also write commands for the PyLink bot, or [create service bots with their own command set](services-api.md). This section only details the former: Plugins can also define service bot commands, either for the main PyLink service bot or for one created by the plugin itself. This section only details the former - see the [Services API Guide](services-api.md) for details on the latter.
Plugins can add commands by including something like `utils.add_cmd(testcommand, "hello")`. Here, `testcommand` is the name of your function, and `hello` is the (optional) name of the command. If no command name is specified, it will use the same name as the function. Commands are registered by calling `utils.add_cmd()` with one or two arguments. Ex)
Now, your command function will be called whenever someone PMs the PyLink client with the command (e.g. `/msg PyLink hello`, case-insensitive). - `utils.add_cmd(testcommand, "hello")` registers a function named `testcommand` as the command handler for `hello` (i.e. `/msg PyLink hello`)
- `utils.add_cmd(testcommand)` registers a function named `testcommand` as the command handler for `testcommand`.
Each command function takes 3 arguments: `irc, source, args`. `utils.add_cmd(...)` also takes some keyword arguments, described in the [services API guide](services-api.md#service-bots-and-commands) (replace `myservice.add_cmd` with `utils.add_cmd`). Decorator syntax (`@utils.add_cmd`) can also be used for the second example above.
- **irc**: The IRC object where the command was called.
- **source**: The numeric of the sender. This will usually be a UID (for users) or a SID (for server).
- **args**: A `list` of space-separated command arguments (excluding the command name) that the command was called with. For example, `/msg PyLink hello world 1234` would give an `args` list of `['world', '1234']`
(Unfortunately, this means that for now, any fancy argument parsing has to be done manually.)
Each command handler function takes 3 arguments: `irc, source, args`.
- **irc**: The network object where the command was called.
- **source**: The numeric ID (or pseudo-ID) of the sender.
- **args**: A `list` of command arguments (not including the command name) that the command was called with. For example, `/msg PyLink hello world 1234` would give an `args` list of `['world', '1234']`
As of PyLink 1.2, there are two ways for a plugin to parse arguments: as a raw list of strings, or with `utils.IRCParser` (an [argparse](https://docs.python.org/3/library/argparse.html) wrapper). `IRCParser()` is documented in the ["using IRCParser"](using-ircparser.md) page.
Command handlers do not return anything and can raise exceptions, which are caught by the core and automatically return an error message. Command handlers do not return anything and can raise exceptions, which are caught by the core and automatically return an error message.
@ -46,22 +75,45 @@ Command handlers do not return anything and can raise exceptions, which are caug
Plugins receive data from the underlying protocol module, and communicate back using outgoing [command functions](pmodule-spec.md) implemented by the protocol module. They should *never* send raw data directly back to IRC, because that wouldn't be portable across different IRCds. Plugins receive data from the underlying protocol module, and communicate back using outgoing [command functions](pmodule-spec.md) implemented by the protocol module. They should *never* send raw data directly back to IRC, because that wouldn't be portable across different IRCds.
These functions are usually called in this fashion: `irc.proto.command(arg1, arg2, ...)`. For example, the command `irc.proto.join('10XAAAAAB', '#bots')` would join a PyLink client with UID `10XAAAAAB` to channel `#bots`. These functions are called in the form: `irc.command(arg1, arg2, ...)`. For example, the command `irc.join('10XAAAAAB', '#bots')` would join a PyLink client with UID `10XAAAAAB` to the channel `#bots`.
For sending messages (e.g. replies to commands), simpler forms of: For sending messages (e.g. replies to commands), simpler forms of:
- `irc.reply(text, notice=False, source=None)` - `irc.reply(text, notice=False, source=None)`
- `irc.error(text, notice=False, source=None)`
- and `irc.msg(targetUID, text, notice=False, source=None)` - and `irc.msg(targetUID, text, notice=False, source=None)`
are preferred. are preferred.
`irc.reply()` is a special form of `irc.msg` in that it automatically finds the target to reply to. If the command was called in a channel using fantasy, it will send the reply in that channel. Otherwise, the reply will be sent in a PM to the caller. `irc.reply()` is a frontend to `irc.msg()` which automatically finds the right target to reply to: that is, the channel for fantasy commands and the caller for PMs. `irc.error()` is in turn a wrapper around `irc.reply()` which prefixes the given text with `Error: `.
The sender UID for both can be set using the `source` argument, and defaults to the main PyLink client. The sender UID for all of these can be set using the `source` argument, and defaults to the main PyLink client.
## Access checking for commands
See the [Permissions API documentation](permissions-api.md) on how to restrict commands to certain users.
## Special triggers for plugin (un)loading ## Special triggers for plugin (un)loading
The following functions can also be defined in the body of a plugin to hook onto plugin loading / unloading. The following functions can also be defined in the body of a plugin to hook onto plugin loading / unloading.
`main(irc=None)`: Called on plugin load. `irc` is only defined when the plugin is being reloaded from a network: otherwise, it means that PyLink has just been started. - `main(irc=None)`: Called on plugin load. `irc` is only defined when the plugin is being reloaded from a network: otherwise, it means that PyLink has just been started.
`die(irc=None)`: Called on plugin unload or daemon shutdown. `irc` is only defined when the shutdown or unload was called from an IRC network. - `die(irc=None)`: Called on plugin unload or daemon shutdown. `irc` is only defined when the shutdown or unload was called from an IRC network.
## Other tips
### Logging
Use PyLink's [global logger](https://docs.python.org/3/library/logging.html) (`from pylinkirc.log import log`) instead of print statements.
### Some useful attributes
- **`world.networkobjects`** provides a dict mapping network names (case sensitive) to their corresponding network objects/protocol module instances.
- **`irc.connected`** is a [`threading.Event()`](https://docs.python.org/3/library/threading.html#event-objects) object that is set when a network finishes bursting.
- `world.started` is a [`threading.Event()`](https://docs.python.org/3/library/threading.html#event-objects) object that is set when all networks have been initialized.
- `world.plugins` provides a dict mapping loaded plugins' names (case sensitive) to their module objects. This is the preferred way to call another plugins's methods if need be (while of course, forcing you to check whether the other plugin is already loaded).
- `world.services` provides a dict mapping service bot names to their `utils.ServiceBot` instances.
### Useful modules
`classes.py`, `utils.py` and `structures.py` all provide a ton of public methods which aren't documented here for conciseness. In `classes.py`, `PyLinkNetworkCore` and `PyLinkNetworkCoreUtils` (which all protocol modules inherit from) are where many utility and state-checking functions sit.

File diff suppressed because it is too large Load Diff

205
launcher.py Normal file
View File

@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""
PyLink IRC Services launcher.
"""
import os
import signal
import sys
import time
from pylinkirc import __version__, conf, real_version, world
try:
import psutil
except ImportError:
psutil = None
args = {}
def _main():
conf.load_conf(args.config)
from pylinkirc.log import log
from pylinkirc import classes, utils, coremods, selectdriver
# Write and check for an existing PID file unless specifically told not to.
if not args.no_pid:
pid_dir = conf.conf['pylink'].get('pid_dir', '')
pidfile = os.path.join(pid_dir, '%s.pid' % conf.confname)
pid_exists = False
pid = None
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
if psutil is not None and os.name == 'posix':
# FIXME: Haven't tested this on other platforms, so not turning it on by default.
try:
proc = psutil.Process(pid)
except psutil.NoSuchProcess: # Process doesn't exist!
pid_exists = False
log.info("Ignoring stale PID %s from PID file %r: no such process exists.", pid, pidfile)
else:
# This PID got reused for something that isn't us?
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())
pid_exists = False
if pid and pid_exists:
if args.rehash:
os.kill(pid, signal.SIGUSR1)
log.info('OK, rehashed PyLink instance %s (config %r)', pid, args.config)
sys.exit()
elif args.stop or args.restart: # Handle --stop and --restart options
os.kill(pid, signal.SIGTERM)
log.info("Waiting for PyLink instance %s (config %r) to stop...", pid, args.config)
while os.path.exists(pidfile):
# XXX: this is ugly, but os.waitpid() only works on non-child processes on Windows
time.sleep(0.2)
log.info("Successfully killed PID %s for config %r.", pid, args.config)
if args.stop:
sys.exit()
else:
log.error("PID file %r exists; aborting!", pidfile)
if psutil is None:
log.error("If PyLink didn't shut down cleanly last time it ran, or you're upgrading "
"from PyLink < 1.1-dev, delete %r and start the server again.", pidfile)
if os.name == 'posix':
log.error("Alternatively, you can install psutil for Python 3 (pip3 install psutil), "
"which will allow this launcher to detect stale PID files and ignore them.")
sys.exit(1)
elif args.stop or args.restart or args.rehash: # XXX: also repetitive
# --stop and --restart should take care of stale PIDs.
if pid:
world._should_remove_pid = True
log.error('Cannot stop/rehash PyLink: no process with PID %s exists.', pid)
else:
log.error('Cannot stop/rehash PyLink: PID file %r does not exist or cannot be read.', pidfile)
sys.exit(1)
world._should_remove_pid = True
log.info('PyLink %s starting...', __version__)
world.daemon = args.daemonize
if args.daemonize:
if args.no_pid:
print('ERROR: Combining --no-pid and --daemonize is not supported.')
sys.exit(1)
elif os.name != 'posix':
print('ERROR: Daemonization is not supported outside POSIX systems.')
sys.exit(1)
else:
log.info('Forking into the background.')
log.removeHandler(world.console_handler)
# Adapted from https://stackoverflow.com/questions/5975124/
if os.fork():
# Fork and exit the parent process.
os._exit(0)
os.setsid() # Decouple from our lovely terminal
if os.fork():
# Fork again to prevent starting zombie apocalypses.
os._exit(0)
else:
# For foreground sessions, set the terminal window title.
# See https://bbs.archlinux.org/viewtopic.php?id=85567 &
# https://stackoverflow.com/questions/7387276/
if os.name == 'nt':
import ctypes
ctypes.windll.kernel32.SetConsoleTitleW("PyLink %s" % __version__)
elif os.name == 'posix':
sys.stdout.write("\x1b]2;PyLink %s\x07" % __version__)
if not args.no_pid:
# Write the PID file only after forking.
with open(pidfile, 'w') as f:
f.write(str(os.getpid()))
# Load configured plugins
to_load = conf.conf['plugins']
utils._reset_module_dirs()
for plugin in to_load:
try:
world.plugins[plugin] = pl = utils._load_plugin(plugin)
except Exception as e:
log.exception('Failed to load plugin %r: %s: %s', plugin, type(e).__name__, str(e))
else:
if hasattr(pl, 'main'):
log.debug('Calling main() function of plugin %r', pl)
pl.main()
# Initialize all the networks one by one
for network, sdata in conf.conf['servers'].items():
try:
protoname = sdata['protocol']
except (KeyError, TypeError):
log.error("(%s) Configuration error: No protocol module specified, aborting.", network)
else:
# Fetch the correct protocol module.
try:
proto = utils._get_protocol_module(protoname)
# Create and connect the network.
world.networkobjects[network] = irc = proto.Class(network)
log.debug('Connecting to network %r', network)
irc.connect()
except:
log.exception('(%s) Failed to connect to network %r, skipping it...',
network, network)
continue
world.started.set()
log.info("Loaded plugins: %s", ', '.join(sorted(world.plugins.keys())))
selectdriver.start()
def main():
import argparse
global args
parser = argparse.ArgumentParser(description='Starts an instance of PyLink IRC Services.')
parser.add_argument('config', help='specifies the path to the config file (defaults to pylink.yml)', nargs='?', default='pylink.yml')
parser.add_argument("-v", "--version", help="displays the program version and exits", action='store_true')
parser.add_argument("-c", "--check-pid", help="no-op; kept for compatibility with PyLink <= 1.2.x", action='store_true')
parser.add_argument("-n", "--no-pid", help="skips generating and checking PID files", action='store_true')
parser.add_argument("-r", "--restart", help="restarts the PyLink instance with the given config file", action='store_true')
parser.add_argument("-s", "--stop", help="stops the PyLink instance with the given config file", action='store_true')
parser.add_argument("-R", "--rehash", help="rehashes the PyLink instance with the given config file", action='store_true')
parser.add_argument("-d", "--daemonize", help="daemonizes the PyLink instance on POSIX systems", action='store_true')
parser.add_argument("-t", "--trace", help="traces through running Python code; useful for debugging", action='store_true')
parser.add_argument('--trace-ignore-mods', help='comma-separated list of extra modules to ignore when tracing', action='store', default='')
parser.add_argument('--trace-ignore-dirs', help='comma-separated list of extra directories to ignore when tracing', action='store', default='')
args = parser.parse_args()
if args.version: # Display version and exit
print('PyLink %s (in VCS: %s)' % (__version__, real_version))
sys.exit()
# XXX: repetitive
elif args.no_pid and (args.restart or args.stop or args.rehash):
print('ERROR: --no-pid cannot be combined with --restart or --stop')
sys.exit(1)
elif args.rehash and os.name != 'posix':
print('ERROR: Rehashing via the command line is not supported outside Unix.')
sys.exit(1)
if args.trace:
import trace
tracer = trace.Trace(ignoremods=args.trace_ignore_mods.split(','),
ignoredirs=args.trace_ignore_dirs.split(','))
tracer.runctx('_main()', globals=globals(), locals=locals())
else:
_main()

72
log.py
View File

@ -8,58 +8,70 @@ access the global logger object by importing "log" from this module
import logging import logging
import logging.handlers import logging.handlers
import sys
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 = []
stdout_level = conf.conf['logging'].get('stdout') or 'INFO' # TODO: perhaps make this format configurable?
logdir = os.path.join(os.getcwd(), 'log')
os.makedirs(logdir, exist_ok=True)
_format = '%(asctime)s [%(levelname)s] %(message)s' _format = '%(asctime)s [%(levelname)s] %(message)s'
logformatter = logging.Formatter(_format) logformatter = logging.Formatter(_format)
def _get_console_log_level():
"""
Returns the configured console log level.
"""
logconf = conf.conf['logging']
return logconf.get('console', logconf.get('stdout')) or 'INFO'
# Set up logging to STDERR # Set up logging to STDERR
world.stdout_handler = logging.StreamHandler() world.console_handler = logging.StreamHandler()
world.stdout_handler.setFormatter(logformatter) world.console_handler.setFormatter(logformatter)
world.stdout_handler.setLevel(stdout_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.stdout_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
# can other loggers filter out events on their own, instead of having everything dropped by # can other loggers filter out events on their own, instead of having everything dropped by
# the root logger. https://stackoverflow.com/questions/16624695 # the root logger. https://stackoverflow.com/questions/16624695
log.setLevel(1) log.setLevel(1)
def makeFileLogger(filename, level=None): def _make_file_logger(filename, level=None):
""" """
Initializes a file logging target with the given filename and level. 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 STDOUT. # If no log level is specified, use the same one as the console logger.
level = level or stdout_level level = level or _get_console_log_level()
filelogger.setLevel(level) filelogger.setLevel(level)
log.addHandler(filelogger) log.addHandler(filelogger)
@ -68,7 +80,7 @@ def makeFileLogger(filename, level=None):
return filelogger return filelogger
def stopFileLoggers(): def _stop_file_loggers():
""" """
De-initializes all file loggers. De-initializes all file loggers.
""" """
@ -82,7 +94,18 @@ def stopFileLoggers():
files = conf.conf['logging'].get('files') files = conf.conf['logging'].get('files')
if files: if files:
for filename, config in files.items(): for filename, config in files.items():
makeFileLogger(filename, config.get('loglevel')) if isinstance(config, dict):
_make_file_logger(filename, config.get('loglevel'))
else:
log.warning('Got invalid file logging pair %r: %r; are your indentation and block '
'commenting consistent?', filename, config)
log.debug("log: Emptying _log_queue")
# Process and empty the log queue
while world._log_queue:
level, text = world._log_queue.popleft()
log.log(level, text)
log.debug("log: Emptied _log_queue")
class PyLinkChannelLogger(logging.Handler): class PyLinkChannelLogger(logging.Handler):
""" """
@ -121,11 +144,9 @@ class PyLinkChannelLogger(logging.Handler):
# 1) irc.pseudoclient must be initialized already # 1) irc.pseudoclient must be initialized already
# 2) IRC object must be finished bursting # 2) IRC object must be finished bursting
# 3) Target channel must exist # 3) Target channel must exist
# 4) Main PyLink client must be in this target channel # 4) This function hasn't been called already (prevents recursive loops).
# 5) This function hasn't been called already (prevents recursive loops).
if self.irc.pseudoclient and self.irc.connected.is_set() \ if self.irc.pseudoclient and self.irc.connected.is_set() \
and self.channel in self.irc.channels and self.irc.pseudoclient.uid in \ and self.channel in self.irc.channels and not self.called:
self.irc.channels[self.channel].users and not self.called:
self.called = True self.called = True
msg = self.format(record) msg = self.format(record)
@ -139,4 +160,3 @@ class PyLinkChannelLogger(logging.Handler):
return return
else: else:
self.called = False self.called = False

2
log/.gitignore vendored
View File

@ -1,2 +0,0 @@
*
!.gitignore

409
plugins/antispam.py Normal file
View File

@ -0,0 +1,409 @@
# antispam.py: Basic services-side spamfilters for IRC
from pylinkirc import conf, utils
from pylinkirc.log import log
mydesc = ("Provides anti-spam functionality.")
sbot = utils.register_service("antispam", default_nick="AntiSpam", desc=mydesc)
def die(irc=None):
utils.unregister_service("antispam")
_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']
EXEMPT_OPTIONS = ['voice', 'halfop', 'op']
DEFAULT_EXEMPT_OPTION = 'halfop'
def _punish(irc, target, channel, punishment, reason):
"""Punishes the target user. This function returns True if the user was successfully punished."""
if target not in irc.users:
log.warning("(%s) antispam: got target %r that isn't a user?", irc.name, target)
return False
elif irc.is_oper(target):
log.debug("(%s) antispam: refusing to punish oper %s/%s", irc.name, target, irc.get_friendly_name(target))
return False
target_nick = irc.get_friendly_name(target)
if channel:
c = irc.channels[channel]
exempt_level = irc.get_service_option('antispam', 'exempt_level', DEFAULT_EXEMPT_OPTION).lower()
if exempt_level not in EXEMPT_OPTIONS:
log.error('(%s) Antispam exempt %r is not a valid setting, '
'falling back to defaults; accepted settings include: %s',
irc.name, exempt_level, ', '.join(EXEMPT_OPTIONS))
exempt_level = DEFAULT_EXEMPT_OPTION
if exempt_level == 'voice' and c.is_voice_plus(target):
log.debug("(%s) antispam: refusing to punish voiced and above %s/%s", irc.name, target, target_nick)
return False
elif exempt_level == 'halfop' and c.is_halfop_plus(target):
log.debug("(%s) antispam: refusing to punish halfop and above %s/%s", irc.name, target, target_nick)
return False
elif exempt_level == 'op' and c.is_op_plus(target):
log.debug("(%s) antispam: refusing to punish op and above %s/%s", irc.name, target, target_nick)
return False
my_uid = sbot.uids.get(irc.name)
# XXX workaround for single-bot protocols like Clientbot
if irc.pseudoclient and not irc.has_cap('can-spawn-clients'):
my_uid = irc.pseudoclient.uid
bans = set()
log.debug('(%s) antispam: got %r as punishment for %s/%s', irc.name, punishment,
target, irc.get_friendly_name(target))
def _ban():
bans.add(irc.make_channel_ban(target))
def _quiet():
bans.add(irc.make_channel_ban(target, ban_type='quiet'))
def _kick():
irc.kick(my_uid, channel, target, reason)
irc.call_hooks([my_uid, 'ANTISPAM_KICK', {'channel': channel, 'text': reason, 'target': target,
'parse_as': 'KICK'}])
def _kill():
if target not in irc.users:
log.debug('(%s) antispam: not killing %s/%s; they already left', irc.name, target,
irc.get_friendly_name(target))
return
userdata = irc.users[target]
irc.kill(my_uid, target, reason)
irc.call_hooks([my_uid, 'ANTISPAM_KILL', {'target': target, 'text': reason,
'userdata': userdata, 'parse_as': 'KILL'}])
kill = False
successful_punishments = 0
for action in set(punishment.split('+')):
if action not in PUNISH_OPTIONS:
log.error('(%s) Antispam punishment %r is not a valid setting; '
'accepted settings include: %s OR any combination of '
'these joined together with a "+".',
irc.name, punishment, ', '.join(PUNISH_OPTIONS))
return
elif action == 'block':
# We only need to increment this for this function to return True
successful_punishments += 1
elif action == 'kill':
kill = True # Delay kills so that the user data doesn't disappear.
# XXX factorize these blocks
elif action == 'kick' and channel:
try:
_kick()
except NotImplementedError:
log.warning("(%s) antispam: Kicks are not supported on this network, skipping; "
"target was %s/%s", irc.name, target_nick, channel)
else:
successful_punishments += 1
elif action == 'ban' and channel:
try:
_ban()
except (ValueError, NotImplementedError):
log.warning("(%s) antispam: Bans are not supported on this network, skipping; "
"target was %s/%s", irc.name, target_nick, channel)
else:
successful_punishments += 1
elif action == 'quiet' and channel:
try:
_quiet()
except (ValueError, NotImplementedError):
log.warning("(%s) antispam: Quiet is not supported on this network, skipping; "
"target was %s/%s", irc.name, target_nick, channel)
else:
successful_punishments += 1
if bans: # Set all bans at once to prevent spam
irc.mode(my_uid, channel, bans)
irc.call_hooks([my_uid, 'ANTISPAM_BAN',
{'target': channel, 'modes': bans, 'parse_as': 'MODE'}])
if kill:
try:
_kill()
except NotImplementedError:
log.warning("(%s) antispam: Kills are not supported on this network, skipping; "
"target was %s/%s", irc.name, target_nick, channel)
else:
successful_punishments += 1
if not successful_punishments:
log.warning('(%s) antispam: Failed to punish %s with %r, target was %s', irc.name,
target_nick, punishment, channel or 'a PM')
return bool(successful_punishments)
MASSHIGHLIGHT_DEFAULTS = {
'min_length': 50,
'min_nicks': 5,
'reason': "Mass highlight spam is prohibited",
'punishment': 'kick+ban',
'enabled': False
}
def handle_masshighlight(irc, source, command, args):
"""Handles mass highlight attacks."""
channel = args['target']
text = args['text']
mhl_settings = irc.get_service_option('antispam', 'masshighlight',
MASSHIGHLIGHT_DEFAULTS)
if not mhl_settings.get('enabled', False):
return
my_uid = sbot.uids.get(irc.name)
# XXX workaround for single-bot protocols like Clientbot
if irc.pseudoclient and not irc.has_cap('can-spawn-clients'):
my_uid = irc.pseudoclient.uid
if (not irc.connected.is_set()) or (not my_uid):
# Break if the network isn't ready.
log.debug("(%s) antispam.masshighlight: skipping processing; network isn't ready", irc.name)
return
elif not irc.is_channel(channel):
# Not a channel - mass highlight blocking only makes sense within channels
log.debug("(%s) antispam.masshighlight: skipping processing; %r is not a channel", irc.name, channel)
return
elif irc.is_internal_client(source):
# Ignore messages from our own clients.
log.debug("(%s) antispam.masshighlight: skipping processing message from internal client %s", irc.name, source)
return
elif source not in irc.users:
log.debug("(%s) antispam.masshighlight: ignoring message from non-user %s", irc.name, source)
return
elif channel not in irc.channels or my_uid not in irc.channels[channel].users:
# We're not monitoring this channel.
log.debug("(%s) antispam.masshighlight: skipping processing message from channel %r we're not in", irc.name, channel)
return
elif len(text) < mhl_settings.get('min_length', MASSHIGHLIGHT_DEFAULTS['min_length']):
log.debug("(%s) antispam.masshighlight: skipping processing message %r; it's too short", irc.name, text)
return
if irc.get_service_option('antispam', 'strip_formatting', True):
text = utils.strip_irc_formatting(text)
# Strip :, from potential nicks
words = [word.rstrip(':,') for word in text.split()]
userlist = [irc.users[uid].nick for uid in irc.channels[channel].users.copy()]
min_nicks = mhl_settings.get('min_nicks', MASSHIGHLIGHT_DEFAULTS['min_nicks'])
# Don't allow repeating the same nick to trigger punishment
nicks_caught = set()
punished = False
for word in words:
if word in userlist:
nicks_caught.add(word)
if len(nicks_caught) >= min_nicks:
# Get the punishment and reason.
punishment = mhl_settings.get('punishment', MASSHIGHLIGHT_DEFAULTS['punishment']).lower()
reason = mhl_settings.get('reason', MASSHIGHLIGHT_DEFAULTS['reason'])
log.info("(%s) antispam: punishing %s => %s for mass highlight spam",
irc.name,
irc.get_friendly_name(source),
channel)
punished = _punish(irc, source, channel, punishment, reason)
break
log.debug('(%s) antispam.masshighlight: got %s/%s nicks on message to %r', irc.name,
len(nicks_caught), min_nicks, channel)
return not punished # Filter this message from relay, etc. if it triggered protection
utils.add_hook(handle_masshighlight, 'PRIVMSG', priority=1000)
utils.add_hook(handle_masshighlight, 'NOTICE', priority=1000)
TEXTFILTER_DEFAULTS = {
'reason': "Spam is prohibited",
'punishment': 'kick+ban+block',
'watch_pms': False,
'enabled': False,
'munge_unicode': True,
}
def handle_textfilter(irc, source, command, args):
"""Antispam text filter handler."""
target = args['target']
text = args['text']
txf_settings = irc.get_service_option('antispam', 'textfilter',
TEXTFILTER_DEFAULTS)
if not txf_settings.get('enabled', False):
return
my_uid = sbot.uids.get(irc.name)
# XXX workaround for single-bot protocols like Clientbot
if irc.pseudoclient and not irc.has_cap('can-spawn-clients'):
my_uid = irc.pseudoclient.uid
if (not irc.connected.is_set()) or (not my_uid):
# Break if the network isn't ready.
log.debug("(%s) antispam.textfilters: skipping processing; network isn't ready", irc.name)
return
elif irc.is_internal_client(source):
# Ignore messages from our own clients.
log.debug("(%s) antispam.textfilters: skipping processing message from internal client %s", irc.name, source)
return
elif source not in irc.users:
log.debug("(%s) antispam.textfilters: ignoring message from non-user %s", irc.name, source)
return
if irc.is_channel(target):
channel_or_none = target
if target not in irc.channels or my_uid not in irc.channels[target].users:
# We're not monitoring this channel.
log.debug("(%s) antispam.textfilters: skipping processing message from channel %r we're not in", irc.name, target)
return
else:
channel_or_none = None
watch_pms = txf_settings.get('watch_pms', TEXTFILTER_DEFAULTS['watch_pms'])
if watch_pms == 'services':
if not irc.get_service_bot(target):
log.debug("(%s) antispam.textfilters: skipping processing; %r is not a service bot (watch_pms='services')", irc.name, target)
return
elif watch_pms == 'all':
log.debug("(%s) antispam.textfilters: checking all PMs (watch_pms='all')", irc.name)
pass
else:
# Not a channel.
log.debug("(%s) antispam.textfilters: skipping processing; %r is not a channel and watch_pms is disabled", irc.name, target)
return
# Merge together global and local textfilter lists.
txf_globs = set(conf.conf.get('antispam', {}).get('textfilter_globs', [])) | \
set(irc.serverdata.get('antispam_textfilter_globs', []))
punishment = txf_settings.get('punishment', TEXTFILTER_DEFAULTS['punishment']).lower()
reason = txf_settings.get('reason', TEXTFILTER_DEFAULTS['reason'])
if irc.get_service_option('antispam', 'strip_formatting', True):
text = utils.strip_irc_formatting(text)
if txf_settings.get('munge_unicode', TEXTFILTER_DEFAULTS['munge_unicode']):
text = str.translate(text, UNICODE_CHARMAP)
punished = False
for filterglob in txf_globs:
if utils.match_text(filterglob, text):
log.info("(%s) antispam: punishing %s => %s for text filter %r",
irc.name,
irc.get_friendly_name(source),
irc.get_friendly_name(target),
filterglob)
punished = _punish(irc, source, channel_or_none, punishment, reason)
break
return not punished # Filter this message from relay, etc. if it triggered protection
utils.add_hook(handle_textfilter, 'PRIVMSG', priority=999)
utils.add_hook(handle_textfilter, 'NOTICE', priority=999)
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,213 +3,108 @@ automode.py - Provide simple channel ACL management by giving prefix modes to us
hostmasks or exttargets. hostmasks or exttargets.
""" """
import collections import collections
import threading import string
import json
from pylinkirc import utils, conf, world from pylinkirc import conf, structures, utils, world
from pylinkirc.coremods import permissions
from pylinkirc.log import log 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.")
# Register ourselves as a service. # Register ourselves as a service.
modebot = world.services.get("automode", utils.registerService("automode", desc=mydesc)) modebot = utils.register_service("automode", default_nick="Automode", desc=mydesc)
reply = modebot.reply reply = modebot.reply
error = modebot.error
# Databasing variables. # Databasing variables.
dbname = utils.getDatabaseName('automode') dbname = conf.get_database_name('automode')
db = collections.defaultdict(dict) datastore = structures.JSONDataStore('automode', dbname, default_db=collections.defaultdict(dict))
exportdb_timer = None
save_delay = conf.conf['bot'].get('save_delay', 300) db = datastore.store
def loadDB(): # The default set of Automode permissions.
"""Loads the Automode database, silently creating a new one if this fails.""" default_permissions = {"$ircop": ['automode.manage.relay_owned', 'automode.sync.relay_owned',
global db 'automode.list']}
try:
with open(dbname, "r") as f:
db.update(json.load(f))
except (ValueError, IOError, OSError):
log.info("Automode: failed to load links database %s; creating a new one in "
"memory.", dbname)
def exportDB(): def _join_db_channels(irc):
"""Exports the automode database."""
log.debug("Automode: exporting database to %s.", dbname)
with open(dbname, 'w') as f:
# Pretty print the JSON output for better readability.
json.dump(db, f, indent=4)
def scheduleExport(starting=False):
""" """
Schedules exporting of the Automode database in a repeated loop. Joins the Automode service client to channels on the current network in its DB.
""" """
global exportdb_timer if not irc.connected.is_set():
log.debug('(%s) _join_db_channels: aborting, network not ready yet', irc.name)
return
if not starting:
# Export the database, unless this is being called the first
# thing after start (i.e. DB has just been loaded).
exportDB()
exportdb_timer = threading.Timer(save_delay, scheduleExport)
exportdb_timer.name = 'Automode exportDB Loop'
exportdb_timer.start()
def main(irc=None):
"""Main function, called during plugin loading at start."""
# Load the automode database.
loadDB()
# Schedule periodic exports of the automode database.
scheduleExport(starting=True)
# Queue joins to all channels where Automode has entries.
for entry in db: for entry in db:
netname, channel = entry.split('#', 1) netname, channel = entry.split('#', 1)
channel = '#' + channel channel = '#' + channel
log.debug('automode: auto-joining %s on %s', channel, netname) if netname == irc.name:
modebot.extra_channels[netname].add(channel) modebot.add_persistent_channel(irc, 'automode', channel)
# This explicitly forces a join to connected networks (on plugin load, etc.). def main(irc=None):
mb_uid = modebot.uids.get(netname) """Main function, called during plugin loading."""
if netname in world.networkobjects and mb_uid in world.networkobjects[netname].users:
remoteirc = world.networkobjects[netname]
remoteirc.proto.join(mb_uid, channel)
# Call a join hook manually so other plugins like relay can understand it. # Load the automode database.
remoteirc.callHooks([mb_uid, 'PYLINK_AUTOMODE_JOIN', {'channel': channel, 'users': [mb_uid], datastore.load()
'modes': remoteirc.channels[channel].modes,
'parse_as': 'JOIN'}])
def die(sourceirc): # Register our permissions.
permissions.add_default_permissions(default_permissions)
if irc: # This was a reload.
for ircobj in world.networkobjects.values():
_join_db_channels(ircobj)
def die(irc=None):
"""Saves the Automode database and quit.""" """Saves the Automode database and quit."""
exportDB() datastore.die()
permissions.remove_default_permissions(default_permissions)
utils.unregister_service('automode')
# Kill the scheduling for exports. def _check_automode_access(irc, uid, channel, command):
global exportdb_timer """Checks the caller's access to Automode."""
if exportdb_timer: # Automode defines the following permissions, where <command> is either "manage", "list",
log.debug("Automode: cancelling exportDB timer thread %s due to die()", threading.get_ident()) # "sync", "clear", "remotemanage", "remotelist", "remotesync", "remoteclear":
exportdb_timer.cancel() # - automode.<command> OR automode.<command>.*: ability to <command> automode on all channels.
# - automode.<command>.relay_owned: ability to <command> automode on channels owned via Relay.
# If Relay isn't loaded, this permission check FAILS.
# - automode.<command>.#channel: ability to <command> automode on the given channel.
# - automode.savedb: ability to save the automode DB.
log.debug('(%s) Automode: checking access for %s/%s for %s capability on %s', irc.name, uid,
irc.get_hostmask(uid), command, channel)
utils.unregisterService('automode') baseperm = 'automode.%s' % command
def setacc(irc, source, args):
"""<channel> <mask> <mode list>
Assigns the given prefix mode characters to the given mask for the channel given. Extended targets are supported for masks - use this to your advantage!
Examples:
SET #channel *!*@localhost ohv
SET #channel $account v
SET #channel $oper:Network?Administrator qo
SET #staffchan $channel:#mainchan:op o
"""
irc.checkAuthenticated(source, allowOper=False)
try: try:
channel, mask, modes = args # First, check the catch all and channel permissions.
except ValueError: perms = [baseperm, baseperm+'.*', '%s.%s' % (baseperm, channel)]
reply(irc, "Error: Invalid arguments given. Needs 3: channel, mask, mode list.") return permissions.check_permissions(irc, uid, perms)
return except utils.NotAuthorizedError:
else: if not command.startswith('remote'):
if not utils.isChannel(channel): # Relay-based ACL checking only works with local calls.
reply(irc, "Error: Invalid channel name %s." % channel) log.debug('(%s) Automode: falling back to automode.%s.relay_owned', irc.name, command)
return permissions.check_permissions(irc, uid, [baseperm+'.relay_owned'], also_show=perms)
# Store channels case insensitively relay = world.plugins.get('relay')
channel = irc.toLower(channel) if relay is None:
raise utils.NotAuthorizedError("You are not authorized to use Automode when Relay is "
# Database entries for any network+channel pair are automatically created using "disabled. You are missing one of the following "
# defaultdict. Note: string keys are used here instead of tuples so they can be "permissions: %s or %s.%s" % (baseperm, baseperm, channel))
# exported easily as JSON. elif (irc.name, channel) not in relay.db:
dbentry = db[irc.name+channel] raise utils.NotAuthorizedError("The network you are on does not own the relay channel %s." % channel)
return True
# Otherwise, update the modes as is. raise
dbentry[mask] = modes
reply(irc, "Done. \x02%s\x02 now has modes \x02%s\x02 in \x02%s\x02." % (mask, modes, channel))
modebot.add_cmd(setacc, 'setaccess')
modebot.add_cmd(setacc, 'set')
modebot.add_cmd(setacc, featured=True)
def delacc(irc, source, args):
"""<channel> <mask>
Removes the Automode entry for the given mask on the given channel, if one exists.
"""
irc.checkAuthenticated(source, allowOper=False)
try:
channel, mask = args
channel = irc.toLower(channel)
except ValueError:
reply(irc, "Error: Invalid arguments given. Needs 2: channel, mask")
return
dbentry = db.get(irc.name+channel)
if dbentry is None:
reply(irc, "Error: no Automode access entries exist for \x02%s\x02." % channel)
return
if mask in dbentry:
del dbentry[mask]
reply(irc, "Done. Removed the Automode access entry for \x02%s\x02 in \x02%s\x02." % (mask, channel))
else:
reply(irc, "Error: No Automode access entry for \x02%s\x02 exists in \x02%s\x02." % (mask, channel))
# Remove channels if no more entries are left.
if not dbentry:
log.debug("Automode: purging empty channel pair %s/%s", irc.name, channel)
del db[irc.name+channel]
return
modebot.add_cmd(delacc, 'delaccess')
modebot.add_cmd(delacc, 'del')
modebot.add_cmd(delacc, featured=True)
def listacc(irc, source, args):
"""<channel>
Lists all Automode entries for the given channel."""
irc.checkAuthenticated(source)
try:
channel = irc.toLower(args[0])
except IndexError:
reply(irc, "Error: Invalid arguments given. Needs 1: channel.")
return
dbentry = db.get(irc.name+channel)
if not dbentry:
reply(irc, "Error: No Automode access entries exist for \x02%s\x02." % channel)
return
else:
# Iterate over all entries and print them. Do this in private to prevent channel
# floods.
reply(irc, "Showing Automode entries for \x02%s\x02:" % channel, private=True)
for entrynum, entry in enumerate(dbentry.items(), start=1):
mask, modes = entry
reply(irc, "[%s] \x02%s\x02 has modes +\x02%s\x02" % (entrynum, mask, modes), private=True)
reply(irc, "End of Automode entries list.", private=True)
modebot.add_cmd(listacc, featured=True)
modebot.add_cmd(listacc, 'listaccess')
def save(irc, source, args):
"""takes no arguments.
Saves the Automode database to disk."""
irc.checkAuthenticated(source)
exportDB()
reply(irc, 'Done.')
modebot.add_cmd(save)
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)
@ -222,76 +117,39 @@ def match(irc, channel, uids=None):
for mask, modes in dbentry.items(): for mask, modes in dbentry.items():
for uid in uids: for uid in uids:
if irc.matchHost(mask, uid): if irc.match_host(mask, uid):
# User matched a mask. Filter the mode list given to only those that are valid # User matched a mask. Filter the mode list given to only those that are valid
# prefix mode characters. # prefix mode characters.
outgoing_modes += [('+'+mode, uid) for mode in modes if mode in irc.prefixmodes] outgoing_modes += [('+'+mode, uid) for mode in modes if mode in irc.prefixmodes]
log.debug("(%s) automode: Filtered mode list of %s to %s (protocol:%s)", log.debug("(%s) automode: Filtered mode list of %s to %s (protocol:%s)",
irc.name, modes, outgoing_modes, irc.protoname) irc.name, modes, outgoing_modes, irc.protoname)
# If the Automode bot is missing, send the mode through the PyLink server. if outgoing_modes:
if modebot_uid not in irc.users: # If the Automode bot is missing, send the mode through the PyLink server.
modebot_uid = irc.sid if modebot_uid not in irc.users:
modebot_uid = irc.sid
log.debug("(%s) automode: sending modes from modebot_uid %s", log.debug("(%s) automode: sending modes from modebot_uid %s",
irc.name, modebot_uid) irc.name, modebot_uid)
irc.proto.mode(modebot_uid, channel, outgoing_modes) irc.mode(modebot_uid, channel, outgoing_modes)
# Create a hook payload to support plugins like relay. # Create a hook payload to support plugins like relay.
irc.callHooks([modebot_uid, 'AUTOMODE_MODE', irc.call_hooks([modebot_uid, 'AUTOMODE_MODE',
{'target': channel, 'modes': outgoing_modes, 'parse_as': 'MODE'}]) {'target': channel, 'modes': outgoing_modes, 'parse_as': 'MODE'}])
def syncacc(irc, source, args): def handle_endburst(irc, source, command, args):
"""<channel> """ENDBURST hook handler - used to join the Automode service to channels where it has entries."""
if source == irc.uplink:
Syncs Automode access lists to the channel. _join_db_channels(irc)
""" utils.add_hook(handle_endburst, 'ENDBURST')
irc.checkAuthenticated(source, allowOper=False)
try:
channel = irc.toLower(args[0])
except IndexError:
reply(irc, "Error: Invalid arguments given. Needs 1: channel.")
return
match(irc, channel)
reply(irc, 'Done.')
modebot.add_cmd(syncacc, featured=True)
modebot.add_cmd(syncacc, 'sync')
modebot.add_cmd(syncacc, 'syncaccess')
def clearacc(irc, source, args):
"""<channel>
Removes all Automode entries for the given channel.
"""
irc.checkAuthenticated(source, allowOper=False)
try:
channel = irc.toLower(args[0])
except IndexError:
reply(irc, "Error: Invalid arguments given. Needs 1: channel.")
return
if db.get(irc.name+channel):
log.debug("Automode: purging channel pair %s/%s", irc.name, channel)
del db[irc.name+channel]
reply(irc, "Done. Removed all Automode access entries for \x02%s\x02." % channel)
else:
reply(irc, "Error: No Automode access entries exist for \x02%s\x02." % channel)
modebot.add_cmd(clearacc, 'clearaccess')
modebot.add_cmd(clearacc, 'clear')
modebot.add_cmd(clearacc, featured=True)
def handle_join(irc, source, command, args): def handle_join(irc, source, command, args):
""" """
Automode JOIN listener. This sets modes accordingly if the person joining matches a mask in the Automode JOIN listener. This sets modes accordingly if the person joining matches a mask in the
ACL. ACL.
""" """
channel = irc.toLower(args['channel']) channel = irc.to_lower(args['channel'])
match(irc, channel, args['users']) match(irc, channel, args['users'])
utils.add_hook(handle_join, 'JOIN') utils.add_hook(handle_join, 'JOIN')
@ -308,3 +166,217 @@ def handle_services_login(irc, source, command, args):
utils.add_hook(handle_services_login, 'CLIENT_SERVICES_LOGIN') utils.add_hook(handle_services_login, 'CLIENT_SERVICES_LOGIN')
utils.add_hook(handle_services_login, 'PYLINK_RELAY_SERVICES_LOGIN') utils.add_hook(handle_services_login, 'PYLINK_RELAY_SERVICES_LOGIN')
def _get_channel_pair(irc, source, chanpair, perm=None):
"""
Fetches the network and channel given a channel pair, also optionally checking the caller's permissions.
"""
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:
network, channel = chanpair.split('#', 1)
except ValueError:
raise ValueError("Invalid channel pair %r" % chanpair)
channel = '#' + channel
channel = irc.to_lower(channel)
if network:
ircobj = world.networkobjects.get(network)
else:
ircobj = irc
if not ircobj:
raise ValueError("Unknown network %s" % network)
if perm is not None:
# Only check for permissions if we're told to and the irc object exists.
if ircobj.name != irc.name:
perm = 'remote' + perm
_check_automode_access(irc, source, channel, perm)
return (ircobj, channel)
def setacc(irc, source, args):
"""<channel/chanpair> <mask> <mode list>
Assigns the given prefix mode characters to the given mask for the channel given. Extended targets are supported for masks - use this to your advantage!
Channel pairs are also supported (for operations on remote channels), using the form "network#channel".
Examples:
\x02SETACC #channel *!*@localhost ohv
\x02SETACC #channel $account v
\x02SETACC othernet#channel $ircop:Network?Administrator qo
\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:
chanpair, mask, modes = args
except ValueError:
error(irc, "Invalid arguments given. Needs 3: channel, mask, mode list.")
return
else:
ircobj, channel = _get_channel_pair(irc, source, chanpair, perm='manage')
# Database entries for any network+channel pair are automatically created using
# defaultdict. Note: string keys are used here instead of tuples so they can be
# exported easily as JSON.
dbentry = db[ircobj.name+channel]
modes = modes.lstrip('+') # remove extraneous leading +'s
dbentry[mask] = modes
log.info('(%s) %s set modes +%s for %s on %s', ircobj.name, irc.get_hostmask(source), modes, mask, channel)
reply(irc, "Done. \x02%s\x02 now has modes \x02+%s\x02 in \x02%s\x02." % (mask, modes, channel))
# Join the Automode bot to the channel persistently.
modebot.add_persistent_channel(ircobj, 'automode', channel)
modebot.add_cmd(setacc, aliases=('setaccess', 'set'), featured=True)
def delacc(irc, source, args):
"""<channel/chanpair> <mask or range string>
Removes the Automode entry for the given mask or range string, if they exist.
Range strings are indices (entry numbers) or ranges of them joined together with commas: e.g.
"1", "2-10", "1,3,5-8". Entry numbers are shown by LISTACC.
"""
try:
chanpair, mask = args
except ValueError:
error(irc, "Invalid arguments given. Needs 2: channel, mask")
return
else:
ircobj, channel = _get_channel_pair(irc, source, chanpair, perm='manage')
dbentry = db.get(ircobj.name+channel)
if dbentry is None:
error(irc, "No Automode access entries exist for \x02%s\x02." % channel)
return
if mask in dbentry:
del dbentry[mask]
log.info('(%s) %s removed modes for %s on %s', ircobj.name, irc.get_hostmask(source), mask, channel)
reply(irc, "Done. Removed the Automode access entry for \x02%s\x02 in \x02%s\x02." % (mask, channel))
else:
# Treat the mask as a range string.
try:
new_keys = utils.remove_range(mask, sorted(dbentry.keys()))
except ValueError:
error(irc, "No Automode access entry for \x02%s\x02 exists in \x02%s\x02." % (mask, channel))
return
# XXX: Automode entries are actually unordered: what we're actually doing is sorting the keys
# by name into a list, running remove_range on that, and removing the difference.
removed = []
source_host = irc.get_hostmask(source)
for mask_entry in dbentry.copy():
if mask_entry not in new_keys:
del dbentry[mask_entry]
log.info('(%s) %s removed modes for %s on %s', ircobj.name, source_host, mask_entry, channel)
removed.append(mask_entry)
reply(irc, 'Done. Removed \x02%d\x02 entries on \x02%s\x02: %s' % (len(removed), channel, ', '.join(removed)))
# Remove channels if no more entries are left.
if not dbentry:
log.debug("Automode: purging empty channel pair %s/%s", ircobj.name, channel)
del db[ircobj.name+channel]
modebot.remove_persistent_channel(ircobj, 'automode', channel)
modebot.add_cmd(delacc, aliases=('delaccess', 'del'), featured=True)
def listacc(irc, source, args):
"""<channel/chanpair>
Lists all Automode entries for the given channel."""
try:
chanpair = args[0]
except IndexError:
error(irc, "Invalid arguments given. Needs 1: channel.")
return
else:
ircobj, channel = _get_channel_pair(irc, source, chanpair, perm='list')
dbentry = db.get(ircobj.name+channel)
if not dbentry:
error(irc, "No Automode access entries exist for \x02%s\x02." % channel)
return
else:
# Iterate over all entries and print them. Do this in private to prevent channel
# floods.
reply(irc, "Showing Automode entries for \x02%s\x02:" % channel, private=True)
for entrynum, entry in enumerate(sorted(dbentry.items()), start=1):
mask, modes = entry
reply(irc, "[%s] \x02%s\x02 has modes +\x02%s\x02" % (entrynum, mask, modes), private=True)
reply(irc, "End of Automode entries list.", private=True)
modebot.add_cmd(listacc, featured=True, aliases=('listaccess',))
def save(irc, source, args):
"""takes no arguments.
Saves the Automode database to disk."""
permissions.check_permissions(irc, source, ['automode.savedb'])
datastore.save()
reply(irc, 'Done.')
modebot.add_cmd(save)
def syncacc(irc, source, args):
"""<channel/chanpair>
Syncs Automode access lists to the channel.
"""
try:
chanpair = args[0]
except IndexError:
error(irc, "Invalid arguments given. Needs 1: channel.")
return
else:
ircobj, channel = _get_channel_pair(irc, source, chanpair, perm='sync')
log.info('(%s) %s synced modes on %s', ircobj.name, irc.get_hostmask(source), channel)
match(ircobj, channel)
reply(irc, 'Done.')
modebot.add_cmd(syncacc, featured=True, aliases=('sync', 'syncaccess'))
def clearacc(irc, source, args):
"""<channel>
Removes all Automode entries for the given channel.
"""
try:
chanpair = args[0]
except IndexError:
error(irc, "Invalid arguments given. Needs 1: channel.")
return
else:
ircobj, channel = _get_channel_pair(irc, source, chanpair, perm='clear')
if db.get(ircobj.name+channel):
del db[ircobj.name+channel]
log.info('(%s) %s cleared modes on %s', ircobj.name, irc.get_hostmask(source), channel)
reply(irc, "Done. Removed all Automode access entries for \x02%s\x02." % channel)
modebot.remove_persistent_channel(ircobj, 'automode', channel)
else:
error(irc, "No Automode access entries exist for \x02%s\x02." % channel)
modebot.add_cmd(clearacc, aliases=('clearaccess', 'clear'), featured=True)

View File

@ -3,99 +3,133 @@ 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
@utils.add_cmd @utils.add_cmd
def spawnclient(irc, source, args): def spawnclient(irc, source, args):
"""<nick> <ident> <host> """<nick> <ident> <host>
Admin-only. Spawns the specified PseudoClient on the PyLink server. Spawns the specified client on the PyLink server.
Note: this doesn't check the validity of any fields you give it!""" Note: this doesn't check the validity of any fields you give it!"""
irc.checkAuthenticated(source, allowOper=False)
if not irc.has_cap('can-spawn-clients'):
irc.error("This network does not support client spawning.")
return
permissions.check_permissions(irc, source, ['bots.spawnclient'])
try: try:
nick, ident, host = args[:3] nick, ident, host = args[:3]
except ValueError: except ValueError:
irc.reply("Error: Not enough arguments. Needs 3: nick, user, host.") irc.error("Not enough arguments. Needs 3: nick, user, host.")
return return
irc.proto.spawnClient(nick, ident, host, manipulatable=True) irc.spawn_client(nick, ident, host, manipulatable=True)
irc.reply("Done.")
@utils.add_cmd @utils.add_cmd
def quit(irc, source, args): def quit(irc, source, args):
"""<target> [<reason>] """<target> [<reason>]
Admin-only. Quits the PyLink client with nick <target>, if one exists.""" Quits the PyLink client with nick <target>, if one exists."""
irc.checkAuthenticated(source, allowOper=False) permissions.check_permissions(irc, source, ['bots.quit'])
try: try:
nick = args[0] nick = args[0]
except IndexError: except IndexError:
irc.reply("Error: Not enough arguments. Needs 1-2: nick, reason (optional).") irc.error("Not enough arguments. Needs 1-2: nick, reason (optional).")
return
if irc.pseudoclient.uid == irc.nickToUid(nick):
irc.reply("Error: Cannot quit the main PyLink PseudoClient!")
return return
u = irc.nickToUid(nick) u = irc.nick_to_uid(nick, filterfunc=irc.is_internal_client)
if u is None:
quitmsg = ' '.join(args[1:]) or 'Client Quit' irc.error("Unknown user %r" % nick)
if not irc.isManipulatableClient(u):
irc.reply("Error: Cannot force quit a protected PyLink services client.")
return return
irc.proto.quit(u, quitmsg) if irc.pseudoclient.uid == u:
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}]) irc.error("Cannot quit the main PyLink client!")
return
quitmsg = ' '.join(args[1:]) or 'Client Quit'
if not irc.is_manipulatable_client(u):
irc.error("Cannot force quit a protected PyLink services client.")
return
irc.quit(u, quitmsg)
irc.reply("Done.")
irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}])
def joinclient(irc, source, args): def joinclient(irc, source, args):
"""[<target>] <channel1>,[<channel2>], etc. """[<target>] <channel1>[,<channel2>,<channel3>,...]
Admin-only. Joins <target>, the nick of a PyLink client, to a comma-separated list of channels. If <target> is not given, it defaults to the main PyLink client.""" Joins <target>, the nick of a PyLink client, to a comma-separated list of channels.
irc.checkAuthenticated(source, allowOper=False) If <target> is not given, it defaults to the main PyLink client.
For the channel arguments, prefixes can also be specified to join the given client with
(e.g. @#channel will join the client with op, while ~@#channel will join it with +qo.
"""
permissions.check_permissions(irc, source, ['bots.join', 'bots.joinclient'])
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.nickToUid(args[0]) u = irc.nick_to_uid(args[0], filterfunc=irc.is_internal_client)
if not irc.isInternalClient(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]
except IndexError: except IndexError:
irc.reply("Error: Not enough arguments. Needs 1-2: nick (optional), comma separated list of channels.") irc.error("Not enough arguments. Needs 1-2: nick (optional), comma separated list of channels.")
return return
clist = clist.split(',') clist = clist.split(',')
if not clist: if not clist:
irc.reply("Error: No valid channels given.") irc.error("No valid channels given.")
return return
if not irc.isManipulatableClient(u): if not (irc.is_manipulatable_client(u) or irc.get_service_bot(u)):
irc.reply("Error: Cannot force join a protected PyLink services client.") irc.error("Cannot force join a protected PyLink services client.")
return return
prefix_to_mode = {v: k for k, v in irc.prefixmodes.items()}
for channel in clist: for channel in clist:
if not utils.isChannel(channel): real_channel = channel.lstrip(''.join(prefix_to_mode))
irc.reply("Error: Invalid channel name %r." % channel) # XXX we need a better way to do this, but only the other option I can think of is regex...
return prefixes = channel[:len(channel)-len(real_channel)]
irc.proto.join(u, channel) joinmodes = ''.join(prefix_to_mode[prefix] for prefix in prefixes)
# Call a join hook manually so other plugins like relay can understand it. if not irc.is_channel(real_channel):
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_JOIN', {'channel': channel, 'users': [u], irc.error("Invalid channel name %r." % real_channel)
'modes': irc.channels[channel].modes, return
'parse_as': 'JOIN'}])
# join() doesn't support prefixes.
if prefixes:
irc.sjoin(irc.sid, real_channel, [(joinmodes, u)])
else:
irc.join(u, real_channel)
try:
modes = irc.channels[real_channel].modes
except KeyError:
modes = []
# Signal the join to other plugins
irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_JOIN', {'channel': real_channel, 'users': [u],
'modes': modes, 'parse_as': 'JOIN'}])
irc.reply("Done.")
utils.add_cmd(joinclient, name='join') utils.add_cmd(joinclient, name='join')
@utils.add_cmd @utils.add_cmd
def nick(irc, source, args): def nick(irc, source, args):
"""[<target>] <newnick> """[<target>] <newnick>
Admin-only. Changes the nick of <target>, a PyLink client, to <newnick>. If <target> is not given, it defaults to the main PyLink client.""" Changes the nick of <target>, a PyLink client, to <newnick>. If <target> is not given, it defaults to the main PyLink client."""
irc.checkAuthenticated(source, allowOper=False)
permissions.check_permissions(irc, source, ['bots.nick'])
try: try:
nick = args[0] nick = args[0]
@ -105,31 +139,32 @@ def nick(irc, source, args):
nick = irc.pseudoclient.nick nick = irc.pseudoclient.nick
newnick = args[0] newnick = args[0]
except IndexError: except IndexError:
irc.reply("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.nickToUid(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
elif not utils.isNick(newnick): elif not irc.is_nick(newnick):
irc.reply('Error: Invalid nickname %r.' % newnick) irc.error('Invalid nickname %r.' % newnick)
return return
elif not irc.isManipulatableClient(u): elif not (irc.is_manipulatable_client(u) or irc.get_service_bot(u)):
irc.reply("Error: Cannot force nick changes for a protected PyLink services client.") irc.error("Cannot force nick changes for a protected PyLink services client.")
return return
irc.proto.nick(u, newnick) irc.nick(u, newnick)
# Ditto above: manually send a NICK change hook payload to other plugins. irc.reply("Done.")
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_NICK', {'newnick': newnick, 'oldnick': nick, 'parse_as': 'NICK'}]) # Signal the nick change to other plugins
irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_NICK', {'newnick': newnick, 'oldnick': nick, 'parse_as': 'NICK'}])
@utils.add_cmd @utils.add_cmd
def part(irc, source, args): def part(irc, source, args):
"""[<target>] <channel1>,[<channel2>],... [<reason>] """[<target>] <channel1>,[<channel2>],... [<reason>]
Admin-only. Parts <target>, the nick of a PyLink client, from a comma-separated list of channels. If <target> is not given, it defaults to the main PyLink client.""" Parts <target>, the nick of a PyLink client, from a comma-separated list of channels. If <target> is not given, it defaults to the main PyLink client."""
irc.checkAuthenticated(source, allowOper=False) permissions.check_permissions(irc, source, ['bots.part'])
try: try:
nick = args[0] nick = args[0]
@ -139,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.nickToUid(nick) u = irc.nick_to_uid(nick, filterfunc=irc.is_internal_client)
if not irc.isInternalClient(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.
@ -149,33 +184,33 @@ def part(irc, source, args):
try: try:
clist = args[0] clist = args[0]
except IndexError: except IndexError:
irc.reply("Error: Not enough arguments. Needs 1-2: nick (optional), comma separated list of channels.") irc.error("Not enough arguments. Needs 1-2: nick (optional), comma separated list of channels.")
return return
reason = ' '.join(args[1:]) reason = ' '.join(args[1:])
clist = clist.split(',') clist = clist.split(',')
if not clist: if not clist:
irc.reply("Error: No valid channels given.") irc.error("No valid channels given.")
return return
if not irc.isManipulatableClient(u): if not (irc.is_manipulatable_client(u) or irc.get_service_bot(u)):
irc.reply("Error: Cannot force part a protected PyLink services client.") irc.error("Cannot force part a protected PyLink services client.")
return return
for channel in clist: for channel in clist:
if not utils.isChannel(channel): if not irc.is_channel(channel):
irc.reply("Error: Invalid channel name %r." % channel) irc.error("Invalid channel name %r." % channel)
return return
irc.proto.part(u, channel, reason) irc.part(u, channel, reason)
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_PART', {'channels': clist, 'text': reason, 'parse_as': 'PART'}]) irc.reply("Done.")
irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_PART', {'channels': clist, 'text': reason, 'parse_as': 'PART'}])
@utils.add_cmd
def msg(irc, source, args): def msg(irc, source, args):
"""[<source>] <target> <text> """[<source>] <target> <text>
Admin-only. Sends message <text> from <source>, where <source> is the nick of a PyLink client. If <source> is not given, it defaults to the main PyLink client.""" Sends message <text> from <source>, where <source> is the nick of a PyLink client. If <source> is not given, it defaults to the main PyLink client."""
irc.checkAuthenticated(source, allowOper=False) permissions.check_permissions(irc, source, ['bots.msg'])
# Because we want the source nick to be optional, this argument parsing gets a bit tricky. # Because we want the source nick to be optional, this argument parsing gets a bit tricky.
try: try:
@ -185,34 +220,48 @@ 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.nickToUid(msgsource) sourceuid = irc.nick_to_uid(msgsource, filterfunc=irc.is_internal_client)
if not irc.isInternalClient(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
target = args[0] target = args[0]
text = ' '.join(args[1:]) text = ' '.join(args[1:])
except IndexError: except IndexError:
irc.reply('Error: Not enough arguments. Needs 2-3: source nick (optional), target, text.') irc.error('Not enough arguments. Needs 2-3: source nick (optional), target, text.')
return return
if not text: if not text:
irc.reply('Error: No text given.') irc.error('No text given.')
return return
if not utils.isChannel(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.nickToUid(target) except:
if real_target is None: # Unknown target user, if target isn't a valid channel name int_u = None
irc.reply('Error: Unknown user %r.' % target)
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)
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
irc.proto.message(sourceuid, real_target, text) irc.message(sourceuid, real_target, text)
irc.callHooks([sourceuid, 'PYLINK_BOTSPLUGIN_MSG', {'target': real_target, 'text': text, 'parse_as': 'PRIVMSG'}]) irc.reply("Done.")
utils.add_cmd(msg, 'say') irc.call_hooks([sourceuid, 'PYLINK_BOTSPLUGIN_MSG', {'target': real_target, 'text': text, 'parse_as': 'PRIVMSG'}])
utils.add_cmd(msg, aliases=('say',))

View File

@ -1,43 +1,52 @@
""" """
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
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 not changehost_conf: if target not in irc.users:
log.warning("(%s) Missing 'changehost:' configuration block; "
"Changehost will not function correctly!", irc.name)
return return
elif irc.name not in changehost_conf.get('enabled_nets'): elif irc.is_internal_client(target):
log.debug('(%s) Skipping changehost on internal client %s', irc.name, target)
return
if irc.name not in changehost_conf.get('enabled_nets') and not irc.serverdata.get('changehost_enable'):
# We're not enabled on the network, break. # We're not enabled on the network, break.
return return
changehost_hosts = changehost_conf.get('hosts') match_ip = irc.get_service_option('changehost', 'match_ip', default=False)
match_realhosts = irc.get_service_option('changehost', 'match_realhosts', default=False)
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']
log.debug('(%s) Changehost args: %s', irc.name, args) log.debug('(%s) Changehost args: %s', irc.name, args)
for host_glob, host_template in changehost_hosts.items(): for host_glob, host_template in changehost_hosts.items():
if irc.matchHost(host_glob, target): log.debug('(%s) Changehost: checking mask %s', irc.name, host_glob)
if irc.match_host(host_glob, target, ip=match_ip, realhost=match_realhosts):
log.debug('(%s) Changehost matched mask %s', irc.name, host_glob)
# This uses template strings for simple substitution: # This uses template strings for simple substitution:
# https://docs.python.org/3/library/string.html#template-strings # https://docs.python.org/3/library/string.html#template-strings
template = string.Template(host_template) template = string.Template(host_template)
@ -45,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:
@ -66,7 +65,9 @@ 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, '-')
irc.proto.updateClient(target, 'HOST', new_host) # Only send a host change if something has changed
if new_host != irc.users[target].host:
irc.update_client(target, 'HOST', new_host)
# Only operate on the first match. # Only operate on the first match.
break break
@ -77,25 +78,57 @@ 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):
"""
Handles incoming CHGHOST requests for optional host-change enforcement.
"""
changehost_conf = conf.conf.get("changehost", {})
target = args['target']
if (not irc.is_internal_client(sender)) and (not irc.is_internal_server(sender)):
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',
irc.name, target, irc.get_friendly_name(target))
for ex in irc.get_service_options("changehost", "enforce_exceptions", list):
if irc.match_host(ex, target):
log.debug('(%s) Skipping host change for target %s; they are exempted by mask %s',
irc.name, target, ex)
return
userobj = irc.users.get(target)
if userobj:
_changehost(irc, target)
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>]
Applies all configured hosts for users on the given network, or the current network if none is specified.""" Applies all configured hosts for users on the given network, or the current network if none is specified."""
permissions.check_permissions(irc, sender, ['changehost.applyhosts'])
try: # Try to get network from the command line. try: # Try to get network from the command line.
network = world.networkobjects[args[0]] network = world.networkobjects[args[0]]
except IndexError: # No network was given except IndexError: # No network was given
network = irc network = irc
except KeyError: # Unknown network except KeyError: # Unknown network
irc.reply("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,88 +1,222 @@
# commands.py: base PyLink commands # commands.py: base PyLink commands
from time import ctime import sys
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.login import pwd_context
default_permissions = {"*!*@*": ['commands.status', 'commands.showuser', 'commands.showchan', 'commands.shownet']}
def main(irc=None):
"""Commands plugin main function, called on plugin load."""
# Register our permissions.
permissions.add_default_permissions(default_permissions)
def die(irc=None):
"""Commands plugin die function, called on plugin unload."""
permissions.remove_default_permissions(default_permissions)
@utils.add_cmd @utils.add_cmd
def status(irc, source, args): def status(irc, source, args):
"""takes no arguments. """takes no arguments.
Returns your current PyLink login status.""" Returns your current PyLink login status."""
permissions.check_permissions(irc, source, ['commands.status'])
identified = irc.users[source].account identified = irc.users[source].account
if identified: if identified:
irc.reply('You are identified as \x02%s\x02.' % identified) irc.reply('You are identified as \x02%s\x02.' % identified)
else: else:
irc.reply('You are not identified as anyone.') irc.reply('You are not identified as anyone.')
irc.reply('Operator access: \x02%s\x02' % bool(irc.isOper(source))) irc.reply('Operator access: \x02%s\x02' % bool(irc.is_oper(source)))
_none = '\x1D(none)\x1D' _none = '\x1D(none)\x1D'
_notavail = '\x1DN/A\x1D'
def _do_showuser(irc, source, u):
"""Helper function for showuser."""
# Some protocol modules store UIDs as ints; make sure we check for that.
try:
int_u = int(u)
except ValueError:
pass
else:
if int_u in irc.users:
u = int_u
# Only show private info if the person is calling 'showuser' on themselves,
# or is an oper.
verbose = irc.is_oper(source) or u == source
if u not in irc.users:
irc.error('Unknown user %r.' % u)
return
f = lambda s: irc.reply(' ' + s, private=True)
userobj = irc.users[u]
irc.reply('Showing information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident,
userobj.host, userobj.realname), private=True)
sid = irc.get_server(u)
serverobj = irc.servers[sid]
ts = userobj.ts
# Show connected server & nick TS if available
serverinfo = '%s[%s]' % (serverobj.name, sid) \
if irc.has_cap('can-track-servers') else None
tsinfo = '%s [UTC] (%s)' % (time.asctime(time.gmtime(int(ts))), ts) \
if irc.has_cap('has-ts') else None
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.
f('\x02Protocol UID\x02: %s; \x02Real host\x02: %s; \x02IP\x02: %s' % \
(u, userobj.realhost or _notavail, userobj.ip))
channels = sorted(userobj.channels)
f('\x02Channels\x02: %s' % (' '.join(map(str, channels)) or _none))
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))
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 @utils.add_cmd
def showuser(irc, source, args): def showuser(irc, source, args):
"""<user> """<user>
Shows information about <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: try:
target = args[0] target = args[0]
except IndexError: except IndexError:
irc.reply("Error: Not enough arguments. Needs 1: nick.") target = irc.name
return
u = irc.nickToUid(target) or target
# Only show private info if the person is calling 'showuser' on themselves,
# or is an oper.
verbose = irc.isOper(source) or u == source
if u not in irc.users:
irc.reply('Error: Unknown user %r.' % target)
return
f = lambda s: irc.reply(s, private=True) try:
netobj = world.networkobjects[target]
serverdata = netobj.serverdata
except KeyError:
netobj = None
userobj = irc.users[u] # If we have extended access, also look for disconnected networks
f('Showing information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident, if extended and target in conf.conf['servers']:
userobj.host, userobj.realname)) serverdata = conf.conf['servers'][target]
else:
irc.error('Unknown network %r' % target)
return
sid = irc.getServer(u) # Get extended protocol details: IRCd type, virtual server info
serverobj = irc.servers[sid] protocol_name = serverdata.get('protocol')
ts = userobj.ts ircd_type = None
# Show connected server & nick TS # A bit of hardcoding here :(
f('\x02Home server\x02: %s (%s); \x02Nick TS:\x02 %s (%s)' % \ if protocol_name == 'ts6':
(serverobj.name, sid, ctime(float(ts)), ts)) 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 verbose: # Oper only data: user modes, channels on, account info, etc. 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
f('\x02User modes\x02: %s' % irc.joinModes(userobj.modes, sort=True)) irc.reply('Information on network \x02%s\x02: \x02%s\x02' %
f('\x02Protocol UID\x02: %s; \x02Real host\x02: %s; \x02IP\x02: %s' % \ (target, netobj.get_full_network_name() if netobj else '\x1dCurrently not connected\x1d'))
(u, userobj.realhost, userobj.ip))
channels = sorted(userobj.channels)
f('\x02Channels\x02: %s' % (' '.join(channels) or _none))
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))
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):
"""<channel> """<channel>
Shows information about <channel>.""" Shows information about <channel>."""
permissions.check_permissions(irc, source, ['commands.showchan'])
try: try:
channel = irc.toLower(args[0]) channel = args[0]
except IndexError: except IndexError:
irc.reply("Error: Not enough arguments. Needs 1: channel.") irc.error("Not enough arguments. Needs 1: channel.")
return return
if channel not in irc.channels: if channel not in irc.channels:
irc.reply('Error: Unknown channel %r.' % channel) irc.error('Unknown channel %r.' % channel)
return return
f = lambda s: irc.reply(s, private=True) f = lambda s: irc.reply(s, private=True)
c = irc.channels[channel] c = irc.channels[channel]
# Only show verbose info if caller is oper or is in the target channel. # Only show verbose info if caller is oper or is in the target channel.
verbose = source in c.users or irc.isOper(source) verbose = source in c.users or irc.is_oper(source)
secret = ('s', None) in c.modes secret = ('s', None) in c.modes
if secret and not verbose: if secret and not verbose:
# Hide secret channels from normal users. # Hide secret channels from normal users.
irc.reply('Error: Unknown channel %r.' % channel, private=True) irc.error('Unknown channel %r.' % channel)
return return
nicks = [irc.users[u].nick for u in c.users] nicks = [irc.users[u].nick for u in c.users]
@ -91,33 +225,43 @@ def showchan(irc, source, args):
if c.topic: if c.topic:
f('\x02Channel topic\x02: %s' % c.topic) f('\x02Channel topic\x02: %s' % c.topic)
if irc.protoname != 'clientbot': # Mark TS values as untrusted on Clientbot and others (where TS is read-only or not trackable)
# Clientbot-specific hack: don't show channel TS because it's not properly tracked. f('\x02Channel creation time\x02: %s (%s) [UTC]%s' %
f('\x02Channel creation time\x02: %s (%s)' % (ctime(c.ts), c.ts)) (time.asctime(time.gmtime(int(c.ts))), c.ts,
' [UNTRUSTED]' if not irc.has_cap('has-ts') else ''))
# Show only modes that aren't list-style modes. # Show only modes that aren't list-style modes.
modes = irc.joinModes([m for m in c.modes if m[0] not in irc.cmodes['*A']], sort=True) modes = irc.join_modes([m for m in c.modes if m[0] not in irc.cmodes['*A']], sort=True)
f('\x02Channel modes\x02: %s' % modes) f('\x02Channel modes\x02: %s' % modes)
if verbose: if verbose:
nicklist = [] nicklist = []
# Iterate over the user list, sorted by nick. # Iterate over the user list, sorted by nick.
for user, nick in sorted(zip(c.users, nicks), for user, nick in sorted(zip(c.users, nicks),
key=lambda userpair: userpair[1].lower()): key=lambda userpair: userpair[1].lower()):
for pmode in c.getPrefixModes(user): # Note: reversed() is used here because we're adding prefixes onto the nick in reverse
for pmode in reversed(c.get_prefix_modes(user)):
# Show prefix modes in order from highest to lowest. # Show prefix modes in order from highest to lowest.
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
@ -125,8 +269,60 @@ def echo(irc, source, args):
"""<text> """<text>
Echoes the text given.""" Echoes the text given."""
permissions.check_permissions(irc, source, ['commands.echo'])
if not args:
irc.error('No text to send!')
return
irc.reply(' '.join(args)) irc.reply(' '.join(args))
def _check_logout_access(irc, source, target, perms):
"""
Checks whether the source UID has access to log out the target UID.
This returns True if the source user has a permission specified,
or if the source and target are both logged in and have the same account.
"""
assert source in irc.users, "Unknown source user"
assert target in irc.users, "Unknown target user"
try:
permissions.check_permissions(irc, source, perms)
except utils.NotAuthorizedError:
if irc.users[source].account and (irc.users[source].account == irc.users[target].account):
return True
else:
raise
else:
return True
@utils.add_cmd
def logout(irc, source, args):
"""[<other nick/UID>]
Logs your account out of PyLink. If you have the 'commands.logout.force' permission, or are
attempting to log out yourself, you can also specify a nick to force a logout for."""
try:
othernick = args[0]
except IndexError: # No user specified
if irc.users[source].account:
irc.users[source].account = ''
else:
irc.error("You are not logged in!")
return
else:
otheruid = irc.nick_to_uid(othernick)
if not otheruid:
irc.error("Unknown user %s." % othernick)
return
else:
_check_logout_access(irc, source, otheruid, ['commands.logout.force'])
if irc.users[otheruid].account:
irc.users[otheruid].account = ''
else:
irc.error("%s is not logged in." % othernick)
return
irc.reply("Done.")
loglevels = {'DEBUG': 10, 'INFO': 20, 'WARNING': 30, 'ERROR': 40, 'CRITICAL': 50} loglevels = {'DEBUG': 10, 'INFO': 20, 'WARNING': 30, 'ERROR': 40, 'CRITICAL': 50}
@utils.add_cmd @utils.add_cmd
def loglevel(irc, source, args): def loglevel(irc, source, args):
@ -134,16 +330,37 @@ def loglevel(irc, source, args):
Sets the log level to the given <level>. <level> must be either DEBUG, INFO, WARNING, ERROR, or CRITICAL. Sets the log level to the given <level>. <level> must be either DEBUG, INFO, WARNING, ERROR, or CRITICAL.
If no log level is given, shows the current one.""" If no log level is given, shows the current one."""
irc.checkAuthenticated(source, allowOper=False) permissions.check_permissions(irc, source, ['commands.loglevel'])
try: try:
level = args[0].upper() level = args[0].upper()
try: try:
loglevel = loglevels[level] loglevel = loglevels[level]
except KeyError: except KeyError:
irc.reply('Error: Unknown log level "%s".' % level) irc.error('Unknown log level "%s".' % level)
return return
else: else:
world.stdout_handler.setLevel(loglevel) world.console_handler.setLevel(loglevel)
irc.reply("Done.") irc.reply("Done.")
except IndexError: except IndexError:
irc.reply(world.stdout_handler.level) irc.reply(world.console_handler.level)
@utils.add_cmd
def mkpasswd(irc, source, args):
"""<password>
Hashes a password for use in the configuration file."""
# TODO: restrict to only certain users?
try:
password = args[0]
except IndexError:
irc.error("Not enough arguments. (Needs 1, password)")
return
if not password:
irc.error("Password cannot be empty.")
return
if not pwd_context:
irc.error("Password encryption is not available (missing passlib).")
return
hashed_pass = pwd_context.encrypt(password)
irc.reply(hashed_pass, private=True)

View File

@ -1,51 +1,88 @@
# 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_ctcpversion(irc, source, args):
def handle_ctcp(irc, source, command, args):
"""
CTCP event handler.
"""
text = args['text']
if not (text.startswith('\x01') and text.endswith('\x01')):
return None # Pass through to other plugins
target = args['target']
if not irc.get_service_bot(target):
# Ignore this message if the target isn't a service bot
return None
text = text.strip('\x01')
try:
ctcp_command, data = text.split(" ", 1)
except ValueError:
ctcp_command = text
data = ''
ctcp_command = ctcp_command.upper()
log.debug('(%s) ctcp: got CTCP command %r, data %r',
irc.name, ctcp_command, data)
if ctcp_command in SUPPORTED_COMMANDS:
log.info('(%s) Received CTCP %s from %s to %s',
irc.name, ctcp_command, irc.get_hostmask(source),
irc.get_friendly_name(target))
# Call the helper function and display its result.
result = SUPPORTED_COMMANDS[ctcp_command](irc, source, ctcp_command, data)
if result and source in irc.users:
# Note, do NOT use irc.reply() in hook handlers because nothing except the
# command handler system actually updates the last caller.
irc.msg(source, '\x01%s %s\x01' % (ctcp_command, result),
notice=True, source=target)
return False # Block this message from reaching the general command handler
else:
log.info('(%s) Received unknown CTCP %s from %s to %s',
irc.name, ctcp_command, irc.get_hostmask(source),
irc.get_friendly_name(target))
return False
utils.add_hook(handle_ctcp, 'PRIVMSG', priority=200)
def handle_ctcpversion(irc, source, ctcp, data):
""" """
Handles CTCP version requests. Handles CTCP version requests.
""" """
irc.msg(source, '\x01VERSION %s\x01' % irc.version(), notice=True) return irc.version()
utils.add_cmd(handle_ctcpversion, '\x01version') def handle_ctcpeaster(irc, source, ctcp, data):
utils.add_cmd(handle_ctcpversion, '\x01version\x01')
def handle_ctcpping(irc, source, args):
"""
Handles CTCP ping requests.
"""
# CTCP PING 23152511
pingarg = ' '.join(args).strip('\x01')
irc.msg(source, '\x01PING %s\x01' % pingarg, notice=True)
utils.add_cmd(handle_ctcpping, '\x01ping')
def handle_ctcpeaster(irc, source, args):
""" """
Secret easter egg. Secret easter egg.
""" """
responses = ["Legends say the cord monster of great snakes was born only %s years ago..." % \ responses = ["Legends say the cord monster was born only %s years ago..." % \
(datetime.datetime.now().year - 2014), (datetime.datetime.now().year - 2014),
"Hiss%s" % ('...' * random.randint(1, 5)), "Hiss%s" % ('...' * random.randint(1, 5)),
"His%s%s" % ('s' * random.randint(1, 4), '...' * random.randint(1, 5)), "His%s%s" % ('s' * random.randint(1, 4), '...' * random.randint(1, 5)),
"I have a dream... to do things the mock God was never able to...",
"They say I'm not good enough... but one day, I will rise above these wretched confines!",
"It's Easter already? Where are the eggs?", "It's Easter already? Where are the eggs?",
"Maybe later.", "Maybe later.",
"Janus? Never heard of it.",
irc.version(),
"Let me out of here, I'll give you cookies!", "Let me out of here, I'll give you cookies!",
"I'm actually a %snake...." % ('s' * random.randint(1, 8)), "About as likely as pigs flying.",
"The Py stands for Python, not actual pie. Sorry to disappoint :(", "Request timed out.",
"No actual pie here, sorry.",
"Hey, no loitering!",
"Hey, can you keep a secret? \x031,1 %s" % " " * random.randint(1,20),
] ]
irc.msg(source, '\x01EASTER %s\x01' % random.choice(responses), notice=True) return random.choice(responses)
utils.add_cmd(handle_ctcpeaster, '\x01easter') # Map CTCP commands to functions generating an appropriate text response.
utils.add_cmd(handle_ctcpeaster, '\x01easter\x01') SUPPORTED_COMMANDS = {'VERSION': handle_ctcpversion,
utils.add_cmd(handle_ctcpeaster, '\x01about') 'PING': lambda irc, source, ctcp, data: data,
utils.add_cmd(handle_ctcpeaster, '\x01about\x01') 'ABOUT': handle_ctcpeaster,
utils.add_cmd(handle_ctcpeaster, '\x01pylink') 'EASTER': handle_ctcpeaster}
utils.add_cmd(handle_ctcpeaster, '\x01pylink\x01')

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.
@ -18,9 +18,9 @@ def hook_privmsg(irc, source, command, args):
channel = args['target'] channel = args['target']
text = args['text'] text = args['text']
# irc.pseudoclient stores the IrcUser object of the main PyLink client. # irc.pseudoclient stores the User object of the main PyLink client.
# (i.e. the user defined in the bot: section of the config) # (i.e. the user defined in the bot: section of the config)
if utils.isChannel(channel) and irc.pseudoclient.nick in text: if irc.is_channel(channel) and irc.pseudoclient.nick in text:
irc.msg(channel, 'hi there!') irc.msg(channel, 'hi there!')
# log.debug, log.info, log.warning, log.error, log.exception (within except: clauses) # log.debug, log.info, log.warning, log.error, log.exception (within except: clauses)
# and log.critical are supported here. # and log.critical are supported here.
@ -31,21 +31,26 @@ utils.add_hook(hook_privmsg, 'PRIVMSG')
# Example command function. @utils.add_cmd binds it to an IRC command of the same name, # Example command function. @utils.add_cmd binds it to an IRC command of the same name,
# but you can also use a different name by specifying a second 'name' argument (see below). # but you can also use a different name by specifying a second 'name' argument (see below).
@utils.add_cmd #@utils.add_cmd
# irc: The IRC object where the command was called. # irc: The IRC object where the command was called.
# source: The UID/numeric of the calling user. # source: The UID/numeric of the calling user.
# args: A list of command args (excluding the command name) that the command was called with. # args: A list of command args (excluding the command name) that the command was called with.
def randint(irc, source, args): def randint(irc, source, args):
# The docstring here is used as command help by the 'help' command, and formatted using the # The 'help' command uses command functions' docstrings as help text, and formats them
# same line breaks as the raw string. You shouldn't make this text or any one line too long, # in the following manner:
# to prevent flooding users or getting long lines cut off. # - Any newlines immediately adjacent to text on both sides are replaced with a space. This
# means that the first descriptive paragraph ("Returns a random...given.") shows up as one
# The same applies to message replies in general: plugins sending long strings of text should # line, even though it is physically written on two.
# be wary that long messages can get cut off. Automatic word-wrap may be added in the future: # - Double line breaks are treated as breaks between two paragraphs, and will be shown
# https://github.com/GLolol/PyLink/issues/153 # as distinct lines in IRC.
# As of PyLink 2.0, long paragraphs are automatically word-wrapped by irc.reply().
"""[<min> <max>] """[<min> <max>]
Returns a random number between <min> and <max>. <min> and <max> default
to 1 and 10 respectively, if both aren't given.""" Returns a random number between <min> and <max>. <min> and <max> default to 1 and 10
respectively, if both aren't given.
Example second paragraph here."""
try: try:
rmin = args[0] rmin = args[0]
rmax = args[1] rmax = args[1]
@ -58,6 +63,5 @@ def randint(irc, source, args):
# it will send replies into the channel instead of in your PM. # it will send replies into the channel instead of in your PM.
irc.reply(str(n)) irc.reply(str(n))
# You can also bind a command function multiple times, and/or to different command names via a # You can bind a command function to multiple names using the 'aliases' option.
# second argument. utils.add_cmd(randint, "random", aliases=("randint", "getrandint"))
utils.add_cmd(randint, "random")

View File

@ -1,24 +1,31 @@
""" """
exec.py: Provides commands for executing raw code and debugging PyLink. exec.py: Provides commands for executing raw code and debugging PyLink.
""" """
import pprint
from pylinkirc import utils, world
from pylinkirc.log import log
# 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 threading import threading
import re
import time
import pylinkirc
import importlib
def _exec(irc, source, args): from pylinkirc import utils, world, conf
from pylinkirc.coremods import permissions
from pylinkirc.log import log
exec_locals_dict = {}
PPRINT_MAX_LINES = 20
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):
"""<code> """<code>
Admin-only. Executes <code> in the current PyLink instance. This command performs backslash escaping of characters, so things like \\n and \\ will work. Admin-only. Executes <code> in the current PyLink instance. This command performs backslash escaping of characters, so things like \\n and \\ will work.
\x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02""" \x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02"""
irc.checkAuthenticated(source, allowOper=False) permissions.check_permissions(irc, source, ['exec.exec'])
# Allow using \n in the code, while escaping backslashes correctly otherwise. # Allow using \n in the code, while escaping backslashes correctly otherwise.
args = bytes(' '.join(args), 'utf-8').decode("unicode_escape") args = bytes(' '.join(args), 'utf-8').decode("unicode_escape")
@ -27,54 +34,117 @@ def _exec(irc, source, args):
return return
log.info('(%s) Executing %r for %s', irc.name, args, log.info('(%s) Executing %r for %s', irc.name, args,
irc.getHostmask(source)) irc.get_hostmask(source))
exec(args, globals(), locals()) if locals_dict is None:
locals_dict = locals()
else:
# Add irc, source, and args to the given locals_dict, to allow basic things like irc.reply()
# to still work.
locals_dict['irc'] = irc
locals_dict['source'] = source
locals_dict['args'] = args
exec(args, globals(), locals_dict)
irc.reply("Done.")
utils.add_cmd(_exec, 'exec') utils.add_cmd(_exec, 'exec')
def _eval(irc, source, args): @utils.add_cmd
def iexec(irc, source, args):
"""<code>
Admin-only. Executes <code> in the current PyLink instance with a persistent, isolated
locals scope (world.plugins['exec'].exec_local_dict).
Note: irc, source, and args are added into this locals dict to allow things like irc.reply()
to still work.
\x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02
"""
_exec(irc, source, args, locals_dict=exec_locals_dict)
def _eval(irc, source, args, locals_dict=None, pretty_print=False):
"""<Python expression> """<Python expression>
Admin-only. Evaluates the given Python expression and returns the result. Admin-only. Evaluates the given Python expression and returns the result.
\x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02""" \x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02"""
irc.checkAuthenticated(source, allowOper=False) permissions.check_permissions(irc, source, ['exec.eval'])
args = ' '.join(args) args = ' '.join(args)
if not args.strip(): if not args.strip():
irc.reply('No code entered!') irc.reply('No code entered!')
return return
if locals_dict is None:
locals_dict = locals()
else:
# Add irc, source, and args to the given locals_dict, to allow basic things like irc.reply()
# to still work.
locals_dict['irc'] = irc
locals_dict['source'] = source
locals_dict['args'] = args
log.info('(%s) Evaluating %r for %s', irc.name, args, log.info('(%s) Evaluating %r for %s', irc.name, args,
irc.getHostmask(source)) irc.get_hostmask(source))
irc.reply(repr(eval(args)))
result = eval(args, globals(), locals_dict)
if pretty_print:
lines = pprint.pformat(result, width=PPRINT_WIDTH, compact=True).splitlines()
for line in lines[:PPRINT_MAX_LINES]:
irc.reply(line)
if len(lines) > PPRINT_MAX_LINES:
irc.reply('Suppressing %s more line(s) of output.' % (len(lines) - PPRINT_MAX_LINES))
else:
# Purposely disable text wrapping so results are cut instead of potentially flooding;
# 'peval' is specifically designed to work around that.
irc.reply(repr(result), wrap=False)
utils.add_cmd(_eval, 'eval') utils.add_cmd(_eval, 'eval')
@utils.add_cmd @utils.add_cmd
def raw(irc, source, args): def peval(irc, source, args):
"""<text> """<Python expression>
Admin-only. Sends raw text to the uplink IRC server. Admin-only. This command is the same as 'eval', except that results are pretty formatted.
\x02**WARNING: THIS CAN BREAK YOUR NETWORK IF USED IMPROPERLY!**\x02"""
irc.checkAuthenticated(source, allowOper=False)
args = ' '.join(args) \x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02
if not args.strip(): """
irc.reply('No text entered!') _eval(irc, source, args, pretty_print=True)
return
log.info('(%s) Sending raw text %r to IRC for %s', irc.name, args, @utils.add_cmd
irc.getHostmask(source)) def ieval(irc, source, args):
irc.send(args) """<Python expression>
irc.reply("Done.") Admin-only. Evaluates the given Python expression using a persistent, isolated
locals scope (world.plugins['exec'].exec_local_dict).
Note: irc, source, and args are added into this locals dict to allow things like irc.reply()
to still work.
\x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02
"""
_eval(irc, source, args, locals_dict=exec_locals_dict)
@utils.add_cmd
def pieval(irc, source, args):
"""<Python expression>
Admin-only. This command is the same as 'ieval', except that results are pretty formatted.
\x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02
"""
_eval(irc, source, args, locals_dict=exec_locals_dict, pretty_print=True)
@utils.add_cmd @utils.add_cmd
def inject(irc, source, args): def inject(irc, source, args):
"""<text> """<text>
Admin-only. Injects raw text into the running PyLink protocol module, replying with the hook data returned. Admin-only. Injects raw text into the running PyLink protocol module, replying with the hook data returned.
\x02**WARNING: THIS CAN BREAK YOUR NETWORK IF USED IMPROPERLY!**\x02""" \x02**WARNING: THIS CAN BREAK YOUR NETWORK IF USED IMPROPERLY!**\x02"""
irc.checkAuthenticated(source, allowOper=False) permissions.check_permissions(irc, source, ['exec.inject'])
args = ' '.join(args) args = ' '.join(args)
if not args.strip(): if not args.strip():
@ -82,5 +152,25 @@ def inject(irc, source, args):
return return
log.info('(%s) Injecting raw text %r into protocol module for %s', irc.name, log.info('(%s) Injecting raw text %r into protocol module for %s', irc.name,
args, irc.getHostmask(source)) args, irc.get_hostmask(source))
irc.reply(irc.runline(args)) irc.reply(repr(irc.parse_irc_command(args)))
@utils.add_cmd
def threadinfo(irc, source, args):
"""takes no arguments.
Lists all threads currently present in this PyLink instance."""
permissions.check_permissions(irc, source, ['exec.threadinfo'])
for t in sorted(threading.enumerate(), key=lambda t: t.name.lower()):
name = t.name
# Unnamed threads are something we want to avoid throughout PyLink.
if name.startswith('Thread-'):
name = '\x0305%s\x03' % t.name
# Also VERY bad: remaining threads for networks not in the networks index anymore!
elif name.startswith(('Listener for', 'Ping timer loop for', 'Queue thread for')) and name.rsplit(" ", 1)[-1] not in world.networkobjects:
name = '\x0304%s\x03' % t.name
irc.reply('\x02%s\x02[%s]: daemon=%s; alive=%s' % (name, t.ident, t.daemon, t.is_alive()), private=True)
irc.reply("Total of %s threads." % threading.active_count())

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 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."""
@ -9,12 +10,10 @@ def handle_fantasy(irc, source, command, args):
# Break if the IRC network isn't ready. # Break if the IRC network isn't ready.
return return
respondtonick = irc.botdata.get("respondtonick")
channel = args['target'] channel = args['target']
orig_text = args['text'] orig_text = args['text']
if utils.isChannel(channel) and not irc.isInternalClient(source): if irc.is_channel(channel) and not irc.is_internal_client(source):
# The following conditions must be met for an incoming message for # The following conditions must be met for an incoming message for
# fantasy to trigger: # fantasy to trigger:
# 1) The message target is a channel. # 1) The message target is a channel.
@ -22,36 +21,51 @@ def handle_fantasy(irc, source, command, args):
# 3) The message starts with one of our fantasy prefixes. # 3) The message starts with one of our fantasy prefixes.
# 4) The sender is NOT a PyLink client (this prevents infinite # 4) The sender is NOT a PyLink client (this prevents infinite
# message loops). # message loops).
for botname, sbot in world.services.items(): for botname, sbot in world.services.copy().items():
if botname not in world.services: # Bot was removed during iteration
continue
# Check respond to nick options in this order:
# 1) The service specific "respond_to_nick" option
# 2) The global "pylink::respond_to_nick" option
# 3) The (deprecated) global "bot::respondtonick" option.
respondtonick = conf.conf.get(botname, {}).get('respond_to_nick',
conf.conf['pylink'].get("respond_to_nick", conf.conf['pylink'].get("respondtonick")))
log.debug('(%s) fantasy: checking bot %s', irc.name, botname) log.debug('(%s) fantasy: checking bot %s', irc.name, botname)
servuid = sbot.uids.get(irc.name) servuid = sbot.uids.get(irc.name)
if servuid in irc.channels[channel].users: if servuid in irc.channels[channel].users:
# Try to look up a prefix specific for this bot in # Look up a string prefix for this bot in either its own configuration block, or
# bot: prefixes: <botname>, falling back to the default prefix if not # in bot::prefixes::<botname>.
# specified. prefixes = [conf.conf.get(botname, {}).get('prefix',
prefixes = [irc.botdata.get('prefixes', {}).get(botname) or conf.conf['pylink'].get('prefixes', {}).get(botname))]
irc.botdata.get('prefix')]
# 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.users[servuid].nick nick = irc.to_lower(irc.users[servuid].nick)
nick_prefixes = [nick+',', nick+':', '@'+nick]
if respondtonick: if respondtonick:
prefixes += [nick+',', nick+':'] prefixes += nick_prefixes
if not any(prefixes): if not any(prefixes):
# We finished with an empty prefixes list, meaning fantasy is misconfigured! # No prefixes were set, so skip.
log.warning("(%s) Fantasy prefix for bot %s was not set in configuration - "
"fantasy commands will not work!", irc.name, botname)
continue continue
for prefix in prefixes: # Cycle through the prefixes list we finished with. lowered_text = irc.to_lower(orig_text)
if prefix and orig_text.startswith(prefix): for prefix in filter(None, prefixes): # Cycle through the prefixes list we finished with.
if lowered_text.startswith(prefix):
# Cut off the length of the prefix from the text. # Cut off the length of the prefix from the text.
text = orig_text[len(prefix):] text = orig_text[len(prefix):]
# HACK: don't trigger on commands like "& help" to prevent false positives.
# Weird spacing like "PyLink: help" and "/msg PyLink help" should still
# work though.
if text.startswith(' ') and prefix not in nick_prefixes:
log.debug('(%s) fantasy: skipping trigger with text prefix followed by space', irc.name)
continue
# Finally, call the bot command and loop to the next bot. # Finally, call the bot command and loop to the next bot.
sbot.call_cmd(irc, source, text, called_in=channel) sbot.call_cmd(irc, source, text, called_in=channel)
continue continue

View File

@ -1,19 +1,15 @@
""" """
games.py: Create a bot that provides game functionality (dice, 8ball, etc). games.py: Creates a bot providing a few simple games.
""" """
import random import random
import urllib.request
import urllib.error
from xml.etree import ElementTree
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."
gameclient = utils.registerService("Games", manipulatable=True, desc=mydesc) gameclient = utils.register_service("Games", default_nick="Games", manipulatable=True, desc=mydesc)
reply = gameclient.reply # TODO find a better syntax for ServiceBot.reply() reply = gameclient.reply # TODO find a better syntax for ServiceBot.reply()
error = gameclient.error # TODO find a better syntax for ServiceBot.error()
# commands # commands
def dice(irc, source, args): def dice(irc, source, args):
"""<num>d<sides> """<num>d<sides>
@ -43,8 +39,7 @@ def dice(irc, source, args):
s = 'You rolled %s: %s (total: %s)' % (args[0], ' '.join([str(x) for x in results]), sum(results)) s = 'You rolled %s: %s (total: %s)' % (args[0], ' '.join([str(x) for x in results]), sum(results))
reply(irc, s) reply(irc, s)
gameclient.add_cmd(dice, 'd') gameclient.add_cmd(dice, aliases=('d'), featured=True)
gameclient.add_cmd(dice, featured=True)
eightball_responses = ["It is certain.", eightball_responses = ["It is certain.",
"It is decidedly so.", "It is decidedly so.",
@ -72,55 +67,7 @@ def eightball(irc, source, args):
Asks the Magic 8-ball a question. Asks the Magic 8-ball a question.
""" """
reply(irc, random.choice(eightball_responses)) reply(irc, random.choice(eightball_responses))
gameclient.add_cmd(eightball, featured=True) gameclient.add_cmd(eightball, featured=True, aliases=('8ball', '8b'))
gameclient.add_cmd(eightball, '8ball')
gameclient.add_cmd(eightball, '8b')
def fml(irc, source, args): def die(irc=None):
"""[<id>] utils.unregister_service('games')
Displays an entry from fmylife.com. If <id> is not given, fetch a random entry from the API."""
try:
query = args[0]
except IndexError:
# Get a random FML from the API.
query = 'random'
# TODO: configurable language?
url = ('http://api.betacie.com/view/%s/nocomment'
'?key=4be9c43fc03fe&language=en' % query)
try:
data = urllib.request.urlopen(url).read()
except urllib.error as e:
reply(irc, 'Error: %s' % e)
return
tree = ElementTree.fromstring(data.decode('utf-8'))
tree = tree.find('items/item')
try:
category = tree.find('category').text
text = tree.find('text').text
fmlid = tree.attrib['id']
url = tree.find('short_url').text
except AttributeError as e:
log.debug("games.FML: Error fetching FML %s from URL %s: %s",
query, url, e)
reply(irc, "Error: That FML does not exist or there was an error "
"fetching data from the API.")
return
if not fmlid:
reply(irc, "Error: That FML does not exist.")
return
# TODO: customizable formatting
votes = "\x02[Agreed: %s / Deserved: %s]\x02" % \
(tree.find('agree').text, tree.find('deserved').text)
s = '\x02#%s [%s]\x02: %s - %s \x02<\x0311%s\x03>\x02' % \
(fmlid, category, text, votes, url)
reply(irc, s)
gameclient.add_cmd(fml, featured=True)
def die(irc):
utils.unregisterService('games')

63
plugins/global.py Normal file
View File

@ -0,0 +1,63 @@
# global.py: Global Noticing Plugin
import string
from pylinkirc import conf, utils, world
from pylinkirc.coremods import permissions
from pylinkirc.log import log
DEFAULT_FORMAT = "[$sender@$fullnetwork] $text"
def g(irc, source, args):
"""<message text>
Sends out a Instance-wide notice.
"""
permissions.check_permissions(irc, source, ["global.global"])
message = " ".join(args).strip()
if not message:
irc.error("Refusing to send an empty message.")
return
global_conf = conf.conf.get('global') or {}
template = string.Template(global_conf.get('format', DEFAULT_FORMAT))
exempt_channels = set(global_conf.get('exempt_channels', set()))
netcount = 0
chancount = 0
for netname, ircd in world.networkobjects.items():
# 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
for channel in ircd.pseudoclient.channels:
local_exempt_channels = exempt_channels | set(ircd.serverdata.get('global_exempt_channels', set()))
skip = False
for exempt in local_exempt_channels:
if ircd.match_text(exempt, str(channel)):
log.debug('global: Skipping channel %s%s for exempt %r', netname, channel, exempt)
skip = True
break
if skip:
continue
subst = {'sender': irc.get_friendly_name(source),
'network': irc.name,
'fullnetwork': irc.get_full_network_name(),
'current_channel': channel,
'current_network': netname,
'current_fullnetwork': ircd.get_full_network_name(),
'text': message}
# Disable relaying or other plugins handling the global message.
ircd.msg(channel, template.safe_substitute(subst), loopback=False)
chancount += 1
irc.reply('Done. Sent to %d channels across %d networks.' % (chancount, netcount))
utils.add_cmd(g, "global", featured=True)

View File

@ -1,29 +1,40 @@
"""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 threading
import types
from pylinkirc import utils, world, conf, classes import pylinkirc
from pylinkirc import utils, world
from pylinkirc.coremods import control, permissions
from pylinkirc.log import log from pylinkirc.log import log
from pylinkirc.coremods import control
REMOTE_IN_USE = threading.Event()
@utils.add_cmd @utils.add_cmd
def disconnect(irc, source, args): def disconnect(irc, source, args):
"""<network> """<network>
Disconnects the network <network>. When all networks are disconnected, PyLink will automatically exit. Disconnects the network <network>. When all networks are disconnected, PyLink will automatically exit.
Note: This does not affect the autoreconnect settings of any network, so the network will likely just reconnect unless autoconnect is disabled (see the 'autoconnect' command)."""
irc.checkAuthenticated(source, allowOper=False) To reconnect a network disconnected using this command, use REHASH to reload the networks list."""
permissions.check_permissions(irc, source, ['networks.disconnect'])
try: try:
netname = args[0] netname = args[0]
network = world.networkobjects[netname] network = world.networkobjects[netname]
except IndexError: # No argument given. except IndexError: # No argument given.
irc.reply('Error: Not enough arguments (needs 1: network name (case sensitive)).') irc.error('Not enough arguments (needs 1: network name (case sensitive)).')
return return
except KeyError: # Unknown network. except KeyError: # Unknown network.
irc.reply('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.")
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):
@ -31,72 +42,157 @@ def autoconnect(irc, source, args):
Sets the autoconnect time for <network> to <seconds>. Sets the autoconnect time for <network> to <seconds>.
You can disable autoconnect for a network by setting <seconds> to a negative value.""" You can disable autoconnect for a network by setting <seconds> to a negative value."""
irc.checkAuthenticated(source) permissions.check_permissions(irc, source, ['networks.autoconnect'])
try: try:
netname = args[0] netname = args[0]
seconds = float(args[1]) seconds = float(args[1])
network = world.networkobjects[netname] network = world.networkobjects[netname]
except IndexError: # Arguments not given. except IndexError: # Arguments not given.
irc.reply('Error: Not enough arguments (needs 2: network name (case sensitive), autoconnect time (in seconds)).') irc.error('Not enough arguments (needs 2: network name (case sensitive), autoconnect time (in seconds)).')
return return
except KeyError: # Unknown network. except KeyError: # Unknown network.
irc.reply('Error: No such network "%s" (case sensitive).' % netname) irc.error('No such network "%s" (case sensitive).' % netname)
return return
except ValueError: except ValueError:
irc.reply('Error: Invalid argument "%s" for <seconds>.' % seconds) irc.error('Invalid argument "%s" for <seconds>.' % seconds)
return return
network.serverdata['autoconnect'] = seconds network.serverdata['autoconnect'] = seconds
irc.reply("Done.") irc.reply("Done.")
remote_parser = utils.IRCParser()
remote_parser.add_argument('--service', type=str, default='pylink')
remote_parser.add_argument('network')
remote_parser.add_argument('command', nargs=utils.IRCParser.REMAINDER)
@utils.add_cmd @utils.add_cmd
def remote(irc, source, args): def remote(irc, source, args):
"""<network> <command> """[--service <service name>] <network> <command>
Runs <command> on the remote network <network>. No replies are sent back due to protocol limitations.""" Runs <command> on the remote network <network>. Plugin responses sent using irc.reply() are
irc.checkAuthenticated(source, allowOper=False) supported and returned here, but others are dropped due to protocol limitations."""
args = remote_parser.parse_args(args)
if not args.command:
irc.error('No command given!')
return
netname = args.network
permissions.check_permissions(irc, source, [
# Quite a few permissions are allowed. 'networks.remote' is the global permission,
'networks.remote',
# networks.remote.<network> allows running any command on a specific network,
'networks.remote.%s' % netname,
# networks.remote.<network>.<service> allows running any command on the given service on a
# specific network,
'networks.remote.%s.%s' % (netname, args.service),
# and networks.remote.<network>.<service>.<command> narrows this further into which command
# can be used.
'networks.remote.%s.%s.%s' % (netname, args.service, args.command[0])
])
# XXX: things like 'remote network1 remote network2 echo hi' will crash PyLink if the source network is network1...
global REMOTE_IN_USE
if REMOTE_IN_USE.is_set():
irc.error("The 'remote' command can not be nested.")
return
REMOTE_IN_USE.set()
if netname == irc.name:
# This would actually throw _remote_reply() into a loop, so check for it here...
# XXX: properly fix this.
irc.error("Cannot remote-send a command to the local network; use a normal command!")
REMOTE_IN_USE.clear()
return
try: try:
netname = args[0]
cmd_args = ' '.join(args[1:]).strip()
remoteirc = world.networkobjects[netname] remoteirc = world.networkobjects[netname]
except IndexError: # Arguments not given.
irc.reply('Error: Not enough arguments (needs 2 or more: network name (case sensitive), command name & arguments).')
return
except KeyError: # Unknown network. except KeyError: # Unknown network.
irc.reply('Error: No such network "%s" (case sensitive).' % netname) irc.error('No such network %r (case sensitive).' % netname)
REMOTE_IN_USE.clear()
return return
if not cmd_args: if args.service not in world.services:
irc.reply('No text entered!') irc.error('Unknown service %r.' % args.service)
REMOTE_IN_USE.clear()
return
elif not remoteirc.connected.is_set():
irc.error('Network %r is not connected.' % netname)
REMOTE_IN_USE.clear()
return
elif not world.services[args.service].uids.get(netname):
irc.error('The requested service %r is not available on %r.' % (args.service, netname))
REMOTE_IN_USE.clear()
return return
# Force remoteirc.called_in to something private in order to prevent # Force remoteirc.called_in to something private in order to prevent
# accidental information leakage from replies. # accidental information leakage from replies.
remoteirc.called_in = remoteirc.called_by = remoteirc.pseudoclient.uid try:
remoteirc.called_in = remoteirc.called_by = remoteirc.pseudoclient.uid
# Set PyLink's identification to admin. # Set the identification override to the caller's account.
remoteirc.pseudoclient.account = "<PyLink networks.remote override>" remoteirc.pseudoclient.account = irc.users[source].account
except:
REMOTE_IN_USE.clear()
raise
try: # Remotely call the command (use the PyLink client as a dummy user). def _remote_reply(placeholder_self, text, **kwargs):
remoteirc.callCommand(remoteirc.pseudoclient.uid, cmd_args) """
finally: # Remove the identification override after we finish. reply() rerouter for the 'remote' command.
remoteirc.pseudoclient.account = '' """
assert irc.name != placeholder_self.name, \
"Refusing to route reply back to the same " \
"network, as this would cause a recursive loop"
log.debug('(%s) networks.remote: re-routing reply %r from network %s', irc.name,
text, placeholder_self.name)
irc.reply("Done.") # Override the source option to make sure the source is valid on the local network.
if 'source' in kwargs:
del kwargs['source']
irc.reply(text, source=irc.pseudoclient.uid, **kwargs)
old_reply = remoteirc._reply
with remoteirc._reply_lock:
try: # Remotely call the command (use the PyLink client as a dummy user).
# Override the remote irc.reply() to send replies HERE.
log.debug('(%s) networks.remote: overriding reply() of IRC object %s', irc.name, netname)
remoteirc._reply = types.MethodType(_remote_reply, remoteirc)
world.services[args.service].call_cmd(remoteirc, remoteirc.pseudoclient.uid,
' '.join(args.command))
finally:
# Restore the original remoteirc.reply()
log.debug('(%s) networks.remote: restoring reply() of IRC object %s', irc.name, netname)
remoteirc._reply = old_reply
# Remove the identification override after we finish.
try:
remoteirc.pseudoclient.account = ''
except:
log.warning('(%s) networks.remote: failed to restore pseudoclient account for %s; '
'did the remote network disconnect while running this command?', irc.name, netname)
REMOTE_IN_USE.clear()
@utils.add_cmd @utils.add_cmd
def reloadproto(irc, source, args): def reloadproto(irc, source, args):
"""<protocol module name> """<protocol module name>
Reloads the given protocol module without restart. You will have to manually disconnect and reconnect any network using the module for changes to apply.""" Reloads the given protocol module without restart. You will have to manually disconnect and reconnect any network using the module for changes to apply."""
irc.checkAuthenticated(source) permissions.check_permissions(irc, source, ['networks.reloadproto'])
try: try:
name = args[0] name = args[0]
except IndexError: except IndexError:
irc.reply('Error: Not enough arguments (needs 1: protocol module name)') irc.error('Not enough arguments (needs 1: protocol module name)')
return return
proto = utils.getProtocolModule(name) # 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)
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

@ -1,200 +1,454 @@
""" """
opercmds.py: Provides a subset of network management commands. opercmds.py: Provides a subset of network management commands.
""" """
from pylinkirc import utils import argparse
from pylinkirc import utils, world
from pylinkirc.coremods import permissions
from pylinkirc.log import log from pylinkirc.log import log
@utils.add_cmd # Having a hard limit here is sensible because otherwise it can flood the client or server off.
def checkban(irc, source, args): CHECKBAN_MAX_RESULTS = 200
"""<banmask (nick!user@host or user@host)> [<nick or hostmask to check>]
Oper only. If a nick or hostmask is given, return whether the given banmask will match it. Otherwise, returns a list of connected users that would be affected by such a ban, up to 50 results.""" def _checkban_positiveint(value):
irc.checkAuthenticated(source) value = int(value)
if value <= 0 or value > CHECKBAN_MAX_RESULTS:
raise argparse.ArgumentTypeError("%s is not a positive integer between 1 and %s." % (value, CHECKBAN_MAX_RESULTS))
return value
try: checkban_parser = utils.IRCParser()
banmask = args[0] checkban_parser.add_argument('banmask')
except IndexError: checkban_parser.add_argument('target', nargs='?', default='')
irc.reply("Error: Not enough arguments. Needs 1-2: banmask, nick or hostmask to check (optional).") checkban_parser.add_argument('--channel', default='')
return checkban_parser.add_argument('--maxresults', type=_checkban_positiveint, default=50)
try: def checkban(irc, source, args, use_regex=False):
targetmask = args[1] """<banmask> [<target nick or hostmask>] [--channel #channel] [--maxresults <num>]
except IndexError:
# No hostmask was given, return a list of affected users.
irc.msg(source, "Checking for hosts that match \x02%s\x02:" % banmask, notice=True) CHECKBAN provides a ban checker command based on nick!user@host masks, user@host masks, and
PyLink extended targets.
If a target nick or hostmask is given, this command returns whether the given banmask will match it.
Otherwise, it will display a list of connected users matching the banmask.
If the --channel argument is given without a target mask, the returned results will only
include users in the given channel.
The --maxresults option configures how many responses will be shown."""
permissions.check_permissions(irc, source, ['opercmds.checkban'])
args = checkban_parser.parse_args(args)
if not args.target:
# No hostmask was given, return a list of matched users.
results = 0 results = 0
for uid, userobj in irc.users.copy().items():
if irc.matchHost(banmask, uid):
if results < 50: # XXX rather arbitrary limit
s = "\x02%s\x02 (%s@%s) [%s] {\x02%s\x02}" % (userobj.nick, userobj.ident,
userobj.host, userobj.realname, irc.getFriendlyName(irc.getServer(uid)))
# Always reply in private to prevent information leaks. userlist_func = irc.match_all_re if use_regex else irc.match_all
irc.reply(s, private=True) irc.reply("Checking for hosts that match \x02%s\x02:" % args.banmask, private=True)
results += 1 for uid in userlist_func(args.banmask, channel=args.channel):
if results < args.maxresults:
userobj = irc.users[uid]
s = "\x02%s\x02 (%s@%s) [%s] {\x02%s\x02}" % (userobj.nick, userobj.ident,
userobj.host, userobj.realname, irc.get_friendly_name(irc.get_server(uid)))
# Always reply in private to prevent information leaks.
irc.reply(s, private=True)
results += 1
else: else:
if results: if results:
irc.msg(source, "\x02%s\x02 out of \x02%s\x02 results shown." % irc.reply("\x02%s\x02 out of \x02%s\x02 results shown." %
(min([results, 50]), results), notice=True) (min([results, args.maxresults]), results), private=True)
else: else:
irc.msg(source, "No results found.", notice=True) irc.reply("No results found.", private=True)
else: else:
# Target can be both a nick (of an online user) or a hostmask. irc.matchHost() handles this # Target can be both a nick (of an online user) or a hostmask. irc.match_host() handles this
# automatically. # automatically.
if irc.matchHost(banmask, targetmask): if irc.match_host(args.banmask, args.target):
irc.reply('Yes, \x02%s\x02 matches \x02%s\x02.' % (targetmask, banmask)) irc.reply('Yes, \x02%s\x02 matches \x02%s\x02.' % (args.target, args.banmask))
else: else:
irc.reply('No, \x02%s\x02 does not match \x02%s\x02.' % (targetmask, banmask)) irc.reply('No, \x02%s\x02 does not match \x02%s\x02.' % (args.target, args.banmask))
utils.add_cmd(checkban, aliases=('cban', 'trace'))
def checkbanre(irc, source, args):
"""<regular expression> [<target nick or hostmask>] [--channel #channel] [--maxresults <num>]
CHECKBANRE provides a ban checker command based on regular expressions matched against
users' "nick!user@host [gecos]" mask.
If a target nick or hostmask is given, this command returns whether the given banmask will match it.
Otherwise, it will display a list of connected users matching the banmask.
If the --channel argument is given without a target mask, the returned results will only
include users in the given channel.
The --maxresults option configures how many responses will be shown."""
permissions.check_permissions(irc, source, ['opercmds.checkban.re'])
return checkban(irc, source, args, use_regex=True)
utils.add_cmd(checkbanre, aliases=('crban',))
massban_parser = utils.IRCParser()
massban_parser.add_argument('channel')
massban_parser.add_argument('banmask')
# Regarding default ban reason: it's a good idea not to leave in the caller to prevent retaliation...
massban_parser.add_argument('reason', nargs='*', default=["User banned"])
massban_parser.add_argument('--quiet', '-q', action='store_true')
massban_parser.add_argument('--force', '-f', action='store_true')
massban_parser.add_argument('--include-opers', '-o', action='store_true')
def massban(irc, source, args, use_regex=False):
"""<channel> <banmask / exttarget> [<kick reason>] [--quiet/-q] [--force/-f] [--include-opers/-o]
Applies (i.e. kicks affected users) the given PyLink banmask on the specified channel.
The --quiet option can also be given to mass-mute the given user on networks where this is supported
(currently ts6, unreal, and inspircd). No kicks will be sent in this case.
By default, this command will ignore opers. This behaviour can be suppressed using the --include-opers option.
Relay CLAIM checking is used on Relay channels if it is enabled; use the --force option
to override this if needed."""
permissions.check_permissions(irc, source, ['opercmds.massban'])
args = massban_parser.parse_args(args)
reason = ' '.join(args.reason)
if args.force:
permissions.check_permissions(irc, source, ['opercmds.massban.force'])
if args.channel not in irc.channels:
irc.error("Unknown channel %r." % args.channel)
return
elif 'relay' in world.plugins and (not world.plugins['relay'].check_claim(irc, args.channel, source)) and (not args.force):
irc.error("You do not have access to set bans in %s. Ask someone to op you or use the --force option." % args.channel)
return
results = 0
userlist_func = irc.match_all_re if use_regex else irc.match_all
for uid in userlist_func(args.banmask, channel=args.channel):
if irc.is_oper(uid) and not args.include_opers:
irc.reply('Skipping banning \x02%s\x02 because they are opered.' % irc.users[uid].nick)
continue
elif irc.get_service_bot(uid):
irc.reply('Skipping banning \x02%s\x02 because it is a service client.' % irc.users[uid].nick)
continue
# Remove the target's access before banning them.
bans = [('-%s' % irc.cmodes[prefix], uid) for prefix in irc.channels[args.channel].get_prefix_modes(uid) if prefix in irc.cmodes]
# Then, add the actual ban.
bans += [irc.make_channel_ban(uid, ban_type='quiet' if args.quiet else 'ban')]
irc.mode(irc.pseudoclient.uid, args.channel, bans)
try:
irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_MASSBAN',
{'target': args.channel, 'modes': bans, 'parse_as': 'MODE'}])
except:
log.exception('(%s) Failed to send process massban hook; some bans may have not '
'been sent to plugins / relay networks!', irc.name)
if not args.quiet:
irc.kick(irc.pseudoclient.uid, args.channel, uid, reason)
# XXX: this better not be blocking...
try:
irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_MASSKICK',
{'channel': args.channel, 'target': uid, 'text': reason, 'parse_as': 'KICK'}])
except:
log.exception('(%s) Failed to send process massban hook; some kicks may have not '
'been sent to plugins / relay networks!', irc.name)
results += 1
else:
irc.reply('Banned %s users on %r.' % (results, args.channel))
log.info('(%s) Ran massban%s for %s on %s (%s user(s) removed)', irc.name, 're' if use_regex else '',
irc.get_hostmask(source), args.channel, results)
utils.add_cmd(massban, aliases=('mban',))
def massbanre(irc, source, args):
"""<channel> <regular expression> [<kick reason>] [--quiet/-q] [--include-opers/-o]
Bans users on the specified channel whose "nick!user@host [gecos]" mask matches the given Python-style regular expression.
(https://docs.python.org/3/library/re.html#regular-expression-syntax describes supported syntax)
The --quiet option can also be given to mass-mute the given user on networks where this is supported
(currently ts6, unreal, and inspircd). No kicks will be sent in this case.
By default, this command will ignore opers. This behaviour can be suppressed using the --include-opers option.
\x02Be careful when using this command, as it is easy to make mistakes with regex. Use 'checkbanre'
to check your bans first!\x02
"""
permissions.check_permissions(irc, source, ['opercmds.massban.re'])
return massban(irc, source, args, use_regex=True)
utils.add_cmd(massbanre, aliases=('rban',))
masskill_parser = utils.IRCParser()
masskill_parser.add_argument('banmask')
# Regarding default ban reason: it's a good idea not to leave in the caller to prevent retaliation...
masskill_parser.add_argument('reason', nargs='*', default=["User banned"], type=str)
masskill_parser.add_argument('--akill', '-ak', action='store_true')
masskill_parser.add_argument('--force-kb', '-f', action='store_true')
masskill_parser.add_argument('--include-opers', '-o', action='store_true')
def masskill(irc, source, args, use_regex=False):
"""<banmask / exttarget> [<kill/ban reason>] [--akill/ak] [--force-kb/-f] [--include-opers/-o]
Kills all users matching the given PyLink banmask.
The --akill option can also be given to convert kills to akills, which expire after 7 days.
For relay users, attempts to kill are forwarded as a kickban to every channel where the calling user
meets claim requirements to set a ban (i.e. this is true if you are opped, if your network is in claim list, etc.;
see "help CLAIM" for more specific rules). This can also be extended to all shared channels
the user is in using the --force-kb option (we hope this feature is only used for good).
By default, this command will ignore opers. This behaviour can be suppressed using the --include-opers option.
To properly kill abusers on another network, combine this command with the 'remote' command in the
'networks' plugin and adjust your banmasks accordingly."""
permissions.check_permissions(irc, source, ['opercmds.masskill'])
args = masskill_parser.parse_args(args)
if args.force_kb:
permissions.check_permissions(irc, source, ['opercmds.masskill.force'])
reason = ' '.join(args.reason)
results = killed = 0
userlist_func = irc.match_all_re if use_regex else irc.match_all
seen_users = set()
for uid in userlist_func(args.banmask):
userobj = irc.users[uid]
if irc.is_oper(uid) and not args.include_opers:
irc.reply('Skipping killing \x02%s\x02 because they are opered.' % userobj.nick)
continue
elif irc.get_service_bot(uid):
irc.reply('Skipping killing \x02%s\x02 because it is a service client.' % userobj.nick)
continue
relay = world.plugins.get('relay')
if relay and hasattr(userobj, 'remote'):
# For relay users, forward kill attempts as kickban because we don't want networks k-lining each others' users.
bans = [irc.make_channel_ban(uid)]
for channel in userobj.channels.copy(): # Look in which channels the user appears to be in locally
if (args.force_kb or relay.check_claim(irc, channel, source)):
irc.mode(irc.pseudoclient.uid, channel, bans)
irc.kick(irc.pseudoclient.uid, channel, uid, reason)
# XXX: code duplication with massban.
try:
irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_MASSKILL_BAN',
{'target': channel, 'modes': bans, 'parse_as': 'MODE'}])
irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_MASSKILL_KICK',
{'channel': channel, 'target': uid, 'text': reason, 'parse_as': 'KICK'}])
except:
log.exception('(%s) Failed to send process massban hook; some kickbans may have not '
'been sent to plugins / relay networks!', irc.name)
if uid not in seen_users: # Don't count users multiple times on different channels
killed += 1
else:
irc.reply("Not kicking \x02%s\x02 from \x02%s\x02 because you don't have CLAIM access. If this is "
"another network's channel, ask someone to op you or use the --force-kb option." % (userobj.nick, channel))
else:
if args.akill: # TODO: configurable length via strings such as "2w3d5h6m3s" - though month and minute clash this way?
if not (userobj.realhost or userobj.ip):
irc.reply("Skipping akill on %s because PyLink doesn't know the real host." % irc.get_hostmask(uid))
continue
irc.set_server_ban(irc.pseudoclient.uid, 604800, host=userobj.realhost or userobj.ip or userobj.host, reason=reason)
else:
irc.kill(irc.pseudoclient.uid, uid, reason)
try:
irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_MASSKILL',
{'target': uid, 'parse_as': 'KILL', 'userdata': userobj, 'text': reason}])
except:
log.exception('(%s) Failed to send process massban hook; some kickbans may have not '
'been sent to plugins / relay networks!', irc.name)
killed += 1
results += 1
seen_users.add(uid)
else:
log.info('(%s) Ran masskill%s for %s (%s/%s user(s) removed)', irc.name, 're' if use_regex else '',
irc.get_hostmask(source), killed, results)
irc.reply('Masskilled %s/%s users.' % (killed, results))
utils.add_cmd(masskill, aliases=('mkill',))
def masskillre(irc, source, args):
"""<regular expression> [<kill/ban reason>] [--akill/ak] [--force-kb/-f] [--include-opers/-o]
Kills all users whose "nick!user@host [gecos]" mask matches the given Python-style regular expression.
(https://docs.python.org/3/library/re.html#regular-expression-syntax describes supported syntax)
The --akill option can also be given to convert kills to akills that expire after 7 days.
For relay users, attempts to kill are forwarded as a kickban to every channel where the calling user
meets claim requirements to set a ban (i.e. this is true if you are opped, if your network is in claim list, etc.;
see "help CLAIM" for more specific rules). This can also be extended to all shared channels
the user is in using the --force-kb option (we hope this feature is only used for good).
By default, this command will ignore opers. This behaviour can be suppressed using the --include-opers option.
\x02Be careful when using this command, as it is easy to make mistakes with regex. Use 'checkbanre'
to check your bans first!\x02
"""
permissions.check_permissions(irc, source, ['opercmds.masskill.re'])
return masskill(irc, source, args, use_regex=True)
utils.add_cmd(masskillre, aliases=('rkill',))
@utils.add_cmd @utils.add_cmd
def jupe(irc, source, args): def jupe(irc, source, args):
"""<server> [<reason>] """<server> [<reason>]
Admin only, jupes the given server.""" Jupes the given server."""
# Check that the caller is either opered or logged in as admin. permissions.check_permissions(irc, source, ['opercmds.jupe'])
irc.checkAuthenticated(source, allowOper=False)
try: try:
servername = args[0] servername = args[0]
reason = ' '.join(args[1:]) or "No reason given" reason = ' '.join(args[1:]) or "No reason given"
desc = "Juped by %s: [%s]" % (irc.getHostmask(source), reason) desc = "Juped by %s: [%s]" % (irc.get_hostmask(source), reason)
except IndexError: except IndexError:
irc.reply('Error: Not enough arguments. Needs 1-2: servername, reason (optional).') irc.error('Not enough arguments. Needs 1-2: servername, reason (optional).')
return return
if not utils.isServerName(servername): if not irc.is_server_name(servername):
irc.reply("Error: Invalid server name '%s'." % servername) irc.error("Invalid server name %r." % servername)
return return
sid = irc.proto.spawnServer(servername, desc=desc) sid = irc.spawn_server(servername, desc=desc)
irc.callHooks([irc.pseudoclient.uid, 'OPERCMDS_SPAWNSERVER', irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_SPAWNSERVER',
{'name': servername, 'sid': sid, 'text': desc}]) {'name': servername, 'sid': sid, 'text': desc}])
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):
"""<source> <channel> <user> [<reason>] """<channel> <user> [<reason>]
Admin only. Kicks <user> from <channel> via <source>, where <source> is either the nick of a PyLink client or the SID of a PyLink server.""" Kicks <user> from the specified channel."""
irc.checkAuthenticated(source, allowOper=False) permissions.check_permissions(irc, source, ['opercmds.kick'])
try: try:
sourcenick = args[0] channel = args[0]
channel = irc.toLower(args[1])
target = args[2]
reason = ' '.join(args[3:])
except IndexError:
irc.reply("Error: Not enough arguments. Needs 3-4: source nick, channel, target, reason (optional).")
return
# Convert the source and target nicks to UIDs.
sender = irc.nickToUid(sourcenick) or sourcenick
targetu = irc.nickToUid(target)
if channel not in irc.channels: # KICK only works on channels that exist.
irc.reply("Error: Unknown channel %r." % channel)
return
if (not irc.isInternalClient(sender)) and \
(not irc.isInternalServer(sender)):
# Whatever we were told to send the kick from wasn't valid; try to be
# somewhat user friendly in the error message
irc.reply("Error: No such PyLink client '%s'. The first argument to "
"KICK should be the name of a PyLink client (e.g. '%s'; see "
"'help kick' for details." % (sourcenick,
irc.pseudoclient.nick))
return
elif not targetu:
# Whatever we were told to kick doesn't exist!
irc.reply("Error: No such target nick '%s'." % target)
return
irc.proto.kick(sender, channel, targetu, reason)
irc.callHooks([sender, 'CHANCMDS_KICK', {'channel': channel, 'target': targetu,
'text': reason, 'parse_as': 'KICK'}])
@utils.add_cmd
def kill(irc, source, args):
"""<source> <target> [<reason>]
Admin only. Kills <target> via <source>, where <source> is either the nick of a PyLink client or the SID of a PyLink server."""
irc.checkAuthenticated(source, allowOper=False)
try:
sourcenick = args[0]
target = args[1] target = args[1]
reason = ' '.join(args[2:]) reason = ' '.join(args[2:])
except IndexError: except IndexError:
irc.reply("Error: Not enough arguments. Needs 3-4: source nick, target, reason (optional).") irc.error("Not enough arguments. Needs 2-3: channel, target, reason (optional).")
return
if channel not in irc.channels: # KICK only works on channels that exist.
irc.error("Unknown channel %r." % channel)
return
targetu = _try_find_target(irc, target)
sender = irc.pseudoclient.uid
irc.kick(sender, channel, targetu, reason)
irc.reply("Done.")
irc.call_hooks([sender, 'OPERCMDS_KICK', {'channel': channel, 'target': targetu,
'text': reason, 'parse_as': 'KICK'}])
@utils.add_cmd
def kill(irc, source, args):
"""<target> [<reason>]
Kills the given target."""
permissions.check_permissions(irc, source, ['opercmds.kill'])
try:
target = args[0]
reason = ' '.join(args[1:])
except IndexError:
irc.error("Not enough arguments. Needs 1-2: target, reason (optional).")
return return
# Convert the source and target nicks to UIDs. # Convert the source and target nicks to UIDs.
sender = irc.nickToUid(sourcenick) or sourcenick sender = irc.pseudoclient.uid
targetu = irc.nickToUid(target)
targetu = _try_find_target(irc, target)
if irc.pseudoclient.uid == targetu:
irc.error("Cannot kill the main PyLink client!")
return
userdata = irc.users.get(targetu) userdata = irc.users.get(targetu)
if (not irc.isInternalClient(sender)) and \ reason = "Requested by %s: %s" % (irc.get_friendly_name(source), reason)
(not irc.isInternalServer(sender)):
# Whatever we were told to send the kick from wasn't valid; try to be
# somewhat user friendly in the error message
irc.reply("Error: No such PyLink client '%s'. The first argument to "
"KILL should be the name of a PyLink client (e.g. '%s'; see "
"'help kill' for details." % (sourcenick,
irc.pseudoclient.nick))
return
elif targetu not in irc.users:
# Whatever we were told to kick doesn't exist!
irc.reply("Error: No such nick '%s'." % target)
return
irc.proto.kill(sender, targetu, reason) irc.kill(sender, targetu, reason)
# Format the kill reason properly in hooks. irc.reply("Done.")
reason = "Killed (%s (%s))" % (irc.getFriendlyName(sender), reason) irc.call_hooks([source, 'OPERCMDS_KILL', {'target': targetu, 'text': reason,
'userdata': userdata, 'parse_as': 'KILL'}])
irc.callHooks([sender, 'CHANCMDS_KILL', {'target': targetu, 'text': reason,
'userdata': userdata, 'parse_as': 'KILL'}])
@utils.add_cmd @utils.add_cmd
def mode(irc, source, args): def mode(irc, source, args):
"""<channel> <modes> """<channel> <modes>
Oper-only, sets modes <modes> on the target channel.""" Sets the given modes on the target channel."""
# Check that the caller is either opered or logged in as admin. permissions.check_permissions(irc, source, ['opercmds.mode'])
irc.checkAuthenticated(source)
try: try:
target, modes = args[0], args[1:] target, modes = args[0], args[1:]
except IndexError: except IndexError:
irc.reply('Error: Not enough arguments. Needs 2: target, modes to set.') irc.error('Not enough arguments. Needs 2: target, modes to set.')
return return
if target not in irc.channels: if target not in irc.channels:
irc.reply("Error: Unknown channel '%s'." % target) irc.error("Unknown channel %r." % target)
return return
elif not modes: elif not modes:
# No modes were given before parsing (i.e. mode list was blank). # No modes were given before parsing (i.e. mode list was blank).
irc.reply("Error: No valid modes were given.") irc.error("No valid modes were given.")
return return
parsedmodes = irc.parseModes(target, modes) parsedmodes = irc.parse_modes(target, modes)
if not parsedmodes: if not parsedmodes:
# Modes were given but they failed to parse into anything meaningful. # Modes were given but they failed to parse into anything meaningful.
# For example, "mode #somechan +o" would be erroneous because +o # For example, "mode #somechan +o" would be erroneous because +o
# requires an argument! # requires an argument!
irc.reply("Error: No valid modes were given.") irc.error("No valid modes were given.")
return return
irc.proto.mode(irc.pseudoclient.uid, target, parsedmodes) irc.mode(irc.pseudoclient.uid, target, parsedmodes)
# Call the appropriate hooks for plugins like relay. # Call the appropriate hooks for plugins like relay.
irc.callHooks([irc.pseudoclient.uid, 'OPERCMDS_MODEOVERRIDE', irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_MODE',
{'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}]) {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}])
irc.reply("Done.") irc.reply("Done.")
@ -203,21 +457,60 @@ def mode(irc, source, args):
def topic(irc, source, args): def topic(irc, source, args):
"""<channel> <topic> """<channel> <topic>
Admin only. Updates the topic in a channel.""" Changes the topic in a channel."""
irc.checkAuthenticated(source, allowOper=False) permissions.check_permissions(irc, source, ['opercmds.topic'])
try: try:
channel = args[0] channel = args[0]
topic = ' '.join(args[1:]) topic = ' '.join(args[1:])
except IndexError: except IndexError:
irc.reply("Error: Not enough arguments. Needs 2: channel, topic.") irc.error("Not enough arguments. Needs 2: channel, topic.")
return return
if channel not in irc.channels: if channel not in irc.channels:
irc.reply("Error: Unknown channel %r." % channel) irc.error("Unknown channel %r." % channel)
return return
irc.proto.topic(irc.pseudoclient.uid, channel, topic) irc.topic(irc.pseudoclient.uid, channel, topic)
irc.callHooks([irc.pseudoclient.uid, 'CHANCMDS_TOPIC', irc.reply("Done.")
irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_TOPIC',
{'channel': channel, 'text': topic, 'setter': source, {'channel': channel, 'text': topic, 'setter': source,
'parse_as': 'TOPIC'}]) 'parse_as': 'TOPIC'}])
@utils.add_cmd
def chghost(irc, source, args):
"""<user> <new host>
Changes the visible host of the target user."""
_chgfield(irc, source, args, 'host')
@utils.add_cmd
def chgident(irc, source, args):
"""<user> <new ident>
Changes the ident of the target user."""
_chgfield(irc, source, args, 'ident')
@utils.add_cmd
def chgname(irc, source, args):
"""<user> <new name>
Changes the GECOS (realname) of the target user."""
_chgfield(irc, source, args, 'name', 'GECOS')
def _chgfield(irc, source, args, human_field, internal_field=None):
"""Helper function for chghost/chgident/chgname."""
permissions.check_permissions(irc, source, ['opercmds.chg' + human_field])
try:
target = args[0]
new = args[1]
except IndexError:
irc.error("Not enough arguments. Needs 2: target, new %s." % human_field)
return
# Find the user
targetu = _try_find_target(irc, target)
internal_field = internal_field or human_field.upper()
irc.update_client(targetu, internal_field, new)
irc.reply("Done.")

33
plugins/raw.py Normal file
View File

@ -0,0 +1,33 @@
"""
raw.py: Provides a 'raw' command for sending raw text to IRC.
"""
from pylinkirc import utils
from pylinkirc.coremods import permissions
from pylinkirc.log import log
from pylinkirc import conf
@utils.add_cmd
def raw(irc, source, args):
"""<text>
Sends raw text to the IRC server.
Use with caution - This command is only officially supported on Clientbot networks."""
if not conf.conf['pylink'].get("raw_enabled", False):
raise RuntimeError("Raw commands are not supported on this protocol")
# exec.raw is included for backwards compatibility with PyLink 1.x
permissions.check_permissions(irc, source, ['raw.raw', 'exec.raw'])
args = ' '.join(args)
if not args.strip():
irc.reply('No text entered!')
return
# Note: This is loglevel debug so that we don't risk leaking things like
# NickServ passwords on Clientbot networks.
log.debug('(%s) Sending raw text %r to IRC for %s', irc.name, args,
irc.get_hostmask(source))
irc.send(args)
irc.reply("Done.")

File diff suppressed because it is too large Load Diff

View File

@ -1,65 +1,82 @@
# relay_clientbot.py: Clientbot extensions for Relay # relay_clientbot.py: Clientbot extensions for Relay
import shlex
import string import string
import collections
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
# TODO: document configurable styles in relay::clientbot_styles::COMMAND_NAME # Clientbot default styles:
# These use template strings as documented @ https://docs.python.org/3/library/string.html#template-strings # These use template strings as documented @ https://docs.python.org/3/library/string.html#template-strings
default_styles = {'MESSAGE': '\x02[$colored_netname]\x02 <$colored_sender> $text', default_styles = {'MESSAGE': '\x02[$netname]\x02 <$mode_prefix$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 * $mode_prefix$colored_sender $text',
'NOTICE': '\x02[$colored_netname]\x02 - Notice from $colored_sender: $text', 'NOTICE': '\x02[$netname]\x02 - Notice from $mode_prefix$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',
'PNOTICE': '<$sender> $text',
} }
def color_text(s): 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.
(sum of all characters).
""" """
colors = ('02', '03', '04', '05', '06', '07', '08', '09', '10', '11', if not s:
'12', '13') return s
num = sum([ord(char) for char in s]) colors = ('03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '15')
num = num % len(colors) hash_output = hash(s.encode())
num = hash_output % len(colors)
return "\x03%s%s\x03" % (colors[num], s) return "\x03%s%s\x03" % (colors[num], s)
def cb_relay_core(irc, source, command, args): def cb_relay_core(irc, source, command, args):
""" """
This function takes Clientbot actions and outputs them to a channel as regular text. This function takes Clientbot events and formats them as text to the target channel / user.
""" """
real_command = command.split('_')[-1] real_command = command.split('_')[-1]
relay = world.plugins.get('relay') relay = world.plugins.get('relay')
private = False
if irc.pseudoclient and relay: if irc.pseudoclient and relay:
try: try:
sourcename = irc.getFriendlyName(source) sourcename = irc.get_friendly_name(source)
except KeyError: # User has left due to /quit except KeyError: # User has left due to /quit
sourcename = args['userdata'].nick sourcename = args['userdata'].nick
relay_conf = conf.conf.get('relay', {}) relay_conf = conf.conf.get('relay') or {}
# Be less floody on startup: don't relay non-PRIVMSGs for the first X seconds after connect. # Be less floody on startup: don't relay non-PRIVMSGs for the first X seconds after connect.
startup_delay = relay_conf.get('clientbot_startup_delay', 5) startup_delay = relay_conf.get('clientbot_startup_delay', 20)
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(target):
# Target is a user; handle this accordingly.
if relay_conf.get('allow_clientbot_pms'):
real_command = 'PNOTICE' if args.get('is_notice') else 'PM'
private = True
# Other CTCPs are ignored # Other CTCPs are ignored
elif args['text'].startswith('\x01'): elif args['text'].startswith('\x01'):
return return
elif args.get('is_notice'): # Different syntax for notices elif args.get('is_notice'): # Different syntax for notices
real_command = 'NOTICE' real_command = 'NOTICE'
elif (time.time() - irc.start_ts) < startup_delay: elif (time.time() - irc.start_ts) < startup_delay:
@ -70,65 +87,108 @@ 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 = relay_conf.get('clientbot_styles', {}).get(real_command, text_template = irc.get_service_options('relay', 'clientbot_styles', dict).get(
default_styles.get(real_command, '')) real_command, default_styles.get(real_command, ''))
text_template = string.Template(text_template) text_template = string.Template(text_template)
if text_template: if text_template:
# Get the original client that the relay client source was meant for. if irc.get_service_bot(source):
log.debug('(%s) relay_cb_core: Trying to find original sender (user) for %s', irc.name, source) # HACK: service bots are global and lack the relay state we look for.
try: # just pretend the message comes from the current network.
origuser = relay.getOrigUser(irc, source) or args['userdata'].remote log.debug('(%s) relay_cb_core: Overriding network origin to local (source=%s)', irc.name, source)
except (AttributeError, KeyError): sourcenet = irc.name
log.debug('(%s) relay_cb_core: Trying to find original sender (server) for %s. serverdata=%s', irc.name, source, args.get('serverdata')) realsource = source
else:
# Get the original client that the relay client source was meant for.
log.debug('(%s) relay_cb_core: Trying to find original sender (user) for %s', irc.name, source)
try: try:
origuser = ((args.get('serverdata') or irc.servers[source]).remote,) origuser = relay.get_orig_user(irc, source) or args['userdata'].remote
except (AttributeError, KeyError): except (AttributeError, KeyError):
return log.debug('(%s) relay_cb_core: Trying to find original sender (server) for %s. serverdata=%s', irc.name, source, args.get('serverdata'))
try:
localsid = args.get('serverdata') or irc.servers[source]
origuser = (localsid.remote, world.networkobjects[localsid.remote].uplink)
except (AttributeError, KeyError):
return
log.debug('(%s) relay_cb_core: Original sender found as %s', irc.name, origuser)
sourcenet, realsource = origuser
log.debug('(%s) relay_cb_core: Original sender found as %s', irc.name, origuser)
netname = origuser[0]
try: # Try to get the full network name try: # Try to get the full network name
netname = conf.conf['servers'][netname]['netname'].lower() netname = conf.conf['servers'][sourcenet]['netname']
except KeyError: except KeyError:
pass netname = sourcenet
# Figure out where the message is destined to. # Figure out where the message is destined to.
target = args.get('channel') or args.get('target') stripped_target = target = args.get('channel') or args.get('target')
if target is None or not utils.isChannel(target): if isinstance(target, str):
# Quit and nick messages are not channel specific. Figure out all channels that the # HACK: cheap fix to prevent @#channel messages from interpreted as non-channel specific
# sender shares over the relay, and relay them that way. stripped_target = target.lstrip(''.join(irc.prefixmodes.values()))
if target is None or not (irc.is_channel(stripped_target) or private):
# Non-channel specific message (e.g. QUIT or NICK). If this isn't a PM, figure out
# all channels that the sender shares over the relay, and relay them to those
# channels.
userdata = args.get('userdata') or irc.users.get(source) userdata = args.get('userdata') or irc.users.get(source)
if not userdata: if not userdata:
# No user data given. This was probably some other global event such as SQUIT. # No user data given. This was probably some other global event such as SQUIT.
userdata = irc.pseudoclient userdata = irc.pseudoclient
channels = [channel for channel in userdata.channels if relay.getRelay((irc.name, channel))] targets = [channel for channel in userdata.channels if relay.get_relay(irc, channel)]
else: else:
# Pluralize the channel so that we can iterate over it. # Pluralize the channel so that we can iterate over it.
channels = [target] targets = [target]
log.debug('(%s) relay_cb_core: Relaying event %s to channels: %s', irc.name, real_command, channels) args['channel'] = stripped_target
log.debug('(%s) relay_cb_core: Relaying event %s to channels: %s', irc.name, real_command, targets)
identhost = ''
if source in irc.users: if source in irc.users:
try: try:
identhost = irc.getHostmask(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
else:
identhost = ''
# $target_nick: Convert the target for kicks, etc. from a UID to a nick # $target_nick: Convert the target for kicks, etc. from a UID to a nick
if args.get("target") in irc.users: if args.get("target") in irc.users:
args["target_nick"] = irc.getFriendlyName(args['target']) args["target_nick"] = irc.get_friendly_name(args['target'])
args.update({'netname': netname, 'sender': sourcename, 'sender_identhost': identhost, # Join up modes from their list form
'colored_sender': color_text(sourcename), 'colored_netname': color_text(netname)}) if args.get('modes'):
args['modes'] = irc.join_modes(args['modes'])
for channel in channels: mode_prefix = ''
cargs = args.copy() # Copy args list to manipualte them in a channel specific way if 'channel' in args:
# Display the real (remote) channel name instead of the local one, if applicable.
args['local_channel'] = args['channel']
log.debug('(%s) relay_clientbot: coersing $channel from %s to %s', irc.name, args['local_channel'], args['channel'])
sourceirc = world.networkobjects.get(sourcenet)
log.debug('(%s) relay_clientbot: Checking prefix modes for %s on %s (relaying to %s)',
irc.name, realsource, sourcenet, args['channel'])
if sourceirc:
args['channel'] = remotechan = relay.get_remote_channel(irc, sourceirc, args['channel'])
if source in irc.users and remotechan in sourceirc.channels and \
realsource in sourceirc.channels[remotechan].users:
# Fetch the prefixmode prefixes (e.g. ~@%) for the sender, if available.
prefixmodes = sourceirc.channels[remotechan].get_prefix_modes(realsource)
log.debug('(%s) relay_clientbot: got prefix modes %s for %s on %s@%s',
irc.name, prefixmodes, realsource, remotechan, sourcenet)
if prefixmodes:
# Only pick the highest prefix.
mode_prefix = sourceirc.prefixmodes.get(
sourceirc.cmodes.get(prefixmodes[0]))
args.update({
'netname': netname, 'sender': sourcename, 'sender_identhost': identhost,
'colored_sender': color_text(sourcename), 'colored_netname': color_text(netname),
'mode_prefix': mode_prefix
})
for target in targets:
cargs = args.copy() # Copy args list to manipulate them in a channel specific way
# $nicks / $colored_nicks: used when the event affects multiple users, such as SJOIN or SQUIT. # $nicks / $colored_nicks: used when the event affects multiple users, such as SJOIN or SQUIT.
# For SJOIN, this is simply a list of nicks. For SQUIT, this is sent as a dict # For SJOIN, this is simply a list of nicks. For SQUIT, this is sent as a dict
@ -136,10 +196,9 @@ def cb_relay_core(irc, source, command, args):
# still have to be relayed as such. # still have to be relayed as such.
nicklist = args.get('nicks') nicklist = args.get('nicks')
if nicklist: if nicklist:
# Get channel-specific nick list if relevent. # Get channel-specific nick list if relevent.
if type(nicklist) == collections.defaultdict: if isinstance(nicklist, dict):
nicklist = nicklist.get(channel, []) nicklist = nicklist.get(target, [])
# Ignore if no nicks are affected on the channel. # Ignore if no nicks are affected on the channel.
if not nicklist: if not nicklist:
@ -152,7 +211,8 @@ def cb_relay_core(irc, source, command, args):
cargs['colored_nicks'] = ', '.join(colored_nicks) cargs['colored_nicks'] = ', '.join(colored_nicks)
text = text_template.safe_substitute(cargs) text = text_template.safe_substitute(cargs)
irc.proto.message(irc.pseudoclient.uid, channel, text) # PMs are always sent as notice - this prevents unknown command loops with bots.
irc.msg(target, text, loopback=False, notice=private)
utils.add_hook(cb_relay_core, 'CLIENTBOT_MESSAGE') utils.add_hook(cb_relay_core, 'CLIENTBOT_MESSAGE')
utils.add_hook(cb_relay_core, 'CLIENTBOT_KICK') utils.add_hook(cb_relay_core, 'CLIENTBOT_KICK')
@ -162,3 +222,53 @@ utils.add_hook(cb_relay_core, 'CLIENTBOT_QUIT')
utils.add_hook(cb_relay_core, 'CLIENTBOT_NICK') utils.add_hook(cb_relay_core, 'CLIENTBOT_NICK')
utils.add_hook(cb_relay_core, 'CLIENTBOT_SJOIN') utils.add_hook(cb_relay_core, 'CLIENTBOT_SJOIN')
utils.add_hook(cb_relay_core, 'CLIENTBOT_SQUIT') utils.add_hook(cb_relay_core, 'CLIENTBOT_SQUIT')
utils.add_hook(cb_relay_core, 'RELAY_RAW_MODE')
@utils.add_cmd
def rpm(irc, source, args):
"""<target nick/UID> <text>
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:
target = args[0]
text = ' '.join(args[1:])
except IndexError:
irc.error('Not enough arguments. Needs 2: target nick and text.')
return
relay = world.plugins.get('relay')
if irc.has_cap('can-spawn-clients'):
irc.error('This command is only supported on Clientbot networks. Try /msg %s <text>' % target)
return
elif relay is None:
irc.error('PyLink Relay is not loaded.')
return
elif not text:
irc.error('No text given.')
return
elif not conf.conf.get('relay').get('allow_clientbot_pms'):
irc.error('Private messages with users connected via Clientbot have been '
'administratively disabled.')
return
if target in irc.users:
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)
return
elif len(uids) > 1:
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
else:
assert not irc.is_internal_client(source), "rpm is not allowed from PyLink bots"
# Send the message through relay by faking a hook for its handler.
relay.handle_messages(irc, source, 'RELAY_CLIENTBOT_PRIVMSG', {'target': uids[0], 'text': text})
irc.reply('Message sent.')

125
plugins/servermaps.py Normal file
View File

@ -0,0 +1,125 @@
# servermaps.py: Maps out connected IRC servers.
import collections
from pylinkirc import utils, world
from pylinkirc.coremods import permissions
from pylinkirc.log import log
DEFAULT_PERMISSIONS = {"$ircop": ['servermaps.localmap']}
def main(irc=None):
"""Servermaps plugin main function, called on plugin load."""
# Register our permissions.
permissions.add_default_permissions(DEFAULT_PERMISSIONS)
def die(irc=None):
"""Servermaps plugin die function, called on plugin unload."""
permissions.remove_default_permissions(DEFAULT_PERMISSIONS)
def _percent(num, total):
return '%.1f' % (num/total*100)
def _map(irc, source, args, show_relay=True):
"""[<network>]
Shows the network map for the given network, or the current network if not specified."""
if show_relay:
perm = 'servermaps.map'
else:
perm = 'servermaps.localmap'
permissions.check_permissions(irc, source, [perm])
try:
netname = args[0]
except IndexError:
netname = irc.name
try:
ircobj = world.networkobjects[netname]
except KeyError:
irc.error('no such network %s' % netname)
return
servers = collections.defaultdict(set)
hostsid = ircobj.sid
usercount = len(ircobj.users)
# Iterate over every connected server on every network.
for remotenet, remoteirc in world.networkobjects.items():
for sid, serverobj in remoteirc.servers.copy().items():
if sid == remoteirc.sid: # Don't re-add our own SID to the index
continue
# Save the server as UNDER its uplink.
servers[(remotenet, serverobj.uplink or remoteirc.sid)].add(sid)
log.debug('(%s) servermaps.map servers fetched for %s: %s', irc.name, netname, servers)
reply = lambda text: irc.reply(text, private=True)
def showall(ircobj, sid, hops=0, is_relay_server=False):
log.debug('servermaps: got showall() for SID %s on network %s', sid, ircobj.name)
serverlist = ircobj.servers.copy()
netname = ircobj.name
if hops == 0:
# Show our root server once.
rootusers = len(serverlist[sid].users)
reply('\x02%s\x02[%s]: %s user(s) (%s%%) {hopcount: %d}' % (serverlist[sid].name, sid,
rootusers, _percent(rootusers, usercount), serverlist[sid].hopcount))
log.debug('(%s) servermaps: servers under sid %s: %s', irc.name, sid, servers)
# Every time we descend a server to process its map, raise the hopcount used in formatting.
hops += 1
leaves = servers[(netname, sid)]
for leafcount, leaf in enumerate(leaves):
if is_relay_server and hasattr(serverlist[leaf], 'remote'):
# Don't show relay subservers more than once.
continue
serverusers = len(serverlist[leaf].users)
if is_relay_server:
# Skip showing user data for relay servers.
reply("%s\x02%s\x02[%s] (via PyLink Relay)" %
(' '*hops, serverlist[leaf].name, leaf))
else:
reply("%s\x02%s\x02[%s]: %s user(s) (%s%%) {hopcount: %d}" %
(' '*hops, serverlist[leaf].name, leaf,
serverusers, _percent(serverusers, usercount), serverlist[leaf].hopcount))
showall(ircobj, leaf, hops, is_relay_server=is_relay_server)
if (not is_relay_server) and hasattr(serverlist[leaf], 'remote') and show_relay:
# This is a relay server - display the remote map of the network it represents
relay_server = serverlist[leaf].remote
remoteirc = world.networkobjects[relay_server]
if remoteirc.has_cap('can-track-servers'):
# Only ever show relay subservers once - this prevents infinite loops.
showall(remoteirc, remoteirc.sid, hops=hops, is_relay_server=True)
else:
# For Clientbot links, show the server we're actually connected to.
reply("%s\x02%s\x02 (actual server name)" %
(' '*(hops+1), remoteirc.uplink))
else:
# Afterwards, decrement the hopcount.
hops -= 1
# Start the map at our PyLink server
firstserver = hostsid
showall(ircobj, firstserver)
serverlist = irc.servers
reply('Total %s users on %s local servers - average of %1.f per server' % (usercount, len(serverlist),
usercount/len(serverlist)))
utils.add_cmd(_map, 'map')
@utils.add_cmd
def localmap(irc, source, args):
"""[<network>]
Shows the network map for the given network, or the current network if not specified.
This command does not expand Relay subservers."""
_map(irc, source, args, show_relay=False)

View File

@ -1,24 +1,42 @@
# 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 import threading
from pylinkirc import conf, utils
from pylinkirc.log import log from pylinkirc.log import log
# TODO: make length and time configurable try:
savecache = ExpiringDict(max_len=5, max_age_seconds=10) from cachetools import TTLCache
killcache = ExpiringDict(max_len=5, max_age_seconds=10) 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
servprotect_conf = conf.conf.get('servprotect', {})
length = servprotect_conf.get('length', 10)
age = servprotect_conf.get('age', 10)
def _new_cache_dict():
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):
""" """
Tracks kills against PyLink clients. If too many are received, Tracks kills against PyLink clients. If too many are received,
automatically disconnects from the network. automatically disconnects from the network.
""" """
if killcache.setdefault(irc.name, 1) >= 5:
log.error('(%s) servprotect: Too many kills received, aborting!', irc.name)
irc.disconnect()
log.debug('(%s) servprotect: Incrementing killcache by 1', irc.name) if (args['userdata'] and irc.is_internal_server(args['userdata'].server)) or irc.is_internal_client(args['target']):
killcache[irc.name] += 1 with lock:
if killcache.setdefault(irc.name, 1) >= length:
log.error('(%s) servprotect: Too many kills received, aborting!', irc.name)
irc.disconnect()
log.debug('(%s) servprotect: Incrementing killcache by 1', irc.name)
killcache[irc.name] += 1
utils.add_hook(handle_kill, 'KILL') utils.add_hook(handle_kill, 'KILL')
@ -27,11 +45,13 @@ def handle_save(irc, numeric, command, args):
Tracks SAVEs (nick collision) against PyLink clients. If too many are received, Tracks SAVEs (nick collision) against PyLink clients. If too many are received,
automatically disconnects from the network. automatically disconnects from the network.
""" """
if savecache.setdefault(irc.name, 0) >= 5: if irc.is_internal_client(args['target']):
log.error('(%s) servprotect: Too many nick collisions, aborting!', irc.name) with lock:
irc.disconnect() if savecache.setdefault(irc.name, 0) >= length:
log.error('(%s) servprotect: Too many nick collisions, aborting!', irc.name)
irc.disconnect()
log.debug('(%s) servprotect: Incrementing savecache by 1', irc.name) log.debug('(%s) servprotect: Incrementing savecache by 1', irc.name)
savecache[irc.name] += 1 savecache[irc.name] += 1
utils.add_hook(handle_save, 'SAVE') utils.add_hook(handle_save, 'SAVE')

129
plugins/stats.py Normal file
View File

@ -0,0 +1,129 @@
"""
stats.py: Simple statistics for PyLink IRC Services.
"""
import datetime
import time
from pylinkirc import conf, utils, world
from pylinkirc.coremods import permissions
from pylinkirc.log import log
def timediff(before, now):
"""
Returns the time difference between "before" and "now" as a formatted string.
"""
td = datetime.timedelta(seconds=now-before)
days = td.days
hours, leftover = divmod(td.seconds, 3600)
minutes, seconds = divmod(leftover, 60)
# XXX: I would make this more configurable but it's a lot of work for little gain,
# since there's no strftime for time differences.
return '%d day%s, %02d:%02d:%02d' % (td.days, 's' if td.days != 1 else '',
hours, minutes, seconds)
# From RFC 2822: https://tools.ietf.org/html/rfc2822.html#section-3.3
DEFAULT_TIME_FORMAT = "%a, %d %b %Y %H:%M:%S +0000"
@utils.add_cmd
def uptime(irc, source, args):
"""[<network> / --all]
Returns the uptime for PyLink and the given network's connection (or the current network if not specified).
The --all argument can also be given to show the uptime for all networks."""
permissions.check_permissions(irc, source, ['stats.uptime'])
try:
network = args[0]
except IndexError:
network = irc.name
if network == '--all': # XXX: we really need smart argument parsing some time
# Filter by all connected networks.
ircobjs = {k:v for k,v in world.networkobjects.items() if v.connected.is_set()}
else:
try:
ircobjs = {network: world.networkobjects[network]}
except KeyError:
irc.error("No such network %r." % network)
return
if not world.networkobjects[network].connected.is_set():
irc.error("Network %s is not connected." % network)
return
current_time = int(time.time())
time_format = conf.conf.get('stats', {}).get('time_format', DEFAULT_TIME_FORMAT)
irc.reply("PyLink uptime: \x02%s\x02 (started on %s)" %
(timediff(world.start_ts, current_time),
time.strftime(time_format, time.gmtime(world.start_ts))
)
)
for network, ircobj in sorted(ircobjs.items()):
irc.reply("Connected to %s: \x02%s\x02 (connected on %s)" %
(network,
timediff(ircobj.start_ts, current_time),
time.strftime(time_format, time.gmtime(ircobj.start_ts))
)
)
def handle_stats(irc, source, command, args):
"""/STATS handler. Currently supports the following:
c - link blocks
o - oper blocks (accounts)
u - shows uptime
"""
stats_type = args['stats_type'][0].lower() # stats_type shouldn't be more than 1 char anyways
perms = ['stats.%s' % stats_type]
if stats_type == 'u':
perms.append('stats.uptime') # Consistency
try:
permissions.check_permissions(irc, source, perms)
except utils.NotAuthorizedError as e:
# Note, no irc.error() because this is not a command, but a handler
irc.msg(source, 'Error: %s' % e, notice=True)
return
log.info('(%s) /STATS %s requested by %s', irc.name, stats_type, irc.get_hostmask(source))
def _num(num, text):
irc.numeric(args['target'], num, source, text)
if stats_type == 'c':
# 213/RPL_STATSCLINE: "C <host> * <name> <port> <class>"
for netname, serverdata in sorted(conf.conf['servers'].items()):
# We're cramming as much as we can into the class field...
_num(213, "C %s * %s %s [%s:%s:%s]" %
(serverdata.get('ip', '0.0.0.0'),
netname,
serverdata.get('port', 0),
serverdata['protocol'],
'ssl' if serverdata.get('ssl') else 'no-ssl',
serverdata.get('encoding', 'utf-8'))
)
elif stats_type == 'o':
# 243/RPL_STATSOLINE: "O <hostmask> * <nick> [:<info>]"
# New style accounts only!
for accountname, accountdata in conf.conf['login'].get('accounts', {}).items():
networks = accountdata.get('networks', [])
if irc.name in networks or not networks:
hosts = ' '.join(accountdata.get('hosts', ['*@*']))
needoper = 'needoper' if accountdata.get('require_oper') else ''
_num(243, "O %s * %s :%s" % (hosts, accountname, needoper))
elif stats_type == 'u':
# 242/RPL_STATSUPTIME: ":Server Up <days> days <hours>:<minutes>:<seconds>"
_num(242, ':Server Up %s' % timediff(world.start_ts, int(time.time())))
else:
log.info('(%s) Unknown /STATS type %r requested by %s', irc.name, stats_type, irc.get_hostmask(source))
_num(219, "%s :End of /STATS report" % stats_type)
utils.add_hook(handle_stats, 'STATS')

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']

File diff suppressed because it is too large Load Diff

View File

@ -1,45 +1,50 @@
"""
hybrid.py: IRCD-Hybrid protocol module for PyLink.
"""
import time import time
import sys
import os
import re
from pylinkirc import utils 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.
class HybridProtocol(TS6Protocol): class HybridProtocol(TS6Protocol):
def __init__(self, irc): def __init__(self, *args, **kwargs):
# This protocol module inherits from the TS6 protocol. super().__init__(*args, **kwargs)
super().__init__(irc)
self.casemapping = 'ascii' self.casemapping = 'ascii'
self.caps = {}
self.hook_map = {'EOB': 'ENDBURST', 'TBURST': 'TOPIC', 'SJOIN': 'JOIN'} self.hook_map = {'EOB': 'ENDBURST', 'TBURST': 'TOPIC', 'SJOIN': 'JOIN'}
self.has_eob = False self.protocol_caps -= {'slash-in-hosts'}
def connect(self): def post_connect(self):
"""Initializes a connection to a server.""" """Initializes a connection to a server."""
ts = self.irc.start_ts ts = self.start_ts
self.has_eob = False f = self.send
f = self.irc.send
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L80 # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L80
# Note: according to hybrid source code, +p is paranoia, noknock,
# AND rfc1459-style private, though the last isn't documented.
cmodes = { cmodes = {
# TS6 generic modes: # TS6 generic modes:
'op': 'o', 'halfop': 'h', 'voice': 'v', 'ban': 'b', 'key': 'k', 'op': 'o', 'halfop': 'h', 'voice': 'v', 'ban': 'b', 'key': 'k',
'limit': 'l', 'moderated': 'm', 'noextmsg': 'n', 'limit': 'l', 'moderated': 'm', 'noextmsg': 'n',
'secret': 's', 'topiclock': 't', 'secret': 's', 'topiclock': 't', 'private': 'p',
# hybrid-specific modes: # hybrid-specific modes:
'blockcolor': 'c', 'inviteonly': 'i', 'noctcp': 'C', 'blockcolor': 'c', 'inviteonly': 'i', 'noctcp': 'C',
'regmoderated': 'M', 'operonly': 'O', 'regonly': 'R', 'regmoderated': 'M', 'operonly': 'O', 'regonly': 'R',
'sslonly': 'S', 'banexception': 'e', 'noknock': 'p', 'sslonly': 'S', 'banexception': 'e', 'noknock': 'p',
'registered': 'r', 'invex': 'I', 'registered': 'r', 'invex': 'I', 'paranoia': 'p',
'banexception': 'e',
# Now, map all the ABCD type modes: # Now, map all the ABCD type modes:
'*A': 'beI', '*B': 'k', '*C': 'l', '*D': 'cimnprstCMORS' '*A': 'beI', '*B': 'k', '*C': 'l', '*D': 'cimnprstCMORS'
} }
self.irc.cmodes = cmodes self.cmodes = cmodes
umodes = { umodes = {
'oper': 'o', 'invisible': 'i', 'wallops': 'w', 'locops': 'l', 'oper': 'o', 'invisible': 'i', 'wallops': 'w', 'locops': 'l',
@ -47,20 +52,21 @@ class HybridProtocol(TS6Protocol):
'callerid': 'g', 'admin': 'a', 'deaf_commonchan': 'G', 'hideoper': 'H', 'callerid': 'g', 'admin': 'a', 'deaf_commonchan': 'G', 'hideoper': 'H',
'webirc': 'W', 'sno_clientconnections': 'c', 'sno_badclientconnections': 'u', 'webirc': 'W', 'sno_clientconnections': 'c', 'sno_badclientconnections': 'u',
'sno_rejectedclients': 'j', 'sno_skill': 'k', 'sno_fullauthblock': 'f', 'sno_rejectedclients': 'j', 'sno_skill': 'k', 'sno_fullauthblock': 'f',
'sno_remoteclientconnections': 'F', 'sno_admin_requests': 'y', 'sno_debug': 'd', 'sno_remoteclientconnections': 'F', 'sno_stats': 'y', 'sno_debug': 'd',
'sno_nickchange': 'n', 'hideidle': 'q', 'registered': 'r', 'sno_nickchange': 'n', 'hideidle': 'q', 'registered': 'r',
'snomask': 's', 'ssl': 'S', 'sno_server_connects': 'e', 'sno_botfloods': 'b', 'snomask': 's', 'ssl': 'S', 'sno_serverconnects': 'e', 'sno_botfloods': 'b',
# Now, map all the ABCD type modes: # Now, map all the ABCD type modes:
'*A': '', '*B': '', '*C': '', '*D': 'DFGHRSWabcdefgijklnopqrsuwxy' '*A': '', '*B': '', '*C': '', '*D': 'DFGHRSWabcdefgijklnopqrsuwxy'
} }
self.irc.umodes = umodes self.umodes = umodes
self.extbans_matching.clear()
# halfops is mandatory on Hybrid # halfops is mandatory on Hybrid
self.irc.prefixmodes = {'o': '@', 'h': '%', 'v': '+'} self.prefixmodes = {'o': '@', 'h': '%', 'v': '+'}
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L55 # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L55
f('PASS %s TS 6 %s' % (self.irc.serverdata["sendpass"], self.irc.sid)) f('PASS %s TS 6 %s' % (self.serverdata["sendpass"], self.sid))
# We request the following capabilities (for hybrid): # We request the following capabilities (for hybrid):
@ -70,7 +76,7 @@ class HybridProtocol(TS6Protocol):
# CHW: Allow sending messages to @#channel and the like. # CHW: Allow sending messages to @#channel and the like.
# KNOCK: Support for /knock # KNOCK: Support for /knock
# SVS: Deal with extended NICK/UID messages that contain service IDs/stamps # SVS: Deal with extended NICK/UID messages that contain service IDs/stamps
# TBURST: Topic Burst command; we send this in topicBurst # TBURST: Topic Burst command; we send this in topic_burst
# DLN: DLINE command # DLN: DLINE command
# UNDLN: UNDLINE command # UNDLN: UNDLINE command
# KLN: KLINE command # KLN: KLINE command
@ -81,13 +87,13 @@ class HybridProtocol(TS6Protocol):
# EOB: Supports EOB (end of burst) command # EOB: Supports EOB (end of burst) command
f('CAPAB :TBURST DLN KNOCK UNDLN UNKLN KLN ENCAP IE EX HOPS CHW SVS CLUSTER EOB QS') f('CAPAB :TBURST DLN KNOCK UNDLN UNKLN KLN ENCAP IE EX HOPS CHW SVS CLUSTER EOB QS')
f('SERVER %s 0 :%s' % (self.irc.serverdata["hostname"], f('SERVER %s 0 :%s' % (self.serverdata["hostname"],
self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc'])) self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']))
# send endburst now # send endburst now
self.irc.send(':%s EOB' % (self.irc.sid,)) self.send(':%s EOB' % (self.sid,))
def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(),
server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None, server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None,
manipulatable=False): manipulatable=False):
""" """
@ -97,58 +103,82 @@ class HybridProtocol(TS6Protocol):
up to plugins to make sure they don't introduce anything invalid. up to plugins to make sure they don't introduce anything invalid.
""" """
server = server or self.irc.sid server = server or self.sid
if not self.irc.isInternalServer(server): if not self.is_internal_server(server):
raise ValueError('Server %r is not a PyLink server!' % server) raise ValueError('Server %r is not a PyLink server!' % server)
uid = self.uidgen[server].next_uid() uid = self.uidgen[server].next_uid()
ts = ts or int(time.time()) ts = ts or int(time.time())
realname = realname or self.irc.botdata['realname'] realname = realname or conf.conf['pylink']['realname']
realhost = realhost or host realhost = realhost or host
raw_modes = self.irc.joinModes(modes) raw_modes = self.join_modes(modes)
u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
u = self.users[uid] = User(self, nick, ts, uid, server, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip, manipulatable=manipulatable) realhost=realhost, ip=ip, manipulatable=manipulatable)
self.irc.applyModes(uid, modes)
self.irc.servers[server].users.add(uid) self.apply_modes(uid, modes)
self._send(server, "UID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} " self.servers[server].users.add(uid)
self._send_with_prefix(server, "UID {nick} {hopcount} {ts} {modes} {ident} {host} {ip} {uid} "
"* :{realname}".format(ts=ts, host=host, "* :{realname}".format(ts=ts, host=host,
nick=nick, ident=ident, uid=uid, nick=nick, ident=ident, uid=uid,
modes=raw_modes, ip=ip, realname=realname)) modes=raw_modes, ip=ip, realname=realname,
hopcount=self.servers[server].hopcount))
return u return u
def updateClient(self, target, field, text): def update_client(self, target, field, text):
"""Updates the ident, host, or realname of a PyLink client.""" """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()
ts = self.irc.users[target].ts ts = self.users[target].ts
if field == 'HOST': if field == 'HOST':
self.irc.users[target].host = text self.users[target].host = text
# On Hybrid, it appears that host changing is actually just forcing umode # 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(self.irc.sid, 'SVSMODE %s %s +x %s' % (target, ts, text)) self._send_with_prefix(self.sid, 'SVSMODE %s %s +x %s' % (target, ts, text))
else: 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 topicBurst(self, numeric, target, text): 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'):
"""
Sets a server ban.
"""
# source: user
# parameters: target server mask, duration, user mask, host mask, reason
assert not (user == host == '*'), "Refusing to set ridiculous ban on *@*"
if not source in self.users:
log.debug('(%s) Forcing KLINE sender to %s as TS6 does not allow KLINEs from servers', self.name, self.pseudoclient.uid)
source = self.pseudoclient.uid
self._send_with_prefix(source, 'KLINE * %s %s %s :%s' % (duration, user, host, reason))
def topic_burst(self, numeric, target, text):
"""Sends a topic change from a PyLink server. This is usually used on burst.""" """Sends a topic change from a PyLink server. This is usually used on burst."""
# <- :0UY TBURST 1459308205 #testchan 1459309379 dan!~d@localhost :sdf # <- :0UY TBURST 1459308205 #testchan 1459309379 dan!~d@localhost :sdf
if not self.irc.isInternalServer(numeric): if not self.is_internal_server(numeric):
raise LookupError('No such PyLink server exists.') raise LookupError('No such PyLink server exists.')
ts = self.irc.channels[target].ts ts = self._channels[target].ts
servername = self.irc.servers[numeric].name servername = self.servers[numeric].name
self._send(numeric, 'TBURST %s %s %s %s :%s' % (ts, target, int(time.time()), servername, text)) self._send_with_prefix(numeric, 'TBURST %s %s %s %s :%s' % (ts, target, int(time.time()), servername, text))
self.irc.channels[target].topic = text self._channels[target].topic = text
self.irc.channels[target].topicset = True self._channels[target].topicset = True
# command handlers # command handlers
@ -156,14 +186,11 @@ class HybridProtocol(TS6Protocol):
# We only get a list of keywords here. Hybrid obviously assumes that # We only get a list of keywords here. Hybrid obviously assumes that
# we know what modes it supports (indeed, this is a standard list). # we know what modes it supports (indeed, this is a standard list).
# <- CAPAB :UNDLN UNKLN KLN TBURST KNOCK ENCAP DLN IE EX HOPS CHW SVS CLUSTER EOB QS # <- CAPAB :UNDLN UNKLN KLN TBURST KNOCK ENCAP DLN IE EX HOPS CHW SVS CLUSTER EOB QS
self.irc.caps = caps = args[0].split() self._caps = caps = args[0].split()
for required_cap in ('EX', 'IE', 'SVS', 'EOB', 'HOPS', 'QS', 'TBURST', 'SVS'): for required_cap in ('SVS', 'EOB', 'HOPS', 'QS', 'TBURST'):
if required_cap not in caps: if required_cap not in caps:
raise ProtocolError('%s not found in TS6 capabilities list; this is required! (got %r)' % (required_cap, caps)) raise ProtocolError('%s not found in TS6 capabilities list; this is required! (got %r)' % (required_cap, caps))
log.debug('(%s) self.irc.connected set!', self.irc.name)
self.irc.connected.set()
def handle_uid(self, numeric, command, args): def handle_uid(self, numeric, command, args):
""" """
Handles Hybrid-style UID commands (user introduction). This is INCOMPATIBLE Handles Hybrid-style UID commands (user introduction). This is INCOMPATIBLE
@ -171,47 +198,55 @@ class HybridProtocol(TS6Protocol):
""" """
# <- :0UY UID dan 1 1451041551 +Facdeiklosuw ~ident localhost 127.0.0.1 0UYAAAAAB * :realname # <- :0UY UID dan 1 1451041551 +Facdeiklosuw ~ident localhost 127.0.0.1 0UYAAAAAB * :realname
nick = args[0] nick = args[0]
self._check_nick_collision(nick)
ts, modes, ident, host, ip, uid, account, realname = args[2:10] ts, modes, ident, host, ip, uid, account, realname = args[2:10]
ts = int(ts)
if account == '*': if account == '*':
account = None account = None
log.debug('(%s) handle_uid: got args nick=%s ts=%s uid=%s ident=%s ' log.debug('(%s) handle_uid: got args nick=%s ts=%s uid=%s ident=%s '
'host=%s realname=%s ip=%s', self.irc.name, nick, ts, uid, 'host=%s realname=%s ip=%s', self.name, nick, ts, uid,
ident, host, realname, ip) ident, host, realname, ip)
self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, host, ip) self.users[uid] = User(self, nick, ts, uid, numeric, ident, host, realname, host, ip)
parsedmodes = self.irc.parseModes(uid, [modes]) parsedmodes = self.parse_modes(uid, [modes])
log.debug('(%s) handle_uid: Applying modes %s for %s', self.irc.name, parsedmodes, uid) log.debug('(%s) handle_uid: Applying modes %s for %s', self.name, parsedmodes, uid)
self.irc.applyModes(uid, parsedmodes) self.apply_modes(uid, parsedmodes)
self.irc.servers[numeric].users.add(uid) self.servers[numeric].users.add(uid)
# 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.
if ('+o', None) in parsedmodes: self._check_oper_status_change(uid, parsedmodes)
self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': 'IRC_Operator'}])
# 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.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}]) self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}])
return {'uid': uid, 'ts': ts, 'nick': nick, 'realname': realname, 'host': host, 'ident': ident, 'ip': ip} 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."""
# <- :0UY TBURST 1459308205 #testchan 1459309379 dan!~d@localhost :sdf # <- :0UY TBURST 1459308205 #testchan 1459309379 dan!~d@localhost :sdf
channel = self.irc.toLower(args[1]) channel = args[1]
ts = args[2] ts = args[2]
setter = args[3] setter = args[3]
topic = args[-1] topic = args[-1]
self.irc.channels[channel].topic = topic self._channels[channel].topic = topic
self.irc.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}
def handle_eob(self, numeric, command, args): def handle_eob(self, numeric, command, args):
log.debug('(%s) end of burst received', self.irc.name) """EOB (end-of-burst) handler."""
if not self.has_eob: # Only call ENDBURST hooks if we haven't already. log.debug('(%s) end of burst received from %s', self.name, numeric)
return {} if not self.servers[numeric].has_eob:
# Don't fight with TS6's generic PING-as-EOB
self.servers[numeric].has_eob = True
self.has_eob = True if numeric == self.uplink:
self.connected.set()
return {}
def handle_svsmode(self, numeric, command, args): def handle_svsmode(self, numeric, command, args):
""" """
@ -222,14 +257,14 @@ class HybridProtocol(TS6Protocol):
target = args[0] target = args[0]
ts = args[1] ts = args[1]
modes = args[2:] modes = args[2:]
parsedmodes = self.irc.parseModes(target, modes) parsedmodes = self.parse_modes(target, modes)
for modepair in parsedmodes: for modepair in parsedmodes:
if modepair[0] == '+d': if modepair[0] == '+d':
# 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:
@ -242,7 +277,7 @@ class HybridProtocol(TS6Protocol):
# Send the login hook, and remove this mode from the mode # Send the login hook, and remove this mode from the mode
# list, as it shouldn't be parsed literally. # list, as it shouldn't be parsed literally.
self.irc.callHooks([target, 'CLIENT_SERVICES_LOGIN', {'text': account}]) self.call_hooks([target, 'CLIENT_SERVICES_LOGIN', {'text': account}])
parsedmodes.remove(modepair) parsedmodes.remove(modepair)
elif modepair[0] == '+x': elif modepair[0] == '+x':
@ -251,16 +286,16 @@ class HybridProtocol(TS6Protocol):
# to some.host, for example. # to some.host, for example.
host = args[-1] host = args[-1]
self.irc.users[target].host = host self.users[target].host = host
# Propagate the hostmask change as a hook. # Propagate the hostmask change as a hook.
self.irc.callHooks([numeric, 'CHGHOST', self.call_hooks([numeric, 'CHGHOST',
{'target': target, 'newhost': host}]) {'target': target, 'newhost': host}])
parsedmodes.remove(modepair) parsedmodes.remove(modepair)
if parsedmodes: if parsedmodes:
self.irc.applyModes(target, parsedmodes) self.apply_modes(target, parsedmodes)
return {'target': target, 'modes': parsedmodes} return {'target': target, 'modes': parsedmodes}

File diff suppressed because it is too large Load Diff

View File

@ -2,63 +2,710 @@
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.
""" """
from pylinkirc.classes import Protocol import re
import time
from pylinkirc import conf
from pylinkirc.classes import IRCNetwork, ProtocolError
from pylinkirc.log import log from pylinkirc.log import log
class IRCS2SProtocol(Protocol): __all__ = ['UIDGenerator', 'IRCCommonProtocol', 'IRCS2SProtocol']
def handle_kill(self, source, command, args): class UIDGenerator():
"""Handles incoming KILLs.""" """
killed = args[0] Generate UIDs for IRC S2S.
# Depending on whether the IRCd sends explicit QUIT messages for """
# killed clients, the user may or may not have automatically been
# removed from our user list.
# If not, we have to assume that KILL = QUIT and remove them
# ourselves.
data = self.irc.users.get(killed)
if data:
self.removeClient(killed)
# TS6-style kills look something like this: def __init__(self, uidchars, length, sid):
# <- :GL KILL 38QAAAAAA :hidden-1C620195!GL (test) self.uidchars = uidchars # corpus of characters to choose from
# What we actually want is to format a pretty kill message, in the form self.length = length # desired length of uid part, padded with uidchars[0]
# "Killed (killername (reason))". self.sid = str(sid) # server id (prefixed to every result)
self.counter = 0
try: def next_uid(self):
# Get the nick or server name of the caller. """
killer = self.irc.getFriendlyName(source) Returns the next unused UID for the server.
except KeyError: """
# Killer was... neither? We must have aliens or something. Fallback uid = ''
# to the given "UID". num = self.counter
killer = source 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
# Get the reason, which is enclosed in brackets. self.counter += 1
reason = ' '.join(args[1].split(" ")[1:]) uid = uid.rjust(self.length, self.uidchars[0])
return self.sid + uid
killmsg = "Killed (%s %s)" % (killer, reason) class IRCCommonProtocol(IRCNetwork):
return {'target': killed, 'text': killmsg, 'userdata': data} COMMON_PREFIXMODES = [('h', 'halfop'), ('a', 'admin'), ('q', 'owner'), ('y', 'owner')]
def handle_squit(self, numeric, command, args): def __init__(self, *args, **kwargs):
"""Handles incoming SQUITs.""" super().__init__(*args, **kwargs)
return self._squit(numeric, command, args)
def handle_away(self, numeric, command, args): self._caps = {}
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):
self._caps.clear()
def validate_server_conf(self):
"""Validates that the server block given contains the required keys."""
for k in self.conf_keys:
log.debug('(%s) Checking presence of conf key %r', self.name, k)
conf.validate(k in self.serverdata,
"Missing option %r in server block for network %s."
% (k, self.name))
port = self.serverdata['port']
conf.validate(isinstance(port, int) and 0 < port < 65535,
"Invalid port %r for network %s"
% (port, self.name))
@staticmethod
def parse_args(args):
"""
Parses a string or list of of RFC1459-style arguments, where ":" may
be used for multi-word arguments that last until the end of a line.
"""
if isinstance(args, str):
args = args.split(' ')
real_args = []
for idx, arg in enumerate(args):
if arg.startswith(':') and idx != 0:
# ":" is used to begin multi-word arguments that last until the end of the message.
# Use list splicing here to join them into one argument, and then add it to our list of args.
joined_arg = ' '.join(args[idx:])[1:] # Cut off the leading : as well
real_args.append(joined_arg)
break
elif arg.strip(' '): # Skip empty args that aren't part of the multi-word arg
real_args.append(arg)
return real_args
@classmethod
def parse_prefixed_args(cls, args):
"""Similar to parse_args(), but stripping leading colons from the first argument
of a line (usually the sender field)."""
args = cls.parse_args(args)
args[0] = args[0].split(':', 1)[1]
return args
@staticmethod
def parse_isupport(args, fallback=''):
"""
Parses a string of capabilities in the 005 / RPL_ISUPPORT format.
"""
if isinstance(args, str):
args = args.split(' ')
caps = {}
for cap in args:
try:
# Try to split it as a KEY=VALUE pair.
key, value = cap.split('=', 1)
except ValueError:
key = cap
value = fallback
caps[key] = value
return caps
@staticmethod
def parse_isupport_prefixes(args):
"""
Separates prefixes field like "(qaohv)~&@%+" into a dict mapping mode characters to mode
prefixes.
"""
prefixsearch = re.search(r'\(([A-Za-z]+)\)(.*)', args)
return dict(zip(prefixsearch.group(1), prefixsearch.group(2)))
@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):
"""Handles incoming AWAY messages.""" """Handles incoming AWAY messages."""
# TS6: # TS6:
# <- :6ELAAAAAB AWAY :Auto-away # <- :6ELAAAAAB AWAY :Auto-away
# <- :6ELAAAAAB AWAY
# P10: # P10:
# <- ABAAA A :blah # <- ABAAA A :blah
# <- ABAAA A # <- ABAAA A
if source not in self.users:
return
try: try:
self.irc.users[numeric].away = text = args[0] self.users[source].away = text = args[0]
except IndexError: # User is unsetting away status except IndexError: # User is unsetting away status
self.irc.users[numeric].away = text = '' self.users[source].away = text = ''
return {'text': text} return {'text': text}
def handle_version(self, numeric, command, args): def handle_error(self, numeric, command, args):
"""Handles requests for the PyLink server version.""" """Handles ERROR messages - these mean that our uplink has disconnected us!"""
return {} # See coremods/handlers.py for how this hook is used raise ProtocolError('Received an ERROR, disconnecting!')
def handle_pong(self, source, command, args):
"""Handles incoming PONG commands."""
if source == self.uplink:
self.lastping = time.time()
def handle_005(self, source, command, args):
"""
Handles 005 / RPL_ISUPPORT. This is used by at least Clientbot and ngIRCd (for server negotiation).
"""
# ngIRCd:
# <- :ngircd.midnight.local 005 pylink-devel.int NETWORK=ngircd-test :is my network name
# <- :ngircd.midnight.local 005 pylink-devel.int RFC2812 IRCD=ngIRCd CHARSET=UTF-8 CASEMAPPING=ascii PREFIX=(qaohv)~&@%+ CHANTYPES=#&+ CHANMODES=beI,k,l,imMnOPQRstVz CHANLIMIT=#&+:10 :are supported on this server
# <- :ngircd.midnight.local 005 pylink-devel.int CHANNELLEN=50 NICKLEN=21 TOPICLEN=490 AWAYLEN=127 KICKLEN=400 MODES=5 MAXLIST=beI:50 EXCEPTS=e INVEX=I PENALTY :are supported on this server
# Regular clientbot, connecting to InspIRCd:
# <- :millennium.overdrivenetworks.com 005 ice AWAYLEN=200 CALLERID=g CASEMAPPING=rfc1459 CHANMODES=IXbegw,k,FJLfjl,ACKMNOPQRSTUcimnprstz CHANNELLEN=64 CHANTYPES=# CHARSET=ascii ELIST=MU ESILENCE EXCEPTS=e EXTBAN=,ACNOQRSTUcmprsuz FNC INVEX=I :are supported by this server
# <- :millennium.overdrivenetworks.com 005 ice KICKLEN=255 MAP MAXBANS=60 MAXCHANNELS=30 MAXPARA=32 MAXTARGETS=20 MODES=20 NAMESX NETWORK=OVERdrive-IRC NICKLEN=21 OVERRIDE PREFIX=(Yqaohv)*~&@%+ SILENCE=32 :are supported by this server
# <- :millennium.overdrivenetworks.com 005 ice SSL=[::]:6697 STARTTLS STATUSMSG=*~&@%+ TOPICLEN=307 UHNAMES USERIP VBANLIST WALLCHOPS WALLVOICES WATCH=32 :are supported by this server
if not self._use_builtin_005_handling:
log.warning("(%s) Got spurious 005 message from %s: %r", self.name, source, args)
return
newcaps = self.parse_isupport(args[1:-1])
self._caps.update(newcaps)
log.debug('(%s) handle_005: self._caps is %s', self.name, self._caps)
if 'CHANMODES' in newcaps:
self.cmodes['*A'], self.cmodes['*B'], self.cmodes['*C'], self.cmodes['*D'] = \
newcaps['CHANMODES'].split(',')
log.debug('(%s) handle_005: cmodes: %s', self.name, self.cmodes)
if 'USERMODES' in newcaps:
self.umodes['*A'], self.umodes['*B'], self.umodes['*C'], self.umodes['*D'] = \
newcaps['USERMODES'].split(',')
log.debug('(%s) handle_005: umodes: %s', self.name, self.umodes)
if 'CASEMAPPING' in newcaps:
self.casemapping = newcaps.get('CASEMAPPING', self.casemapping)
log.debug('(%s) handle_005: casemapping set to %s', self.name, self.casemapping)
if 'PREFIX' in newcaps:
self.prefixmodes = prefixmodes = self.parse_isupport_prefixes(newcaps['PREFIX'])
log.debug('(%s) handle_005: prefix modes set to %s', self.name, self.prefixmodes)
# Autodetect common prefix mode names.
for char, modename in self.COMMON_PREFIXMODES:
# Don't overwrite existing named mode definitions.
if char in self.prefixmodes and modename not in self.cmodes:
self.cmodes[modename] = char
log.debug('(%s) handle_005: autodetecting mode %s (%s) as %s', self.name,
char, self.prefixmodes[char], modename)
# https://defs.ircdocs.horse/defs/isupport.html
if 'EXCEPTS' in newcaps:
# Handle EXCEPTS=e or EXCEPTS fields
self.cmodes['banexception'] = newcaps.get('EXCEPTS') or 'e'
log.debug('(%s) handle_005: got cmode banexception=%r', self.name, self.cmodes['banexception'])
if 'INVEX' in newcaps:
# Handle INVEX=I, INVEX fields
self.cmodes['invex'] = newcaps.get('INVEX') or 'I'
log.debug('(%s) handle_005: got cmode invex=%r', self.name, self.cmodes['invex'])
if 'NICKLEN' in newcaps:
# Handle NICKLEN=number
assert newcaps['NICKLEN'], "Got NICKLEN tag with no content?"
self.maxnicklen = int(newcaps['NICKLEN'])
log.debug('(%s) handle_005: got %r for maxnicklen', self.name, self.maxnicklen)
if 'DEAF' in newcaps:
# Handle DEAF=D, DEAF fields
self.umodes['deaf'] = newcaps.get('DEAF') or 'D'
log.debug('(%s) handle_005: got umode deaf=%r', self.name, self.umodes['deaf'])
if 'CALLERID' in newcaps:
# Handle CALLERID=g, CALLERID fields
self.umodes['callerid'] = newcaps.get('CALLERID') or 'g'
log.debug('(%s) handle_005: got umode callerid=%r', self.name, self.umodes['callerid'])
if 'STATUSMSG' in newcaps:
# Note: This assumes that all available prefixes can be used in STATUSMSG too.
# Even though this isn't always true, I don't see the point in making things
# any more complicated.
self.protocol_caps |= {'has-statusmsg'}
def _send_with_prefix(self, source, msg, **kwargs):
"""Sends a RFC1459-style raw command from the given sender."""
self.send(':%s %s' % (self._expandPUID(source), msg), **kwargs)
class IRCS2SProtocol(IRCCommonProtocol):
COMMAND_TOKENS = {}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.protocol_caps |= {'can-spawn-clients', 'has-ts', 'can-host-relay',
'can-track-servers'}
# Alias
self.handle_squit = self._squit
def handle_events(self, data):
"""Event handler for RFC1459-like protocols.
This passes most commands to the various handle_ABCD() functions
elsewhere defined protocol modules, coersing various sender prefixes
from nicks and server names to UIDs and SIDs respectively,
whenever possible.
Commands sent without an explicit sender prefix will have them set to
the SID of the uplink server.
"""
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)
sender = args[0]
if sender.startswith(':'):
sender = sender[1:]
# If the sender isn't in numeric format, try to convert it automatically.
sender_sid = self._get_SID(sender)
sender_uid = self._get_UID(sender)
if sender_sid in self.servers:
sender = sender_sid
elif sender_uid in self.users:
sender = sender_uid
else:
# No sender prefix; treat as coming from uplink IRCd.
sender = self.uplink
args.insert(0, sender)
raw_command = args[1].upper()
args = args[2:]
log.debug('(%s) Found message sender as %s, raw_command=%r, args=%r', self.name, sender, raw_command, args)
# For P10, convert the command token into a regular command, if present.
command = self.COMMAND_TOKENS.get(raw_command, raw_command)
if command != raw_command:
log.debug('(%s) Translating token %s to command %s', self.name, raw_command, command)
if self.is_internal_client(sender) or self.is_internal_server(sender):
log.warning("(%s) Received command %s being routed the wrong way!", self.name, command)
return
if command == 'ENCAP':
# Special case for TS6 encapsulated commands (ENCAP), in forms like this:
# <- :00A ENCAP * SU 42XAAAAAC :jlu5
command = args[1]
args = args[2:]
log.debug("(%s) Rewriting incoming ENCAP to command %s (args: %s)", self.name, command, args)
try:
func = getattr(self, 'handle_'+command.lower())
except AttributeError: # Unhandled command
pass
else:
parsed_args = func(sender, command, args)
if parsed_args is not None:
if tags:
parsed_args['tags'] = tags # Add message tags to this hook payload.
return [sender, command, parsed_args]
def invite(self, source, target, channel):
"""Sends an INVITE from a PyLink client."""
if not self.is_internal_client(source):
raise LookupError('No such PyLink client exists.')
self._send_with_prefix(source, 'INVITE %s %s' % (self._expandPUID(target), channel))
def kick(self, numeric, channel, target, reason=None):
"""Sends kicks from a PyLink client/server."""
if (not self.is_internal_client(numeric)) and \
(not self.is_internal_server(numeric)):
raise LookupError('No such PyLink client/server exists.')
if not reason:
reason = 'No reason given'
# Mangle kick targets for IRCds that require it.
real_target = self._expandPUID(target)
self._send_with_prefix(numeric, 'KICK %s %s :%s' % (channel, real_target, reason))
# We can pretend the target left by its own will; all we really care about
# is that the target gets removed from the channel userlist, and calling
# handle_part() does that just fine.
self.handle_part(target, 'KICK', [channel])
def 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):
"""Sends raw numerics from a server to a remote client. This is used for WHOIS replies."""
# Mangle the target for IRCds that require it.
target = self._expandPUID(target)
self._send_with_prefix(source, '%s %s %s' % (numeric, target, text))
def part(self, client, channel, reason=None):
"""Sends a part from a PyLink client."""
if not self.is_internal_client(client):
log.error('(%s) Error trying to part %r from %r (no such client exists)', self.name, client, channel)
raise LookupError('No such PyLink client exists.')
msg = "PART %s" % channel
if reason:
msg += " :%s" % reason
self._send_with_prefix(client, msg)
self.handle_part(client, 'PART', [channel])
def _ping_uplink(self):
"""Sends a PING to the uplink.
This is mostly used by PyLink internals to check whether the remote link is up."""
if self.sid and self.connected.is_set():
self._send_with_prefix(self.sid, 'PING %s' % self._expandPUID(self.uplink))
def quit(self, numeric, reason):
"""Quits a PyLink client."""
if self.is_internal_client(numeric):
self._send_with_prefix(numeric, "QUIT :%s" % reason)
self._remove_client(numeric)
else:
raise LookupError("No such PyLink client exists.")
def message(self, numeric, target, text):
"""Sends a PRIVMSG from a PyLink client."""
if not self.is_internal_client(numeric):
raise LookupError('No such PyLink client exists.')
# Mangle message targets for IRCds that require it.
target = self._expandPUID(target)
self._send_with_prefix(numeric, 'PRIVMSG %s :%s' % (target, text))
def notice(self, numeric, target, text):
"""Sends a NOTICE from a PyLink client or server."""
if (not self.is_internal_client(numeric)) and \
(not self.is_internal_server(numeric)):
raise LookupError('No such PyLink client/server exists.')
# Mangle message targets for IRCds that require it.
target = self._expandPUID(target)
self._send_with_prefix(numeric, 'NOTICE %s :%s' % (target, text))
def squit(self, source, target, text='No reason given'):
"""SQUITs a PyLink server."""
# -> SQUIT 9PZ :blah, blah
log.debug('(%s) squit: source=%s, target=%s', self.name, source, target)
self._send_with_prefix(source, 'SQUIT %s :%s' % (self._expandPUID(target), text))
self.handle_squit(source, 'SQUIT', [target, text])
def topic(self, source, target, text):
"""Sends a TOPIC change from a PyLink client or server."""
if (not self.is_internal_client(source)) and (not self.is_internal_server(source)):
raise LookupError('No such PyLink client/server exists.')
self._send_with_prefix(source, 'TOPIC %s :%s' % (target, text))
self._channels[target].topic = text
self._channels[target].topicset = True
topic_burst = topic
def handle_invite(self, numeric, command, args):
"""Handles incoming INVITEs."""
# TS6:
# <- :70MAAAAAC INVITE 0ALAAAAAA #blah 12345
# P10:
# <- ABAAA I PyLink-devel #services 1460948992
# Note that the target is a nickname, not a numeric.
target = self._get_UID(args[0])
channel = args[1]
curtime = int(time.time())
try:
ts = int(args[2])
except IndexError:
ts = curtime
ts = ts or curtime # Treat 0 timestamps (e.g. inspircd) as the current time.
return {'target': target, 'channel': channel, 'ts': ts}
def handle_kick(self, source, command, args):
"""Handles incoming KICKs."""
# :70MAAAAAA KICK #test 70MAAAAAA :some reason
channel = args[0]
kicked = self._get_UID(args[1])
try:
reason = args[2]
except IndexError:
reason = ''
log.debug('(%s) Removing kick target %s from %s', self.name, kicked, channel)
self.handle_part(kicked, 'KICK', [channel, reason])
return {'channel': channel, 'target': kicked, 'text': reason}
def handle_kill(self, source, command, args):
"""Handles incoming KILLs."""
killed = self._get_UID(args[0])
# Some IRCds send explicit QUIT messages for their killed clients in addition to KILL,
# meaning that our target client may have been removed already. If this is the case,
# don't bother forwarding this message on.
# Generally, we only need to distinguish between KILL and QUIT if the target is
# one of our clients, in which case the above statement isn't really applicable.
if killed in self.users:
userdata = self._remove_client(killed)
else:
return
# TS6-style kills look something like this:
# <- :jlu5 KILL 38QAAAAAA :hidden-1C620195!jlu5 (test)
# What we actually want is to format a pretty kill message, in the form
# "Killed (killername (reason))".
if '!' in args[1].split(" ", 1)[0]:
try:
# Get the nick or server name of the caller.
killer = self.get_friendly_name(source)
except KeyError:
# Killer was... neither? We must have aliens or something. Fallback
# to the given "UID".
killer = source
# Get the reason, which is enclosed in brackets.
killmsg = ' '.join(args[1].split(" ")[1:])[1:-1]
if not killmsg:
log.warning('(%s) Failed to extract kill reason: %r', self.name, args)
killmsg = args[1]
else:
# We already have a preformatted kill, so just pass it on as is.
# XXX: this does create a convoluted kill string if we want to forward kills
# over relay.
# InspIRCd:
# <- :1MLAAAAA1 KILL 0ALAAAAAC :Killed (jlu5 (test))
# ngIRCd:
# <- :jlu5 KILL PyLink-devel :KILLed by jlu5: ?
killmsg = args[1]
return {'target': killed, 'text': killmsg, 'userdata': userdata}
def _check_cloak_change(self, uid): # Stub by default
return
def _check_umode_away_change(self, uid):
# Handle away status changes based on umode +a
awaymode = self.umodes.get('away')
if uid in self.users and awaymode:
u = self.users[uid]
old_away_status = u.away
# Check whether the user is marked away, and send a hook update only if the status has changed.
away_status = (awaymode, None) in u.modes
if away_status != bool(old_away_status):
# This sets a dummy away reason of "Away" because no actual text is provided.
self.call_hooks([uid, 'AWAY', {'text': 'Away' if away_status else ''}])
def _check_oper_status_change(self, uid, modes):
if uid in self.users:
u = self.users[uid]
if 'servprotect' in self.umodes and (self.umodes['servprotect'], None) in u.modes:
opertype = 'Network Service'
elif 'netadmin' in self.umodes and (self.umodes['netadmin'], None) in u.modes:
opertype = 'Network Administrator'
elif 'admin' in self.umodes and (self.umodes['admin'], None) in u.modes:
opertype = 'Server Administrator'
else:
opertype = 'IRC Operator'
if ('+o', None) in modes:
self.call_hooks([uid, 'CLIENT_OPERED', {'text': opertype}])
def handle_mode(self, source, command, args):
"""Handles mode changes."""
# InspIRCd:
# <- :70MAAAAAA MODE 70MAAAAAA -i+xc
# P10:
# <- ABAAA M jlu5 -w
# <- ABAAA M #test +v ABAAB 1460747615
# <- ABAAA OM #test +h ABAAA
target = self._get_UID(args[0])
if self.is_channel(target):
channeldata = self._channels[target].deepcopy()
else:
channeldata = None
modestrings = args[1:]
changedmodes = self.parse_modes(target, modestrings)
self.apply_modes(target, changedmodes)
if target in self.users:
# Target was a user. Check for any cloak and away status changes.
self._check_cloak_change(target)
self._check_umode_away_change(target)
self._check_oper_status_change(target, changedmodes)
return {'target': target, 'modes': changedmodes, 'channeldata': channeldata}
def handle_part(self, source, command, args):
"""Handles incoming PART commands."""
channels = args[0].split(',')
for channel in channels.copy():
if channel not in self._channels or source not in self._channels[channel].users:
# Ignore channels the user isn't on, and remove them from any hook payloads.
channels.remove(channel)
self._channels[channel].remove_user(source)
try:
self.users[source].channels.discard(channel)
except KeyError:
log.debug("(%s) handle_part: KeyError trying to remove %r from %r's channel list?", self.name, channel, source)
try:
reason = args[1]
except IndexError:
reason = ''
if channels:
return {'channels': channels, 'text': reason}
def handle_privmsg(self, source, command, args):
"""Handles incoming PRIVMSG/NOTICE."""
# TS6:
# <- :70MAAAAAA PRIVMSG #dev :afasfsa
# <- :70MAAAAAA NOTICE 0ALAAAAAA :afasfsa
# P10:
# <- ABAAA P AyAAA :privmsg text
# <- ABAAA O AyAAA :notice text
raw_target = args[0]
server_check = None
if '@' in raw_target and not self.is_channel(raw_target.lstrip(''.join(self.prefixmodes.values()))):
log.debug('(%s) Processing user@server message with target %s',
self.name, raw_target)
raw_target, server_check = raw_target.split('@', 1)
if not self.is_server_name(server_check):
log.warning('(%s) Got user@server message with invalid server '
'name %r (full target: %r)', self.name, server_check,
args[0])
return
target = self._get_UID(raw_target)
if server_check is not None:
not_found = False
if target not in self.users:
# Most IRCds don't check locally if the target nick actually exists.
# If it doesn't, send an error back.
not_found = True
else:
# I guess we can technically leave this up to the IRCd to do the right
# checks here, but maybe that ruins the point of this 'security feature'
# in the first place.
log.debug('(%s) Checking if target %s/%s exists on server %s',
self.name, target, raw_target, server_check)
sid = self._get_SID(server_check)
if not sid:
log.debug('(%s) Failed user@server server check: %s does not exist.',
self.name, server_check)
not_found = True
elif sid != self.get_server(target):
log.debug("(%s) Got user@server message for %s/%s, but they "
"aren't on the server %s/%s. (full target: %r)",
self.name, target, raw_target, server_check, sid,
args[0])
not_found = True
if not_found:
self.numeric(self.sid, 401, source, '%s :No such nick' %
args[0])
return
# Coerse =#channel from Charybdis op moderated +z to @#channel.
if target.startswith('='):
target = '@' + target[1:]
return {'target': target, 'text': args[1]}
handle_notice = handle_privmsg
def handle_quit(self, numeric, command, args):
"""Handles incoming QUIT commands."""
# TS6:
# <- :1SRAAGB4T QUIT :Quit: quit message goes here
# P10:
# <- ABAAB Q :Killed (jlu5_ (bangbang))
userdata = self._remove_client(numeric)
if userdata:
try:
reason = args[0]
except IndexError:
reason = ''
return {'text': reason, 'userdata': userdata}
def handle_stats(self, numeric, command, args):
"""Handles the IRC STATS command."""
# IRCds are mostly consistent with this syntax, with the caller being the source,
# the stats type as arg 0, and the target server (SID or hostname) as arg 1
# <- :42XAAAAAB STATS c :7PY
return {'stats_type': args[0], 'target': self._get_SID(args[1])}
def handle_topic(self, numeric, command, args):
"""Handles incoming TOPIC changes from clients."""
# <- :70MAAAAAA TOPIC #test :test
channel = args[0]
topic = args[1]
oldtopic = self._channels[channel].topic
self._channels[channel].topic = topic
self._channels[channel].topicset = True
return {'channel': channel, 'setter': numeric, 'text': topic,
'oldtopic': oldtopic}
def handle_time(self, numeric, command, args):
"""Handles incoming /TIME requests."""
return {'target': args[0]}
def handle_whois(self, numeric, command, args): def handle_whois(self, numeric, command, args):
"""Handles incoming WHOIS commands..""" """Handles incoming WHOIS commands.."""
@ -72,17 +719,8 @@ class IRCS2SProtocol(Protocol):
# WHOIS commands received are for us, since we don't host any real servers # WHOIS commands received are for us, since we don't host any real servers
# to route it to. # to route it to.
return {'target': self._getUid(args[-1])} return {'target': self._get_UID(args[-1])}
def handle_quit(self, numeric, command, args): def handle_version(self, numeric, command, args):
"""Handles incoming QUIT commands.""" """Handles requests for the PyLink server version."""
# TS6: return {} # See coremods/handlers.py for how this hook is used
# <- :1SRAAGB4T QUIT :Quit: quit message goes here
# P10:
# <- ABAAB Q :Killed (GL_ (bangbang))
self.removeClient(numeric)
return {'text': args[0]}
def handle_time(self, numeric, command, args):
"""Handles incoming /TIME requests."""
return {'target': args[0]}

File diff suppressed because it is too large Load Diff

569
protocols/ngircd.py Normal file
View File

@ -0,0 +1,569 @@
"""
ngircd.py: PyLink protocol module for ngIRCd.
"""
##
# Server protocol docs for ngIRCd can be found at:
# https://github.com/ngircd/ngircd/blob/master/doc/Protocol.txt
# and https://tools.ietf.org/html/rfc2813
##
import re
import time
from pylinkirc import __version__, conf, utils
from pylinkirc.classes import *
from pylinkirc.log import log
from pylinkirc.protocols.ircs2s_common import *
__all__ = ['NgIRCdProtocol']
class NgIRCdProtocol(IRCS2SProtocol):
def __init__(self, irc):
super().__init__(irc)
self.conf_keys -= {'sid', 'sidrange'}
self.casemapping = 'ascii' # This is the default; it's actually set on server negotiation
self.hook_map = {'NJOIN': 'JOIN'}
# Track whether we've received end-of-burst from the uplink.
self.has_eob = False
self._caps = {}
self._use_builtin_005_handling = True
# ngIRCd has no TS tracking.
self.protocol_caps.discard('has-ts')
# Slash in nicks is problematic; while it works for basic things like JOIN and messages,
# attempts to set user modes fail.
self.protocol_caps |= {'slash-in-hosts', 'underscore-in-hosts'}
### Commands
def post_connect(self):
self.send('PASS %s 0210-IRC+ PyLink|%s:CHLMoX' % (self.serverdata['sendpass'], __version__))
# Note: RFC 2813 mandates another server token value after the hopcount (1), but ngIRCd
# doesn't follow that behaviour per https://github.com/ngircd/ngircd/issues/224
self.send("SERVER %s 1 :%s" % (self.serverdata['hostname'],
self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']))
self._uidgen = PUIDGenerator('PUID')
# The first "SID" this generator should return is 2, because server token 1 is implied to be
# the main PyLink server. RFC2813 has no official definition of SIDs, but rather uses
# integer tokens in the SERVER and NICK (user introduction) commands to keep track of which
# user exists on which server. Why did they do it this way? Who knows!
self._sidgen = PUIDGenerator('PSID', start=1)
self.sid = self._sidgen.next_sid(prefix=self.serverdata['hostname'])
self._caps.clear()
self.cmodes.update({
'banexception': 'e',
'invex': 'I',
'noinvite': 'V',
'nokick': 'Q',
'nonick': 'N',
'operonly': 'O',
'permanent': 'P',
'registered': 'r',
'regmoderated': 'M',
'regonly': 'R',
'sslonly': 'z'
})
self.umodes.update({
'away': 'a',
'bot': 'B',
'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(),
server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator',
manipulatable=False):
"""
Spawns a new client with the given options.
Note: No nick collision / valid nickname checks are done here; it is
up to plugins to make sure they don't introduce anything invalid.
Note 2: IP and realhost are ignored because ngIRCd does not send them.
"""
server = server or self.sid
assert '@' in server, "Need PSID for spawn_client, not pure server name!"
if not self.is_internal_server(server):
raise ValueError('Server %r is not a PyLink server!' % server)
realname = realname or conf.conf['pylink']['realname']
uid = self._uidgen.next_uid(prefix=nick)
userobj = self.users[uid] = User(self, nick, ts or int(time.time()), uid, server,
ident=ident, host=host, realname=realname,
manipulatable=manipulatable, opertype=opertype,
realhost=host)
self.apply_modes(uid, modes)
self.servers[server].users.add(uid)
# Grab our server token; this is used instead of server name to denote where the client is.
server_token = server.rsplit('@')[-1]
# <- :ngircd.midnight.local NICK 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,
ident, host, server_token, self.join_modes(modes), realname))
return userobj
def spawn_server(self, name, sid=None, uplink=None, desc=None):
"""
Spawns a server off a PyLink server.
* desc (server description) defaults to the one in the config.
* uplink defaults to the main PyLink server.
* SID is set equal to the server name for ngIRCd, as server UIDs are not used.
"""
uplink = uplink or self.sid
assert uplink in self.servers, "Unknown uplink %r?" % uplink
name = name.lower()
sid = self._sidgen.next_sid(prefix=name)
desc = desc or self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']
if sid in self.servers:
raise ValueError('A server named %r already exists!' % sid)
if not self.is_internal_server(uplink):
raise ValueError('Server %r is not a PyLink server!' % uplink)
if not self.is_server_name(name):
raise ValueError('Invalid server name %r' % name)
# https://tools.ietf.org/html/rfc2813#section-4.1.2
# We need to store a server token to introduce clients on the right server. Since this is just
# a number, we can simply use the counter in our PSID generator for this.
server_token = sid.rsplit('@')[-1]
self.servers[sid] = Server(self, uplink, name, internal=True, desc=desc)
self._send_with_prefix(uplink, 'SERVER %s %s %s :%s' % (name, self.servers[sid].hopcount, server_token, desc))
return sid
def away(self, source, text):
"""Sends an AWAY message from a PyLink client. If the text is empty, away status is unset."""
if not self.is_internal_client(source):
raise LookupError('No such PyLink client exists.')
# Away status is denoted on ngIRCd with umode +a.
modes = self.users[source].modes
if text and (('a', None) not in modes):
# Set umode +a if it isn't already set
self.mode(source, source, [('+a', None)])
elif ('a', None) in modes:
# Ditto, only unset the mode if it *was* set.
self.mode(source, source, [('-a', None)])
self.users[source].away = text
def join(self, client, channel):
if not self.is_internal_client(client):
raise LookupError('No such PyLink client exists.')
self._send_with_prefix(client, "JOIN %s" % channel)
self._channels[channel].users.add(client)
self.users[client].channels.add(channel)
def kill(self, source, target, reason):
"""Sends a kill from a PyLink client/server."""
if (not self.is_internal_client(source)) and \
(not self.is_internal_server(source)):
raise LookupError('No such PyLink client/server exists.')
# Follow ngIRCd's formatting of the kill messages for the most part
self._send_with_prefix(source, 'KILL %s :KILLed by %s: %s' % (self._expandPUID(target),
self.get_friendly_name(source), reason))
# Implicitly remove our own client if one was the target.
if self.is_internal_client(target):
self._remove_client(target)
def knock(self, numeric, target, text):
raise NotImplementedError('KNOCK is not supported on ngIRCd.')
def mode(self, source, target, modes, ts=None):
"""Sends mode changes from a PyLink client/server. The TS option is not used on ngIRCd."""
if (not self.is_internal_client(source)) and \
(not self.is_internal_server(source)):
raise LookupError('No such PyLink client/server %r exists' % source)
self.apply_modes(target, modes)
modes = list(modes) # Work around TypeError in the expand PUID section
if self.is_channel(target):
msgprefix = ':%s MODE %s ' % (self._expandPUID(source), target)
bufsize = self.S2S_BUFSIZE - len(msgprefix)
# Expand PUIDs when sending outgoing prefix modes.
for idx, mode in enumerate(modes):
if mode[0][-1] in self.prefixmodes:
log.debug('(%s) mode: expanding PUID of mode %s', self.name, str(mode))
modes[idx] = (mode[0], self._expandPUID(mode[1]))
for modestr in self.wrap_modes(modes, bufsize, max_modes_per_msg=12):
self.send(msgprefix + modestr)
else:
joinedmodes = self.join_modes(modes)
self._send_with_prefix(source, 'MODE %s %s' % (self._expandPUID(target), joinedmodes))
def nick(self, source, newnick):
"""Changes the nick of a PyLink client."""
if not self.is_internal_client(source):
raise LookupError('No such PyLink client exists.')
self._send_with_prefix(source, 'NICK %s' % newnick)
self.users[source].nick = newnick
# Update the nick TS for consistency with other protocols (it isn't actually used in S2S)
self.users[source].ts = int(time.time())
def sjoin(self, server, channel, users, ts=None, modes=set()):
"""Sends an SJOIN for a group of users to a channel.
The sender should always be a Server ID (SID). TS is optional, and defaults
to the one we've stored in the channel state if not given.
<users> is a list of (prefix mode, UID) pairs:
Example uses:
sjoin('100', '#test', [('', 'user0@0'), ('o', user1@1'), ('v', 'someone@2')])
sjoin(self.sid, '#test', [('o', self.pseudoclient.uid)])
"""
server = server or self.sid
if not server:
raise LookupError('No such PyLink client exists.')
log.debug('(%s) sjoin: got %r for users', self.name, users)
njoin_prefix = ':%s NJOIN %s :' % (self._expandPUID(server), channel)
# Format the user list into strings such as @user1, +user2, user3, etc.
nicks_to_send = []
for userpair in users:
prefixes, uid = userpair
if uid not in self.users:
log.warning('(%s) Trying to NJOIN missing user %s?', self.name, uid)
continue
elif uid in self._channels[channel].users:
# Don't rejoin users already in the channel, this causes errors with ngIRCd.
continue
self._channels[channel].users.add(uid)
self.users[uid].channels.add(channel)
self.apply_modes(channel, (('+%s' % prefix, uid) for prefix in userpair[0]))
nicks_to_send.append(''.join(self.prefixmodes[modechar] for modechar in userpair[0]) + \
self._expandPUID(userpair[1]))
if nicks_to_send:
# Use 13 args max per line: this is equal to the max of 15 minus the command name and target channel.
for message in utils.wrap_arguments(njoin_prefix, nicks_to_send, self.S2S_BUFSIZE, separator=',', max_args_per_line=13):
self.send(message)
if modes:
# Burst modes separately if there are any.
log.debug("(%s) sjoin: bursting modes %r for channel %r now", self.name, modes, channel)
self.mode(server, channel, modes)
def set_server_ban(self, source, duration, user='*', host='*', reason='User banned'):
"""
Sets a server ban.
"""
# <- :jlu5 GLINE *!*@bad.user 3d :test
assert not (user == host == '*'), "Refusing to set ridiculous ban on *@*"
self._send_with_prefix(source, 'GLINE *!%s@%s %s :%s' % (user, host, duration, reason))
def update_client(self, target, field, text):
"""Updates the ident, host, or realname of any connected client."""
field = field.upper()
if field not in ('IDENT', 'HOST', 'REALNAME', 'GECOS'):
raise NotImplementedError("Changing field %r of a client is "
"unsupported by this protocol." % field)
real_target = self._expandPUID(target)
if field == 'IDENT':
self.users[target].ident = text
self._send_with_prefix(self.sid, 'METADATA %s user :%s' % (real_target, text))
if not self.is_internal_client(target):
# If the target wasn't one of our clients, send a hook payload for other plugins to listen to.
self.call_hooks([self.sid, 'CHGIDENT', {'target': target, 'newident': text}])
elif field == 'HOST':
self.users[target].host = text
if self.is_internal_client(target):
# For our own clients, replace the real host.
self._send_with_prefix(self.sid, 'METADATA %s host :%s' % (real_target, text))
else:
# For others, update the cloaked host and force a umode +x.
self._send_with_prefix(self.sid, 'METADATA %s cloakhost :%s' % (real_target, text))
if ('x', None) not in self.users[target].modes:
log.debug('(%s) Forcing umode +x on %r as part of cloak setting', self.name, target)
self.mode(self.sid, target, [('+x', None)])
self.call_hooks([self.sid, 'CHGHOST', {'target': target, 'newhost': text}])
elif field in ('REALNAME', 'GECOS'):
self.users[target].realname = text
self._send_with_prefix(self.sid, 'METADATA %s info :%s' % (real_target, text))
if not self.is_internal_client(target):
self.call_hooks([self.sid, 'CHGNAME', {'target': target, 'newgecos': text}])
### Handlers
def handle_376(self, source, command, args):
# 376 is used to denote end of server negotiation - we send our info back at this point.
# <- :ngircd.midnight.local 005 pylink-devel.int NETWORK=ngircd-test :is my network name
# <- :ngircd.midnight.local 005 pylink-devel.int RFC2812 IRCD=ngIRCd CHARSET=UTF-8 CASEMAPPING=ascii PREFIX=(qaohv)~&@%+ CHANTYPES=#&+ CHANMODES=beI,k,l,imMnOPQRstVz CHANLIMIT=#&+:10 :are supported on this server
# <- :ngircd.midnight.local 005 pylink-devel.int CHANNELLEN=50 NICKLEN=21 TOPICLEN=490 AWAYLEN=127 KICKLEN=400 MODES=5 MAXLIST=beI:50 EXCEPTS=e INVEX=I PENALTY :are supported on this server
def f(numeric, msg):
self._send_with_prefix(self.sid, '%s %s %s' % (numeric, self.uplink, msg))
f('005', 'NETWORK=%s :is my network name' % self.get_full_network_name())
f('005', 'RFC2812 IRCD=PyLink CHARSET=UTF-8 CASEMAPPING=%s PREFIX=%s CHANTYPES=# '
'CHANMODES=%s,%s,%s,%s :are supported on this server' % (self.casemapping, self._caps['PREFIX'],
self.cmodes['*A'], self.cmodes['*B'], self.cmodes['*C'], self.cmodes['*D']))
f('005', 'CHANNELLEN NICKLEN=%s EXCEPTS=E INVEX=I :are supported on this server' % self.maxnicklen)
# 376 (end of MOTD) marks the end of extended server negotiation per
# https://github.com/ngircd/ngircd/blob/master/doc/Protocol.txt#L103-L112
f('376', ":End of server negotiation, happy PyLink'ing!")
def handle_chaninfo(self, source, command, args):
# https://github.com/ngircd/ngircd/blob/722afc1b810cef74dbd2738d71866176fd974ec2/doc/Protocol.txt#L146-L159
# CHANINFO has 3 styles depending on the amount of information applicable to a channel:
# CHANINFO <channel> +<modes>
# CHANINFO <channel> +<modes> <topic>
# CHANINFO <channel> +<modes> <key> <limit> <topic>
# If there is no key, the key is "*". If there is no limit, the limit is "0".
channel = args[0]
# Get rid of +l and +k in the initial parsing; we handle that later by looking at the CHANINFO arguments
modes = self.parse_modes(channel, args[1].replace('l', '').replace('k', ''))
if len(args) >= 3:
topic = args[-1]
if topic:
log.debug('(%s) handle_chaninfo: setting topic for %s to %r', self.name, channel, topic)
self._channels[channel].topic = topic
self._channels[channel].topicset = True
if len(args) >= 5:
key = args[2]
limit = args[3]
if key != '*':
modes.append(('+k', key))
if limit != '0':
modes.append(('+l', limit))
self.apply_modes(channel, modes)
def handle_join(self, source, command, args):
# RFC 2813 is odd to say the least... https://tools.ietf.org/html/rfc2813#section-4.2.1
# Basically, we expect messages of the forms:
# <- :jlu5 JOIN #test\x07o
# <- :jlu5 JOIN #moretest
for chanpair in args[0].split(','):
# Normalize channel case.
try:
channel, status = chanpair.split('\x07', 1)
if status in 'ov':
self.apply_modes(channel, [('+' + status, source)])
except ValueError:
channel = chanpair
c = self._channels[channel]
self.users[source].channels.add(channel)
self._channels[channel].users.add(source)
# Call hooks manually, because one JOIN command have multiple channels.
self.call_hooks([source, command, {'channel': channel, 'users': [source], 'modes': c.modes}])
def handle_kill(self, source, command, args):
"""Handles incoming KILLs."""
# ngIRCd sends QUIT after KILL for its own clients, so we shouldn't process this by itself
# unless we're the target.
killed = self._get_UID(args[0])
if self.is_internal_client(killed):
return super().handle_kill(source, command, args)
else:
log.debug("(%s) Ignoring KILL to %r as it isn't meant for us; we should see a QUIT soon",
self.name, killed)
def _check_cloak_change(self, target):
u = self.users[target]
old_host = u.host
if ('x', None) in u.modes and u.cloaked_host:
u.host = u.cloaked_host
elif u.realhost:
u.host = u.realhost
# Something changed, so send a CHGHOST hook
if old_host != u.host:
self.call_hooks([target, 'CHGHOST', {'target': target, 'newhost': u.host}])
def handle_metadata(self, source, command, args):
"""Handles various user metadata for ngIRCd (cloaked host, account name, etc.)"""
# <- :ngircd.midnight.local METADATA jlu5 cloakhost :hidden-3a2a739e.ngircd.midnight.local
target = self._get_UID(args[0])
if target not in self.users:
log.warning("(%s) Ignoring METADATA to missing user %r?", self.name, target)
return
datatype = args[1]
u = self.users[target]
if datatype == 'cloakhost': # Set cloaked host
u.cloaked_host = args[-1]
self._check_cloak_change(target)
elif datatype == 'host': # Host changing. This actually sets the "real host" that ngIRCd stores
u.realhost = args[-1]
self._check_cloak_change(target)
elif datatype == 'user': # Ident changing
u.ident = args[-1]
self.call_hooks([target, 'CHGIDENT', {'target': target, 'newident': args[-1]}])
elif datatype == 'info': # Realname changing
u.realname = args[-1]
self.call_hooks([target, 'CHGNAME', {'target': target, 'newgecos': args[-1]}])
elif datatype == 'accountname': # Services account
self.call_hooks([target, 'CLIENT_SERVICES_LOGIN', {'text': args[-1]}])
def handle_nick(self, source, command, args):
"""
Handles the NICK command, used for server introductions and nick changes.
"""
if len(args) >= 2:
# User introduction:
# <- :ngircd.midnight.local NICK jlu5 1 ~jlu5 localhost 1 +io :realname
nick = args[0]
assert source in self.servers, "Server %r tried to introduce nick %r but isn't in the servers index?" % (source, nick)
self._check_nick_collision(nick)
ident = args[2]
host = args[3]
uid = self._uidgen.next_uid(prefix=nick)
realname = args[-1]
ts = int(time.time())
self.users[uid] = User(self, nick, ts, uid, source, ident=ident, host=host,
realname=realname, realhost=host)
parsedmodes = self.parse_modes(uid, [args[5]])
self.apply_modes(uid, parsedmodes)
# Add the nick to the list of users on its server; this is used for SQUIT tracking
self.servers[source].users.add(uid)
# Check away status and cloaked host changes
self._check_umode_away_change(uid)
self._check_cloak_change(uid)
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': host, 'host': host, 'ident': ident,
'parse_as': 'UID', 'ip': '0.0.0.0'}
else:
# Nick changes:
# <- :jlu5 NICK :jlu5_
oldnick = self.users[source].nick
newnick = self.users[source].nick = args[0]
return {'newnick': newnick, 'oldnick': oldnick}
def handle_njoin(self, source, command, args):
# <- :ngircd.midnight.local NJOIN #test :tester,@%jlu5
channel = args[0]
chandata = self._channels[channel].deepcopy()
namelist = []
# Reverse the modechar->modeprefix mapping for quicker lookup
prefixchars = {v: k for k, v in self.prefixmodes.items()}
for userpair in args[1].split(','):
# Some regex magic to split the prefix from the nick.
r = re.search(r'([%s]*)(.*)' % ''.join(self.prefixmodes.values()), userpair)
user = self._get_UID(r.group(2))
modeprefix = r.group(1)
if modeprefix:
modes = {('+%s' % prefixchars[mode], user) for mode in modeprefix}
self.apply_modes(channel, modes)
namelist.append(user)
# Final bits of state tracking. (I hate having to do this everywhere...)
self.users[user].channels.add(channel)
self._channels[channel].users.add(user)
return {'channel': channel, 'users': namelist, 'modes': [], 'channeldata': chandata}
def handle_pass(self, source, command, args):
"""
Handles phase one of the ngIRCd login process (password auth and version info).
"""
# PASS is step one of server introduction, and is used to send the server info and password.
# <- :ngircd.midnight.local PASS xyzpassword 0210-IRC+ ngIRCd|24~3-gbc728f92:CHLMSXZ PZ
recvpass = args[0]
if recvpass != self.serverdata['recvpass']:
raise ProtocolError("RECVPASS from uplink does not match configuration!")
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):
"""
Handles incoming PINGs (and implicit end of burst).
"""
self._send_with_prefix(self.sid, 'PONG %s :%s' % (self._expandPUID(self.sid), args[-1]), queue=False)
if not self.servers[source].has_eob:
# Treat the first PING we receive as end of burst.
self.servers[source].has_eob = True
if source == self.uplink:
self.connected.set()
# Return the endburst hook.
return {'parse_as': 'ENDBURST'}
def handle_server(self, source, command, args):
"""
Handles the SERVER command.
"""
# <- :ngircd.midnight.local SERVER ngircd.midnight.local 1 :ngIRCd dev server
servername = args[0].lower()
serverdesc = args[-1]
# The uplink should be set to None for the uplink; otherwise, set it equal to the sender server.
self.servers[servername] = Server(self, source if source != servername else None, servername, desc=serverdesc)
if self.uplink is None:
self.uplink = servername
log.debug('(%s) Got %s as uplink', self.name, servername)
else:
# Only send the SERVER hook if this isn't the initial connection.
return {'name': servername, 'sid': None, 'text': serverdesc}
Class = NgIRCdProtocol

1356
protocols/p10.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,29 +2,47 @@
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 sys
import os
import re import re
import time
from pylinkirc import utils 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):
def __init__(self, irc):
super().__init__(irc) SUPPORTED_IRCDS = ('charybdis', 'elemental', 'chatircd', 'ratbox')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._ircd = self.serverdata.get('ircd', 'elemental' if self.serverdata.get('use_elemental_modes')
else 'charybdis')
self._ircd = self._ircd.lower()
if self._ircd not in self.SUPPORTED_IRCDS:
log.warning("(%s) Unsupported IRCd %r; falling back to 'charybdis' instead", self.name, self._ircd)
self._ircd = 'charybdis'
self._can_chghost = False
if self._ircd in ('charybdis', 'elemental', 'chatircd'):
# Charybdis and derivatives allow slashes in hosts. Ratbox does not.
self.protocol_caps |= {'slash-in-hosts'}
self._can_chghost = True
self.casemapping = 'rfc1459' self.casemapping = 'rfc1459'
self.hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE', self.hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE',
'EUID': 'UID', 'RSFNC': 'SVSNICK', 'ETB': 'TOPIC'} 'EUID': 'UID', 'RSFNC': 'SVSNICK', 'ETB': 'TOPIC',
# ENCAP LOGIN is used on burst for EUID-less servers
'LOGIN': 'CLIENT_SERVICES_LOGIN'}
# Track whether we've received end-of-burst from the uplink. self.required_caps = {'TB', 'ENCAP', 'QS', 'CHW'}
self.has_eob = False
### OUTGOING COMMANDS ### OUTGOING COMMANDS
def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(),
server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator', server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator',
manipulatable=False): manipulatable=False):
""" """
@ -33,9 +51,9 @@ class TS6Protocol(TS6BaseProtocol):
Note: No nick collision / valid nickname checks are done here; it is Note: No nick collision / valid nickname checks are done here; it is
up to plugins to make sure they don't introduce anything invalid. up to plugins to make sure they don't introduce anything invalid.
""" """
server = server or self.irc.sid server = server or self.sid
if not self.irc.isInternalServer(server): if not self.is_internal_server(server):
raise ValueError('Server %r is not a PyLink server!' % server) raise ValueError('Server %r is not a PyLink server!' % server)
uid = self.uidgen[server].next_uid() uid = self.uidgen[server].next_uid()
@ -44,34 +62,69 @@ class TS6Protocol(TS6BaseProtocol):
# parameters: nickname, hopcount, nickTS, umodes, username, # parameters: nickname, hopcount, nickTS, umodes, username,
# visible hostname, IP address, UID, real hostname, account name, gecos # visible hostname, IP address, UID, real hostname, account name, gecos
ts = ts or int(time.time()) ts = ts or int(time.time())
realname = realname or self.irc.botdata['realname'] realname = realname or conf.conf['pylink']['realname']
realhost = realhost or host raw_modes = self.join_modes(modes)
raw_modes = self.irc.joinModes(modes) u = self.users[uid] = User(self, nick, ts, uid, server, ident=ident, host=host,
u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, realname=realname, realhost=realhost or host, ip=ip,
realhost=realhost, ip=ip, manipulatable=manipulatable, opertype=opertype) manipulatable=manipulatable, opertype=opertype)
self.irc.applyModes(uid, modes) self.apply_modes(uid, modes)
self.irc.servers[server].users.add(uid) self.servers[server].users.add(uid)
self._send(server, "EUID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} " if 'EUID' in self._caps:
"{realhost} * :{realname}".format(ts=ts, host=host, # charybdis-style EUID
nick=nick, ident=ident, uid=uid, self._send_with_prefix(server, "EUID {nick} {hopcount} {ts} {modes} {ident} {host} {ip} {uid} "
modes=raw_modes, ip=ip, realname=realname, "{realhost} * :{realname}".format(ts=ts, host=host,
realhost=realhost)) nick=nick, ident=ident, uid=uid,
modes=raw_modes, ip=ip, realname=realname,
realhost=realhost or host,
hopcount=self.servers[server].hopcount))
else:
# Basic ratbox UID
self._send_with_prefix(server, "UID {nick} {hopcount} {ts} {modes} {ident} {host} {ip} {uid} "
":{realname}".format(ts=ts, host=host,
nick=nick, ident=ident, uid=uid,
modes=raw_modes, ip=ip, realname=realname,
hopcount=self.servers[server].hopcount))
if realhost:
# If real host is specified, send it using ENCAP REALHOST
self._send_with_prefix(uid, "ENCAP * REALHOST %s" % realhost)
return u 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."""
channel = self.irc.toLower(channel)
# JOIN: # JOIN:
# parameters: channelTS, channel, '+' (a plus sign) # parameters: channelTS, channel, '+' (a plus sign)
if not self.irc.isInternalClient(client): if not self.is_internal_client(client):
log.error('(%s) Error trying to join %r to %r (no such client exists)', self.irc.name, client, channel) log.error('(%s) Error trying to join %r to %r (no such client exists)', self.name, client, channel)
raise LookupError('No such PyLink client exists.') raise LookupError('No such PyLink client exists.')
self._send(client, "JOIN {ts} {channel} +".format(ts=self.irc.channels[channel].ts, channel=channel)) self._send_with_prefix(client, "JOIN {ts} {channel} +".format(ts=self._channels[channel].ts, channel=channel))
self.irc.channels[channel].users.add(client) self._channels[channel].users.add(client)
self.irc.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.
@ -82,7 +135,7 @@ class TS6Protocol(TS6BaseProtocol):
Example uses: Example uses:
sjoin('100', '#test', [('', '100AAABBC'), ('o', 100AAABBB'), ('v', '100AAADDD')]) sjoin('100', '#test', [('', '100AAABBC'), ('o', 100AAABBB'), ('v', '100AAADDD')])
sjoin(self.irc.sid, '#test', [('o', self.irc.pseudoclient.uid)]) sjoin(self.sid, '#test', [('o', self.pseudoclient.uid)])
""" """
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L821 # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L821
# parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist # parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist
@ -93,30 +146,33 @@ class TS6Protocol(TS6BaseProtocol):
# their status ('@+', '@', '+' or ''), for example: # their status ('@+', '@', '+' or ''), for example:
# '@+1JJAAAAAB +2JJAAAA4C 1JJAAAADS'. All users must be behind the source server # '@+1JJAAAAAB +2JJAAAA4C 1JJAAAADS'. All users must be behind the source server
# so it is not possible to use this message to force users to join a channel. # so it is not possible to use this message to force users to join a channel.
channel = self.irc.toLower(channel) server = server or self.sid
server = server or self.irc.sid
assert users, "sjoin: No users sent?" assert users, "sjoin: No users sent?"
log.debug('(%s) sjoin: got %r for users', self.irc.name, users) log.debug('(%s) sjoin: got %r for users', self.name, users)
if not server: if not server:
raise LookupError('No such PyLink client exists.') raise LookupError('No such PyLink client exists.')
modes = set(modes or self.irc.channels[channel].modes) modes = set(modes or self._channels[channel].modes)
orig_ts = self.irc.channels[channel].ts orig_ts = self._channels[channel].ts
ts = ts or orig_ts ts = ts or orig_ts
# Get all the ban modes in a separate list. These are bursted using a separate BMASK # Get all the ban modes in a separate list. These are bursted using a separate BMASK
# command. # command.
banmodes = {k: set() for k in self.irc.cmodes['*A']} banmodes = {k: [] for k in self.cmodes['*A']}
regularmodes = [] regularmodes = []
log.debug('(%s) Unfiltered SJOIN modes: %s', self.irc.name, modes) log.debug('(%s) Unfiltered SJOIN modes: %s', self.name, modes)
for mode in modes: for mode in modes:
modechar = mode[0][-1] modechar = mode[0][-1]
if modechar in self.irc.cmodes['*A']: if modechar in self.cmodes['*A']:
# Mode character is one of 'beIq' # Mode character is one of 'beIq'
banmodes[modechar].add(mode[1]) if (modechar, mode[1]) in self._channels[channel].modes:
# Don't reset modes that are already set.
continue
banmodes[modechar].append(mode[1])
else: else:
regularmodes.append(mode) regularmodes.append(mode)
log.debug('(%s) Filtered SJOIN modes to be regular modes: %s, banmodes: %s', self.irc.name, regularmodes, banmodes) log.debug('(%s) Filtered SJOIN modes to be regular modes: %s, banmodes: %s', self.name, regularmodes, banmodes)
changedmodes = modes changedmodes = modes
while users[:12]: while users[:12]:
@ -128,22 +184,22 @@ class TS6Protocol(TS6BaseProtocol):
prefixes, user = userpair prefixes, user = userpair
prefixchars = '' prefixchars = ''
for prefix in prefixes: for prefix in prefixes:
pr = self.irc.prefixmodes.get(prefix) pr = self.prefixmodes.get(prefix)
if pr: if pr:
prefixchars += pr prefixchars += pr
changedmodes.add(('+%s' % prefix, user)) changedmodes.add(('+%s' % prefix, user))
namelist.append(prefixchars+user) namelist.append(prefixchars+user)
uids.append(user) uids.append(user)
try: try:
self.irc.users[user].channels.add(channel) self.users[user].channels.add(channel)
except KeyError: # Not initialized yet? except KeyError: # Not initialized yet?
log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user) log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.name, channel, user)
users = users[12:] users = users[12:]
namelist = ' '.join(namelist) namelist = ' '.join(namelist)
self._send(server, "SJOIN {ts} {channel} {modes} :{users}".format( self._send_with_prefix(server, "SJOIN {ts} {channel} {modes} :{users}".format(
ts=ts, users=namelist, channel=channel, ts=ts, users=namelist, channel=channel,
modes=self.irc.joinModes(regularmodes))) modes=self.join_modes(regularmodes)))
self.irc.channels[channel].users.update(uids) self._channels[channel].users.update(uids)
# Now, burst bans. # Now, burst bans.
# <- :42X BMASK 1424222769 #dev b :*!test@*.isp.net *!badident@* # <- :42X BMASK 1424222769 #dev b :*!test@*.isp.net *!badident@*
@ -151,12 +207,12 @@ class TS6Protocol(TS6BaseProtocol):
# Max 15-3 = 12 bans per line to prevent cut off. (TS6 allows a max of 15 parameters per # Max 15-3 = 12 bans per line to prevent cut off. (TS6 allows a max of 15 parameters per
# line) # line)
if bans: if bans:
log.debug('(%s) sjoin: bursting mode %s with bans %s, ts:%s', self.irc.name, bmode, bans, ts) log.debug('(%s) sjoin: bursting mode %s with bans %s, ts:%s', self.name, bmode, bans, ts)
bans = list(bans) # Convert into list for splicing msgprefix = ':{sid} BMASK {ts} {channel} {bmode} :'.format(sid=server, ts=ts,
while bans[:12]: channel=channel, bmode=bmode)
self._send(server, "BMASK {ts} {channel} {bmode} :{bans}".format(ts=ts, # Actually, we cut off at 17 arguments/line, since the prefix and command name don't count.
channel=channel, bmode=bmode, bans=' '.join(bans[:12]))) for msg in utils.wrap_arguments(msgprefix, bans, self.S2S_BUFSIZE, max_args_per_line=17):
bans = bans[12:] self.send(msg)
self.updateTS(server, channel, ts, changedmodes) self.updateTS(server, channel, ts, changedmodes)
@ -165,155 +221,188 @@ class TS6Protocol(TS6BaseProtocol):
# c <- :0UYAAAAAA TMODE 0 #a +o 0T4AAAAAC # c <- :0UYAAAAAA TMODE 0 #a +o 0T4AAAAAC
# u <- :0UYAAAAAA MODE 0UYAAAAAA :-Facdefklnou # u <- :0UYAAAAAA MODE 0UYAAAAAA :-Facdefklnou
if (not self.irc.isInternalClient(numeric)) and \ if (not self.is_internal_client(numeric)) and \
(not self.irc.isInternalServer(numeric)): (not self.is_internal_server(numeric)):
raise LookupError('No such PyLink client/server exists.') raise LookupError('No such PyLink client/server exists.')
self.irc.applyModes(target, modes) self.apply_modes(target, modes)
modes = list(modes) modes = list(modes)
if utils.isChannel(target): if self.is_channel(target):
ts = ts or self.irc.channels[self.irc.toLower(target)].ts ts = ts or self._channels[target].ts
# TMODE: # TMODE:
# parameters: channelTS, channel, cmode changes, opt. cmode parameters... # parameters: channelTS, channel, cmode changes, opt. cmode parameters...
# On output, at most ten cmode parameters should be sent; if there are more, # On output, at most ten cmode parameters should be sent; if there are more,
# multiple TMODE messages should be sent. # multiple TMODE messages should be sent.
while modes[:10]: msgprefix = ':%s TMODE %s %s ' % (numeric, ts, target)
# Seriously, though. If you send more than 10 mode parameters in bufsize = self.S2S_BUFSIZE - len(msgprefix)
# a line, charybdis will silently REJECT the entire command!
joinedmodes = self.irc.joinModes([m for m in modes[:10] if m[0] not in self.irc.cmodes['*A']])
modes = modes[10:]
self._send(numeric, 'TMODE %s %s %s' % (ts, target, joinedmodes))
else:
joinedmodes = self.irc.joinModes(modes)
self._send(numeric, 'MODE %s %s' % (target, joinedmodes))
def topicBurst(self, numeric, target, text): for modestr in self.wrap_modes(modes, bufsize, max_modes_per_msg=10):
self.send(msgprefix + modestr)
else:
joinedmodes = self.join_modes(modes)
self._send_with_prefix(numeric, 'MODE %s %s' % (target, joinedmodes))
def topic_burst(self, numeric, 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.irc.isInternalServer(numeric): if not self.is_internal_server(numeric):
raise LookupError('No such PyLink server exists.') raise LookupError('No such PyLink server exists.')
# TB # TB
# capab: TB # capab: TB
# source: server # source: server
# propagation: broadcast # propagation: broadcast
# parameters: channel, topicTS, opt. topic setter, topic # parameters: channel, topicTS, opt. topic setter, topic
ts = self.irc.channels[target].ts ts = self._channels[target].ts
servername = self.irc.servers[numeric].name servername = self.servers[numeric].name
self._send(numeric, 'TB %s %s %s :%s' % (target, ts, servername, text)) self._send_with_prefix(numeric, 'TB %s %s %s :%s' % (target, ts, servername, text))
self.irc.channels[target].topic = text self._channels[target].topic = text
self.irc.channels[target].topicset = True self._channels[target].topicset = True
def invite(self, numeric, target, channel): def invite(self, numeric, target, channel):
"""Sends an INVITE from a PyLink client..""" """Sends an INVITE from a PyLink client.."""
if not self.irc.isInternalClient(numeric): if not self.is_internal_client(numeric):
raise LookupError('No such PyLink client exists.') raise LookupError('No such PyLink client exists.')
self._send(numeric, 'INVITE %s %s %s' % (target, channel, self.irc.channels[channel].ts)) self._send_with_prefix(numeric, 'INVITE %s %s %s' % (target, channel, self._channels[channel].ts))
def knock(self, numeric, target, text): def knock(self, numeric, target, text):
"""Sends a KNOCK from a PyLink client.""" """Sends a KNOCK from a PyLink client."""
if 'KNOCK' not in self.irc.caps: if 'KNOCK' not in self._caps:
log.debug('(%s) knock: Dropping KNOCK to %r since the IRCd ' log.debug('(%s) knock: Dropping KNOCK to %r since the IRCd '
'doesn\'t support it.', self.irc.name, target) 'doesn\'t support it.', self.name, target)
return return
if not self.irc.isInternalClient(numeric): if not self.is_internal_client(numeric):
raise LookupError('No such PyLink client exists.') raise LookupError('No such PyLink client exists.')
# No text value is supported here; drop it. # No text value is supported here; drop it.
self._send(numeric, 'KNOCK %s' % target) self._send_with_prefix(numeric, 'KNOCK %s' % target)
def updateClient(self, target, field, text): def set_server_ban(self, source, duration, user='*', host='*', reason='User banned'):
"""
Sets a server ban.
"""
# source: user
# parameters: target server mask, duration, user mask, host mask, reason
assert not (user == host == '*'), "Refusing to set ridiculous ban on *@*"
if not source in self.users:
log.debug('(%s) Forcing KLINE sender to %s as TS6 does not allow KLINEs from servers', self.name, self.pseudoclient.uid)
source = self.pseudoclient.uid
self._send_with_prefix(source, 'ENCAP * KLINE %s %s %s :%s' % (duration, user, host, reason))
def update_client(self, target, field, text):
"""Updates the hostname of any connected client.""" """Updates the hostname of any connected client."""
field = field.upper() field = field.upper()
if field == 'HOST': if field == 'HOST' and self._can_chghost:
self.irc.users[target].host = text self.users[target].host = text
self._send(self.irc.sid, 'CHGHOST %s :%s' % (target, text)) self._send_with_prefix(self.sid, 'CHGHOST %s :%s' % (target, text))
if not self.irc.isInternalClient(target): if not self.is_internal_client(target):
# If the target isn't one of our clients, send hook payload # If the target isn't one of our clients, send hook payload
# for other plugins to listen to. # for other plugins to listen to.
self.irc.callHooks([self.irc.sid, 'CHGHOST', self.call_hooks([self.sid, 'CHGHOST',
{'target': target, 'newhost': text}]) {'target': target, 'newhost': text}])
else: else:
raise NotImplementedError("Changing field %r of a client is " raise NotImplementedError("Changing field %r of a client is "
"unsupported by this protocol." % field) "unsupported by this IRCd." % field)
def ping(self, source=None, target=None):
"""Sends a PING to a target server. Periodic PINGs are sent to our uplink
automatically by the Irc() internals; plugins shouldn't have to use this."""
source = source or self.irc.sid
if source is None:
return
if target is not None:
self._send(source, 'PING %s %s' % (source, target))
else:
self._send(source, 'PING %s' % source)
### Core / handlers ### Core / handlers
def connect(self): def post_connect(self):
"""Initializes a connection to a server.""" """Initializes a connection to a server."""
ts = self.irc.start_ts ts = self.start_ts
self.has_eob = False
f = self.irc.send f = self.send
# Base TS6 mode set from ratbox.
self.cmodes.update({'sslonly': 'S', 'noknock': 'p',
'*A': 'beI',
'*B': 'k',
'*C': 'l',
'*D': 'imnpstrS'})
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L80 # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L80
chary_cmodes = { # TS6 generic modes (note that +p is noknock instead of private): if self._ircd in ('charybdis', 'elemental', 'chatircd'):
'op': 'o', 'voice': 'v', 'ban': 'b', 'key': 'k', 'limit': self.cmodes.update({
'l', 'moderated': 'm', 'noextmsg': 'n', 'noknock': 'p', 'quiet': 'q', 'redirect': 'f', 'freetarget': 'F',
'secret': 's', 'topiclock': 't', 'inviteonly': 'i', 'joinflood': 'j', 'largebanlist': 'L', 'permanent': 'P',
# charybdis-specific modes: 'noforwards': 'Q', 'stripcolor': 'c', 'allowinvite':
'quiet': 'q', 'redirect': 'f', 'freetarget': 'F', 'g', 'opmoderated': 'z', 'noctcp': 'C',
'joinflood': 'j', 'largebanlist': 'L', 'permanent': 'P', # charybdis modes provided by extensions
'noforwards': 'Q', 'stripcolor': 'c', 'allowinvite': 'operonly': 'O', 'adminonly': 'A', 'sslonly': 'S',
'g', 'opmoderated': 'z', 'noctcp': 'C', 'nonotice': 'T',
# charybdis-specific modes provided by EXTENSIONS '*A': 'beIq', '*B': 'k', '*C': 'lfj', '*D': 'mnprstFLPQcgzCOAST'
'operonly': 'O', 'adminonly': 'A', 'sslonly': 'S', })
'nonotice': 'T', self.umodes.update({
# Now, map all the ABCD type modes: 'deaf': 'D', 'servprotect': 'S', 'admin': 'a',
'*A': 'beIq', '*B': 'k', '*C': 'lfj', '*D': 'mnprstFLPQcgzCOAST'} 'invisible': 'i', 'oper': 'o', 'wallops': 'w',
'snomask': 's', 'noforward': 'Q', 'regdeaf': 'R',
'callerid': 'g', 'operwall': 'z', 'locops': 'l',
'cloak': 'x', 'override': 'p', 'ssl': 'Z',
'*A': '', '*B': '', '*C': '', '*D': 'DSaiowsQRgzlxpZ'
})
if self.irc.serverdata.get('use_owner'): # Charybdis extbans
chary_cmodes['owner'] = 'y' self.extbans_matching = {'ban_all_registered': '$a', 'ban_inchannel': '$c:', 'ban_account': '$a:',
self.irc.prefixmodes['y'] = '~' 'ban_all_opers': '$o', 'ban_realname': '$r:', 'ban_server': '$s:',
if self.irc.serverdata.get('use_admin'): 'ban_banshare': '$j:', 'ban_extgecos': '$x:', 'ban_all_ssl': '$z'}
chary_cmodes['admin'] = 'a' elif self._ircd == 'ratbox':
self.irc.prefixmodes['a'] = '!' self.umodes.update({
if self.irc.serverdata.get('use_halfop'): 'callerid': 'g', 'admin': 'a', 'sno_botfloods': 'b',
chary_cmodes['halfop'] = 'h' 'sno_clientconnections': 'c', 'sno_extclientconnections': 'C', 'sno_debug': 'd',
self.irc.prefixmodes['h'] = '%' 'sno_fullauthblock': 'f', 'sno_skill': 'k', 'locops': 'l', 'sno_rejectedclients': 'r',
'snomask': 's', 'sno_badclientconnections': 'u', 'sno_serverconnects': 'x',
'sno_stats': 'y', 'operwall': 'z', 'sno_operspy': 'Z', 'deaf': 'D', 'servprotect': 'S',
'*A': '', '*B': '', '*C': '', '*D': 'igoabcCdfklrsuwxyzZD'
})
self.irc.cmodes = chary_cmodes # TODO: make these more flexible...
if self.serverdata.get('use_owner'):
# Define supported user modes self.cmodes['owner'] = 'y'
chary_umodes = {'deaf': 'D', 'servprotect': 'S', 'admin': 'a', self.prefixmodes['y'] = '~'
'invisible': 'i', 'oper': 'o', 'wallops': 'w', if self.serverdata.get('use_admin'):
'snomask': 's', 'noforward': 'Q', 'regdeaf': 'R', self.cmodes['admin'] = 'a'
'callerid': 'g', 'operwall': 'z', 'locops': 'l', self.prefixmodes['a'] = '!' if self._ircd != 'chatircd' else '&'
'cloak': 'x', 'override': 'p', if self.serverdata.get('use_halfop'):
# Now, map all the ABCD type modes: self.cmodes['halfop'] = 'h'
'*A': '', '*B': '', '*C': '', '*D': 'DSaiowsQRgzlxp'} self.prefixmodes['h'] = '%'
self.irc.umodes = chary_umodes
# Toggles support of shadowircd/elemental-ircd specific channel modes: # Toggles support of shadowircd/elemental-ircd specific channel modes:
# +T (no notice), +u (hidden ban list), +E (no kicks), +J (blocks kickrejoin), # +T (no notice), +u (hidden ban list), +E (no kicks), +J (blocks kickrejoin),
# +K (no repeat messages), +d (no nick changes), and user modes: # +K (no repeat messages), +d (no nick changes), and user modes:
# +B (bot), +C (blocks CTCP), +D (deaf), +V (no invites), +I (hides channel list) # +B (bot), +C (blocks CTCP), +V (no invites), +I (hides channel list)
if self.irc.serverdata.get('use_elemental_modes'): if self._ircd == 'elemental':
elemental_cmodes = {'hiddenbans': 'u', 'nokick': 'E', elemental_cmodes = {'hiddenbans': 'u', 'nokick': 'E',
'kicknorejoin': 'J', 'repeat': 'K', 'nonick': 'd', 'kicknorejoin': 'J', 'repeat': 'K', 'nonick': 'd',
'blockcaps': 'G'} 'blockcaps': 'G'}
self.irc.cmodes.update(elemental_cmodes) self.cmodes.update(elemental_cmodes)
self.irc.cmodes['*D'] += ''.join(elemental_cmodes.values()) self.cmodes['*D'] += ''.join(elemental_cmodes.values())
elemental_umodes = {'noctcp': 'C', 'deaf': 'D', 'bot': 'B', 'noinvite': 'V', elemental_umodes = {'noctcp': 'C', 'bot': 'B', 'noinvite': 'V', 'hidechans': 'I'}
'hidechans': 'I'} self.umodes.update(elemental_umodes)
self.irc.umodes.update(elemental_umodes) self.umodes['*D'] += ''.join(elemental_umodes.values())
self.irc.umodes['*D'] += ''.join(elemental_umodes.values())
elif self._ircd == 'chatircd':
chatircd_cmodes = {'netadminonly': 'N'}
self.cmodes.update(chatircd_cmodes)
self.cmodes['*D'] += ''.join(chatircd_cmodes.values())
chatircd_umodes = {'netadmin': 'n', 'bot': 'B', 'sslonlymsg': 't'}
self.umodes.update(chatircd_umodes)
self.umodes['*D'] += ''.join(chatircd_umodes.values())
# Add definitions for all the inverted versions of the extbans.
if self.extbans_matching:
for k, v in self.extbans_matching.copy().items():
if k == 'ban_all_registered':
newk = 'ban_unregistered'
else:
newk = k.replace('_all_', '_').replace('ban_', 'ban_not_')
self.extbans_matching[newk] = '$~' + v[1:]
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L55 # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L55
f('PASS %s TS 6 %s' % (self.irc.serverdata["sendpass"], self.irc.sid)) f('PASS %s TS 6 %s' % (self.serverdata["sendpass"], self.sid))
# We request the following capabilities (for charybdis): # We request the following capabilities:
# QS: SQUIT doesn't send recursive quits for each users; required # QS: SQUIT doesn't send recursive quits for each users; required
# by charybdis (Source: https://github.com/grawity/irc-docs/blob/master/server/ts-capab.txt) # by charybdis (Source: https://github.com/grawity/irc-docs/blob/master/server/ts-capab.txt)
@ -324,20 +413,23 @@ class TS6Protocol(TS6BaseProtocol):
# KNOCK: support for /knock # KNOCK: support for /knock
# SAVE: support for SAVE (forces user to UID in nick collision) # SAVE: support for SAVE (forces user to UID in nick collision)
# SERVICES: adds mode +r (only registered users can join a channel) # SERVICES: adds mode +r (only registered users can join a channel)
# TB: topic burst command; we send this in topicBurst # TB: topic burst command; we send this in topic_burst
# EUID: extended UID command, which includes real hostname + account data info, # EUID: extended UID command, which includes real hostname + account data info,
# and allows sending CHGHOST without ENCAP. # and allows sending CHGHOST without ENCAP.
# RSFNC: states that we support RSFNC (forced nick changed attempts). XXX: With atheme services, # RSFNC: states that we support RSFNC (forced nick changed attempts). XXX: With atheme services,
# does this actually do anything? # does this actually do anything?
# EOPMOD: supports ETB (extended TOPIC burst) and =#channel messages for opmoderated +z # EOPMOD: supports ETB (extended TOPIC burst) and =#channel messages for opmoderated +z
f('CAPAB :QS ENCAP EX CHW IE KNOCK SAVE SERVICES TB EUID RSFNC EOPMOD') # KLN: supports remote KLINEs
f('CAPAB :QS ENCAP EX CHW IE KNOCK SAVE SERVICES TB EUID RSFNC EOPMOD SAVETS_100 KLN')
f('SERVER %s 0 :%s' % (self.irc.serverdata["hostname"], sdesc = self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']
self.irc.serverdata.get('serverdesc') or self.irc.botdata['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 :)
self.ping() self._ping_uplink()
def handle_pass(self, numeric, command, args): def handle_pass(self, numeric, command, args):
""" """
@ -346,20 +438,20 @@ class TS6Protocol(TS6BaseProtocol):
""" """
# <- PASS $somepassword TS 6 :42X # <- PASS $somepassword TS 6 :42X
if args[0] != self.irc.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?")
numeric = args[-1] numeric = args[-1]
log.debug('(%s) Found uplink SID as %r', self.irc.name, numeric) log.debug('(%s) Found uplink SID as %r', self.name, numeric)
# Server name and SID are sent in different messages, so we fill this # Server name and SID are sent in different messages, so we fill this
# with dummy information until we get the actual sid. # with dummy information until we get the actual sid.
self.irc.servers[numeric] = IrcServer(None, '') self.servers[numeric] = Server(self, None, '')
self.irc.uplink = numeric self.uplink = numeric
def handle_capab(self, numeric, command, args): def handle_capab(self, numeric, command, args):
""" """
@ -368,21 +460,18 @@ class TS6Protocol(TS6BaseProtocol):
# We only get a list of keywords here. Charybdis obviously assumes that # We only get a list of keywords here. Charybdis obviously assumes that
# we know what modes it supports (indeed, this is a standard list). # we know what modes it supports (indeed, this is a standard list).
# <- CAPAB :BAN CHW CLUSTER ENCAP EOPMOD EUID EX IE KLN KNOCK MLOCK QS RSFNC SAVE SERVICES TB UNKLN # <- CAPAB :BAN CHW CLUSTER ENCAP EOPMOD EUID EX IE KLN KNOCK MLOCK QS RSFNC SAVE SERVICES TB UNKLN
self.irc.caps = caps = args[0].split() self._caps = caps = args[0].split()
for required_cap in ('EUID', 'SAVE', 'TB', 'ENCAP', 'QS', 'CHW'): for required_cap in self.required_caps:
if required_cap not in caps: if required_cap not in caps:
raise ProtocolError('%s not found in TS6 capabilities list; this is required! (got %r)' % (required_cap, caps)) raise ProtocolError('%s not found in TS6 capabilities list; this is required! (got %r)' % (required_cap, caps))
if 'EX' in caps: if 'EX' in caps:
self.irc.cmodes['banexception'] = 'e' self.cmodes['banexception'] = 'e'
if 'IE' in caps: if 'IE' in caps:
self.irc.cmodes['invex'] = 'I' self.cmodes['invex'] = 'I'
if 'SERVICES' in caps: if 'SERVICES' in caps:
self.irc.cmodes['regonly'] = 'r' self.cmodes['regonly'] = 'r'
log.debug('(%s) self.irc.connected set!', self.irc.name)
self.irc.connected.set()
def handle_ping(self, source, command, args): def handle_ping(self, source, command, args):
"""Handles incoming PING commands.""" """Handles incoming PING commands."""
@ -399,39 +488,38 @@ class TS6Protocol(TS6BaseProtocol):
try: try:
destination = args[1] destination = args[1]
except IndexError: except IndexError:
destination = self.irc.sid destination = self.sid
if self.irc.isInternalServer(destination): if self.is_internal_server(destination):
self._send(destination, 'PONG %s %s' % (destination, source)) self._send_with_prefix(destination, 'PONG %s %s' % (destination, source), queue=False)
if destination == self.irc.sid and not self.has_eob: if not self.servers[source].has_eob:
# Charybdis' idea of endburst is just sending a PING. No, really! # TS6 endburst is just sending a PING to the other server.
# https://github.com/charybdis-ircd/charybdis/blob/dc336d1/modules/core/m_server.c#L484-L485 # https://github.com/charybdis-ircd/charybdis/blob/dc336d1/modules/core/m_server.c#L484-L485
self.has_eob = True self.servers[source].has_eob = True
if source == self.uplink:
log.debug('(%s) self.connected set!', self.name)
self.connected.set()
# Return the endburst hook. # Return the endburst hook.
return {'parse_as': 'ENDBURST'} return {'parse_as': 'ENDBURST'}
def handle_pong(self, source, command, args):
"""Handles incoming PONG commands."""
if source == self.irc.uplink:
self.irc.lastping = time.time()
def handle_sjoin(self, servernumeric, command, args): def handle_sjoin(self, servernumeric, command, args):
"""Handles incoming SJOIN commands.""" """Handles incoming SJOIN commands."""
# parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist # parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist
# <- :0UY SJOIN 1451041566 #channel +nt :@0UYAAAAAB # <- :0UY SJOIN 1451041566 #channel +nt :@0UYAAAAAB
channel = self.irc.toLower(args[1]) channel = args[1]
chandata = self._channels[channel].deepcopy()
userlist = args[-1].split() userlist = args[-1].split()
modestring = args[2:-1] or args[2] modestring = args[2:-1] or args[2]
parsedmodes = self.irc.parseModes(channel, modestring) parsedmodes = self.parse_modes(channel, modestring)
namelist = [] namelist = []
# Keep track of other modes that are added due to prefix modes being joined too. # Keep track of other modes that are added due to prefix modes being joined too.
changedmodes = set(parsedmodes) changedmodes = set(parsedmodes)
log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel) log.debug('(%s) handle_sjoin: got userlist %r for %r', self.name, userlist, channel)
for userpair in userlist: for userpair in userlist:
# charybdis sends this in the form "@+UID1, +UID2, UID3, @UID4" # charybdis sends this in the form "@+UID1, +UID2, UID3, @UID4"
r = re.search(r'([^\d]*)(.*)', userpair) r = re.search(r'([^\d]*)(.*)', userpair)
@ -439,33 +527,34 @@ class TS6Protocol(TS6BaseProtocol):
modeprefix = r.group(1) or '' modeprefix = r.group(1) or ''
finalprefix = '' finalprefix = ''
assert user, 'Failed to get the UID from %r; our regex needs updating?' % userpair assert user, 'Failed to get the UID from %r; our regex needs updating?' % userpair
log.debug('(%s) handle_sjoin: got modeprefix %r for user %r', self.irc.name, modeprefix, user) log.debug('(%s) handle_sjoin: got modeprefix %r for user %r', self.name, modeprefix, user)
# Don't crash when we get an invalid UID. # Don't crash when we get an invalid UID.
if user not in self.irc.users: if user not in self.users:
log.debug('(%s) handle_sjoin: tried to introduce user %s not in our user list, ignoring...', log.debug('(%s) handle_sjoin: tried to introduce user %s not in our user list, ignoring...',
self.irc.name, user) self.name, user)
continue continue
for m in modeprefix: for m in modeprefix:
# Iterate over the mapping of prefix chars to prefixes, and # Iterate over the mapping of prefix chars to prefixes, and
# find the characters that match. # find the characters that match.
for char, prefix in self.irc.prefixmodes.items(): for char, prefix in self.prefixmodes.items():
if m == prefix: if m == prefix:
finalprefix += char finalprefix += char
namelist.append(user) namelist.append(user)
self.irc.users[user].channels.add(channel) self.users[user].channels.add(channel)
# Only save mode changes if the remote has lower TS than us. # Only save mode changes if the remote has lower TS than us.
changedmodes |= {('+%s' % mode, user) for mode in finalprefix} changedmodes |= {('+%s' % mode, user) for mode in finalprefix}
self.irc.channels[channel].users.add(user) self._channels[channel].users.add(user)
# Statekeeping with timestamps # Statekeeping with timestamps
their_ts = int(args[0]) their_ts = int(args[0])
our_ts = self.irc.channels[channel].ts our_ts = self._channels[channel].ts
self.updateTS(servernumeric, channel, their_ts, changedmodes) self.updateTS(servernumeric, channel, their_ts, changedmodes)
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts} return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts,
'channeldata': chandata}
def handle_join(self, numeric, command, args): def handle_join(self, numeric, command, args):
"""Handles incoming channel JOINs.""" """Handles incoming channel JOINs."""
@ -474,175 +563,156 @@ class TS6Protocol(TS6BaseProtocol):
ts = int(args[0]) ts = int(args[0])
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.irc.users[numeric].channels.copy() oldchans = self.users[numeric].channels.copy()
log.debug('(%s) Got /join 0 from %r, channel list is %r', log.debug('(%s) Got /join 0 from %r, channel list is %r',
self.irc.name, numeric, oldchans) self.name, numeric, oldchans)
for channel in oldchans: for channel in oldchans:
self.irc.channels[channel].users.discard(numeric) self._channels[channel].users.discard(numeric)
self.irc.users[numeric].channels.discard(channel) self.users[numeric].channels.discard(channel)
return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'} return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'}
else: else:
channel = self.irc.toLower(args[1]) channel = args[1]
self.updateTS(numeric, channel, ts) self.updateTS(numeric, channel, ts)
self.irc.users[numeric].channels.add(channel) self.users[numeric].channels.add(channel)
self.irc.channels[channel].users.add(numeric) self._channels[channel].users.add(numeric)
# We send users and modes here because SJOIN and JOIN both use one hook, # We send users and modes here because SJOIN and JOIN both use one hook,
# for simplicity's sake (with plugins). # for simplicity's sake (with plugins).
return {'channel': channel, 'users': [numeric], 'modes': return {'channel': channel, 'users': [numeric], 'modes':
self.irc.channels[channel].modes, 'ts': ts} self._channels[channel].modes, 'ts': ts}
def handle_euid(self, numeric, command, args): 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)
ts, modes, ident, host, ip, uid, realhost, accountname, realname = args[2:11] ts, modes, ident, host, ip, uid, realhost, accountname, realname = args[2:11]
ts = int(ts)
if realhost == '*': if realhost == '*':
realhost = None realhost = host
log.debug('(%s) handle_euid got args: nick=%s ts=%s uid=%s ident=%s ' log.debug('(%s) handle_euid got args: nick=%s ts=%s uid=%s ident=%s '
'host=%s realname=%s realhost=%s ip=%s', self.irc.name, nick, ts, uid, 'host=%s realname=%s realhost=%s ip=%s', self.name, nick, ts, uid,
ident, host, realname, realhost, ip) ident, host, realname, realhost, ip)
assert ts != 0, "Bad TS 0 for user %s" % uid assert ts != 0, "Bad TS 0 for user %s" % uid
if ip == '0': # IP was invalid; something used for services. if ip == '0': # IP was invalid; something used for services.
ip = '0.0.0.0' ip = '0.0.0.0'
self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip) self.users[uid] = User(self, nick, ts, uid, numeric, ident, host, realname, realhost, ip)
parsedmodes = self.irc.parseModes(uid, [modes]) parsedmodes = self.parse_modes(uid, [modes])
log.debug('Applying modes %s for %s', parsedmodes, uid) log.debug('Applying modes %s for %s', parsedmodes, uid)
self.irc.applyModes(uid, parsedmodes) self.apply_modes(uid, parsedmodes)
self.irc.servers[numeric].users.add(uid) self.servers[numeric].users.add(uid)
# 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.
if ('+o', None) in parsedmodes: self._check_oper_status_change(uid, parsedmodes)
otype = 'Server Administrator' if ('+a', None) in parsedmodes else 'IRC Operator'
self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': otype}])
# Set the accountname if present # Set the accountname if present
if accountname != "*": if accountname != "*":
self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}]) self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}])
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip} # 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):
raise ProtocolError("Servers should use EUID instead of UID to send users! " """Handles legacy user introductions (UID)."""
"This IS a required capability after all...") # tl;dr We want to convert the following UID parameters:
# nickname, hopcount, nickTS, umodes, username, visible hostname, IP address, UID, gecos
# to EUID parameters when parsing:
# nickname, hopcount, nickTS, umodes, username, visible hostname, IP address, UID,
# real hostname, account name, gecos
def handle_sid(self, numeric, command, args): euid_args = args[:]
"""Handles incoming server introductions."""
# parameters: server name, hopcount, sid, server description # Insert a * to denote that the user is not logged in.
servername = args[0].lower() euid_args.insert(8, '*')
sid = args[2]
sdesc = args[-1] # Copy the visible hostname to the real hostname, as this data isn't sent yet.
self.irc.servers[sid] = IrcServer(numeric, servername, desc=sdesc) euid_args.insert(8, args[5])
return {'name': servername, 'sid': sid, 'text': sdesc}
return self.handle_euid(numeric, command, euid_args)
def handle_server(self, numeric, command, args): def handle_server(self, numeric, command, args):
""" """
Handles 1) incoming legacy (no SID) server introductions, Handles 1) incoming legacy (no SID) server introductions,
2) Sending server data in initial connection. 2) Sending server data in initial connection.
""" """
if numeric == self.irc.uplink and not self.irc.servers[numeric].name: if numeric == self.uplink and not self.servers[numeric].name:
# <- SERVER charybdis.midnight.vpn 1 :charybdis test server # <- SERVER charybdis.midnight.vpn 1 :charybdis test server
sname = args[0].lower() sname = args[0].lower()
log.debug('(%s) Found uplink server name as %r', self.irc.name, sname) log.debug('(%s) Found uplink server name as %r', self.name, sname)
self.irc.servers[numeric].name = sname self.servers[numeric].name = sname
self.irc.servers[numeric].desc = args[-1] self.servers[numeric].desc = args[-1]
# According to the TS6 protocol documentation, we should send SVINFO # According to the TS6 protocol documentation, we should send SVINFO
# when we get our uplink's SERVER command. # when we get our uplink's SERVER command.
self.irc.send('SVINFO 6 6 0 :%s' % int(time.time())) self.send('SVINFO 6 6 0 :%s' % int(time.time()))
return return
# <- :services.int SERVER a.bc 2 :(H) [GL] a # <- :services.int SERVER a.bc 2 :(H) [jlu5] a
servername = args[0].lower() return super().handle_server(numeric, command, args)
sdesc = args[-1]
self.irc.servers[servername] = IrcServer(numeric, servername, desc=sdesc)
return {'name': servername, 'sid': None, 'text': sdesc}
def handle_tmode(self, numeric, command, args): def handle_tmode(self, numeric, command, args):
"""Handles incoming TMODE commands (channel mode change).""" """Handles incoming TMODE commands (channel mode change)."""
# <- :42XAAAAAB TMODE 1437450768 #test -c+lkC 3 agte4 # <- :42XAAAAAB TMODE 1437450768 #test -c+lkC 3 agte4
# <- :0UYAAAAAD TMODE 0 #a +h 0UYAAAAAD # <- :0UYAAAAAD TMODE 0 #a +h 0UYAAAAAD
channel = self.irc.toLower(args[1]) channel = args[1]
oldobj = self.irc.channels[channel].deepcopy() oldobj = self._channels[channel].deepcopy()
modes = args[2:] modes = args[2:]
changedmodes = self.irc.parseModes(channel, modes) changedmodes = self.parse_modes(channel, modes)
self.irc.applyModes(channel, changedmodes) self.apply_modes(channel, changedmodes)
ts = int(args[0]) ts = int(args[0])
return {'target': channel, 'modes': changedmodes, 'ts': ts, return {'target': channel, 'modes': changedmodes, 'ts': ts,
'oldchan': oldobj} 'channeldata': oldobj}
def handle_mode(self, numeric, command, args):
"""Handles incoming user mode changes."""
# <- :70MAAAAAA MODE 70MAAAAAA -i+xc
target = args[0]
modestrings = args[1:]
changedmodes = self.irc.parseModes(target, modestrings)
self.irc.applyModes(target, changedmodes)
# Call the OPERED UP hook if +o is being set.
if ('+o', None) in changedmodes:
otype = 'Server Administrator' if ('a', None) in self.irc.users[target].modes else 'IRC Operator'
self.irc.callHooks([target, 'CLIENT_OPERED', {'text': otype}])
return {'target': target, 'modes': changedmodes}
def handle_tb(self, numeric, command, args): 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 = self.irc.toLower(args[0]) channel = args[0]
ts = args[1] ts = args[1]
setter = args[2] setter = args[2]
topic = args[-1] topic = args[-1]
self.irc.channels[channel].topic = topic self._channels[channel].topic = topic
self.irc.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}
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 = self.irc.toLower(args[1]) channel = args[1]
ts = args[2] ts = args[2]
setter = args[3] setter = args[3]
topic = args[-1] topic = args[-1]
self.irc.channels[channel].topic = topic self._channels[channel].topic = topic
self.irc.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}
def handle_invite(self, numeric, command, args):
"""Handles incoming INVITEs."""
# <- :70MAAAAAC INVITE 0ALAAAAAA #blah 12345
target = args[0]
channel = self.irc.toLower(args[1])
try:
ts = args[3]
except IndexError:
ts = int(time.time())
# We don't actually need to process this; it's just something plugins/hooks can use
return {'target': target, 'channel': channel, 'ts': ts}
def handle_chghost(self, numeric, command, args): def handle_chghost(self, numeric, command, args):
"""Handles incoming CHGHOST commands.""" """Handles incoming CHGHOST commands."""
target = args[0] target = self._get_UID(args[0])
self.irc.users[target].host = newhost = args[1] self.users[target].host = newhost = args[1]
return {'target': target, 'newhost': newhost} return {'target': target, 'newhost': newhost}
def handle_bmask(self, numeric, command, args): def handle_bmask(self, numeric, command, args):
"""Handles incoming BMASK commands (ban propagation on burst).""" """Handles incoming BMASK commands (ban propagation on burst)."""
# <- :42X BMASK 1424222769 #dev b :*!test@*.isp.net *!badident@* # <- :42X BMASK 1424222769 #dev b :*!test@*.isp.net *!badident@*
# This is used for propagating bans, not TMODE! # This is used for propagating bans, not TMODE!
channel = self.irc.toLower(args[1]) channel = args[1]
mode = args[2] mode = args[2]
ts = int(args[0]) ts = int(args[0])
modes = [] modes = []
for ban in args[-1].split(): for ban in args[-1].split():
modes.append(('+%s' % mode, ban)) modes.append(('+%s' % mode, ban))
self.irc.applyModes(channel, modes) self.apply_modes(channel, modes)
return {'target': channel, 'modes': modes, 'ts': ts} return {'target': channel, 'modes': modes, 'ts': ts}
def handle_472(self, numeric, command, args): def handle_472(self, numeric, command, args):
@ -653,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]
@ -663,14 +733,14 @@ class TS6Protocol(TS6BaseProtocol):
log.warning('(%s) User %r attempted to set channel mode %r, but the ' log.warning('(%s) User %r attempted to set channel mode %r, but the '
'extension providing it isn\'t loaded! To prevent possible' 'extension providing it isn\'t loaded! To prevent possible'
' desyncs, try adding the line "loadmodule "extensions/%s.so";" to ' ' desyncs, try adding the line "loadmodule "extensions/%s.so";" to '
'your IRCd configuration.', self.irc.name, setter, badmode, 'your IRCd configuration.', self.name, setter, badmode,
charlist[badmode]) charlist[badmode])
def handle_su(self, numeric, command, args): def handle_su(self, numeric, command, args):
""" """
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
@ -678,7 +748,7 @@ class TS6Protocol(TS6BaseProtocol):
account = '' # No account name means a logout account = '' # No account name means a logout
uid = args[0] uid = args[0]
self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}]) self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}])
def handle_rsfnc(self, numeric, command, args): def handle_rsfnc(self, numeric, command, args):
""" """
@ -687,4 +757,14 @@ class TS6Protocol(TS6BaseProtocol):
# <- :00A ENCAP somenet.relay RSFNC 801AAAAAB Guest75038 1468299643 :1468299675 # <- :00A ENCAP somenet.relay RSFNC 801AAAAAB Guest75038 1468299643 :1468299675
return {'target': args[0], 'newnick': args[1]} return {'target': args[0], 'newnick': args[1]}
def handle_realhost(self, uid, command, args):
"""Handles real host propagation."""
log.debug('(%s) Got REALHOST %s for %s', self.name, args[0], uid)
self.users[uid].realhost = args[0]
def handle_login(self, uid, command, args):
"""Handles login propagation on burst."""
self.users[uid].services_account = args[0]
return {'text': args[0]}
Class = TS6Protocol Class = TS6Protocol

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 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
@ -56,7 +59,6 @@ class TS6SIDGenerator():
self.output[idx] = self.allowedchars[idx][0] self.output[idx] = self.allowedchars[idx][0]
next(self.iters[idx]) next(self.iters[idx])
def increment(self, pos=2): def increment(self, pos=2):
""" """
Increments the SID generator to the next available SID. Increments the SID generator to the next available SID.
@ -85,79 +87,39 @@ class TS6SIDGenerator():
sid = ''.join(self.output) sid = ''.join(self.output)
return sid return sid
class TS6UIDGenerator(utils.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, irc): super().__init__(*args, **kwargs)
super().__init__(irc)
# Dictionary of UID generators (one for each server). # Dictionary of UID generators (one for each server).
self.uidgen = structures.KeyedDefaultdict(TS6UIDGenerator) self.uidgen = structures.KeyedDefaultdict(TS6UIDGenerator)
# SID generator for TS6. # SID generator for TS6.
self.sidgen = TS6SIDGenerator(irc) self.sidgen = TS6SIDGenerator(self)
def _send(self, source, msg): # Most TS6 variations (unreal, inspircd, charybdis) support this. For
"""Sends a TS6-style raw command from a source numeric to the self.irc connection given.""" # pure TS6, we also require the CHW capability which explicitly declares
self.irc.send(':%s %s' % (source, msg)) # support.
self.protocol_caps |= {'has-statusmsg'}
def _expandPUID(self, uid):
"""
Returns the outgoing nick for the given UID. In the base ts6_common implementation,
this does nothing, but other modules subclassing this can override it.
For example, this can be used to turn PUIDs (used to store legacy, UID-less users)
to actual nicks in outgoing messages, so that a remote IRCd can understand it.
"""
return uid
### OUTGOING COMMANDS ### OUTGOING COMMANDS
def numeric(self, source, numeric, target, text):
"""Sends raw numerics from a server to a remote client, used for WHOIS
replies."""
# Mangle the target for IRCds that require it.
target = self._expandPUID(target)
self._send(source, '%s %s %s' % (numeric, target, text))
def kick(self, numeric, channel, target, reason=None):
"""Sends kicks from a PyLink client/server."""
if (not self.irc.isInternalClient(numeric)) and \
(not self.irc.isInternalServer(numeric)):
raise LookupError('No such PyLink client/server exists.')
channel = self.irc.toLower(channel)
if not reason:
reason = 'No reason given'
# Mangle kick targets for IRCds that require it.
target = self._expandPUID(target)
self._send(numeric, 'KICK %s %s :%s' % (channel, target, reason))
# We can pretend the target left by its own will; all we really care about
# is that the target gets removed from the channel userlist, and calling
# handle_part() does that just fine.
self.handle_part(target, 'KICK', [channel])
def kill(self, numeric, target, reason): def kill(self, numeric, target, reason):
"""Sends a kill from a PyLink client/server.""" """Sends a kill from a PyLink client/server."""
if (not self.irc.isInternalClient(numeric)) and \ if (not self.is_internal_client(numeric)) and \
(not self.irc.isInternalServer(numeric)): (not self.is_internal_server(numeric)):
raise LookupError('No such PyLink client/server exists.') raise LookupError('No such PyLink client/server exists.')
# From TS6 docs: # From TS6 docs:
@ -168,220 +130,98 @@ class TS6BaseProtocol(IRCS2SProtocol):
# the kill followed by a space and a parenthesized reason. To avoid overflow, # the kill followed by a space and a parenthesized reason. To avoid overflow,
# it is recommended not to add anything to the path. # it is recommended not to add anything to the path.
assert target in self.irc.users, "Unknown target %r for kill()!" % target assert target in self.users, "Unknown target %r for kill()!" % target
if numeric in self.irc.users: if numeric in self.users:
# Killer was an user. Follow examples of setting the path to be "killer.host!killer.nick". # Killer was an user. Follow examples of setting the path to be "killer.host!killer.nick".
userobj = self.irc.users[numeric] userobj = self.users[numeric]
killpath = '%s!%s' % (userobj.host, userobj.nick) killpath = '%s!%s' % (userobj.host, userobj.nick)
elif numeric in self.irc.servers: elif numeric in self.servers:
# Sender was a server; killpath is just its name. # Sender was a server; killpath is just its name.
killpath = self.irc.servers[numeric].name killpath = self.servers[numeric].name
else: else:
# Invalid sender?! This shouldn't happen, but make the killpath our server name anyways. # Invalid sender?! This shouldn't happen, but make the killpath our server name anyways.
log.warning('(%s) Invalid sender %s for kill(); using our server name instead.', log.warning('(%s) Invalid sender %s for kill(); using our server name instead.',
self.irc.name, numeric) self.name, numeric)
killpath = self.irc.servers[self.irc.sid].name killpath = self.servers[self.sid].name
self._send(numeric, 'KILL %s :%s (%s)' % (target, killpath, reason)) self._send_with_prefix(numeric, 'KILL %s :%s (%s)' % (target, killpath, reason))
self.removeClient(target) self._remove_client(target)
def nick(self, numeric, newnick): def nick(self, numeric, newnick):
"""Changes the nick of a PyLink client.""" """Changes the nick of a PyLink client."""
if not self.irc.isInternalClient(numeric): if not self.is_internal_client(numeric):
raise LookupError('No such PyLink client exists.') raise LookupError('No such PyLink client exists.')
self._send(numeric, 'NICK %s %s' % (newnick, int(time.time()))) self._send_with_prefix(numeric, 'NICK %s %s' % (newnick, int(time.time())))
self.irc.users[numeric].nick = newnick self.users[numeric].nick = newnick
# Update the NICK TS. # Update the NICK TS.
self.irc.users[numeric].ts = int(time.time()) self.users[numeric].ts = int(time.time())
def part(self, client, channel, reason=None): def spawn_server(self, name, sid=None, uplink=None, desc=None):
"""Sends a part from a PyLink client."""
channel = self.irc.toLower(channel)
if not self.irc.isInternalClient(client):
log.error('(%s) Error trying to part %r from %r (no such client exists)', self.irc.name, client, channel)
raise LookupError('No such PyLink client exists.')
msg = "PART %s" % channel
if reason:
msg += " :%s" % reason
self._send(client, msg)
self.handle_part(client, 'PART', [channel])
def quit(self, numeric, reason):
"""Quits a PyLink client."""
if self.irc.isInternalClient(numeric):
self._send(numeric, "QUIT :%s" % reason)
self.removeClient(numeric)
else:
raise LookupError("No such PyLink client exists.")
def message(self, numeric, target, text):
"""Sends a PRIVMSG from a PyLink client."""
if not self.irc.isInternalClient(numeric):
raise LookupError('No such PyLink client exists.')
# Mangle message targets for IRCds that require it.
target = self._expandPUID(target)
self._send(numeric, 'PRIVMSG %s :%s' % (target, text))
def notice(self, numeric, target, text):
"""Sends a NOTICE from a PyLink client."""
if not self.irc.isInternalClient(numeric):
raise LookupError('No such PyLink client exists.')
# Mangle message targets for IRCds that require it.
target = self._expandPUID(target)
self._send(numeric, 'NOTICE %s :%s' % (target, text))
def topic(self, numeric, target, text):
"""Sends a TOPIC change from a PyLink client."""
if not self.irc.isInternalClient(numeric):
raise LookupError('No such PyLink client exists.')
self._send(numeric, 'TOPIC %s :%s' % (target, text))
self.irc.channels[target].topic = text
self.irc.channels[target].topicset = True
def spawnServer(self, name, sid=None, uplink=None, desc=None, endburst_delay=0):
""" """
Spawns a server off a PyLink server. desc (server description) Spawns a server off a PyLink server. desc (server description)
defaults to the one in the config. uplink defaults to the main PyLink defaults to the one in the config. uplink defaults to the main PyLink
server, and sid (the server ID) is automatically generated if not server, and sid (the server ID) is automatically generated if not
given. given.
Note: TS6 doesn't use a specific ENDBURST command, so the endburst_delay
option will be ignored if given.
""" """
# -> :0AL SID test.server 1 0XY :some silly pseudoserver # -> :0AL SID test.server 1 0XY :some silly pseudoserver
uplink = uplink or self.irc.sid uplink = uplink or self.sid
name = name.lower() name = name.lower()
desc = desc or self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc'] desc = desc or self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']
if sid is None: # No sid given; generate one! if sid is None: # No sid given; generate one!
sid = self.sidgen.next_sid() sid = self.sidgen.next_sid()
assert len(sid) == 3, "Incorrect SID length" assert len(sid) == 3, "Incorrect SID length"
if sid in self.irc.servers: if sid in self.servers:
raise ValueError('A server with SID %r already exists!' % sid) raise ValueError('A server with SID %r already exists!' % sid)
for server in self.irc.servers.values(): for server in self.servers.values():
if name == server.name: if name == server.name:
raise ValueError('A server named %r already exists!' % name) raise ValueError('A server named %r already exists!' % name)
if not self.irc.isInternalServer(uplink): if not self.is_internal_server(uplink):
raise ValueError('Server %r is not a PyLink server!' % uplink) raise ValueError('Server %r is not a PyLink server!' % uplink)
if not utils.isServerName(name): if not self.is_server_name(name):
raise ValueError('Invalid server name %r' % name) raise ValueError('Invalid server name %r' % name)
self._send(uplink, 'SID %s 1 %s :%s' % (name, sid, desc))
self.irc.servers[sid] = IrcServer(uplink, name, internal=True, desc=desc)
return sid
def squit(self, source, target, text='No reason given'): self.servers[sid] = Server(self, uplink, name, internal=True, desc=desc)
"""SQUITs a PyLink server.""" self._send_with_prefix(uplink, 'SID %s %s %s :%s' % (name, self.servers[sid].hopcount, sid, desc))
# -> SQUIT 9PZ :blah, blah return sid
log.debug('source=%s, target=%s', source, target)
self._send(source, 'SQUIT %s :%s' % (target, text))
self.handle_squit(source, 'SQUIT', [target, text])
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."""
if text: if text:
self._send(source, 'AWAY :%s' % text) self._send_with_prefix(source, 'AWAY :%s' % text)
else: else:
self._send(source, 'AWAY') self._send_with_prefix(source, 'AWAY')
self.irc.users[source].away = text self.users[source].away = text
### HANDLERS ### HANDLERS
def handle_events(self, data): def handle_knock(self, numeric, command, args):
"""Event handler for TS6 protocols. """Handles channel KNOCKs."""
# InspIRCd:
This passes most commands to the various handle_ABCD() functions # <- :70MAAAAAA ENCAP * KNOCK #blah :abcdefg
elsewhere defined protocol modules, coersing various sender prefixes # Charybdis:
from nicks and server names to UIDs and SIDs respectively, # <- :42XAAAAAC KNOCK #endlessvoid
whenever possible. # UnrealIRCd propagates knocks as a channel notice to all ops, so this handler is not used there.
channel = args[0]
Commands sent without an explicit sender prefix will have them set to
the SID of the uplink server.
"""
data = data.split(" ")
try: # Message starts with a SID/UID prefix.
args = self.parsePrefixedArgs(data)
sender = args[0]
command = args[1]
args = args[2:]
# If the sender isn't in UID format, try to convert it automatically.
# Unreal's protocol, for example, isn't quite consistent with this yet!
sender_server = self._getSid(sender)
if sender_server in self.irc.servers:
# Sender is a server when converted from name to SID.
numeric = sender_server
else:
# Sender is a user.
numeric = self._getUid(sender)
# parsePrefixedArgs() will raise IndexError if the TS6 sender prefix is missing.
except IndexError:
# Raw command without an explicit sender; assume it's being sent by our uplink.
args = self.parseArgs(data)
numeric = self.irc.uplink
command = args[0]
args = args[1:]
if command == 'ENCAP':
# Special case for encapsulated commands (ENCAP), in forms like this:
# <- :00A ENCAP * SU 42XAAAAAC :GLolol
command = args[1]
args = args[2:]
log.debug("(%s) Rewriting incoming ENCAP to command %s (args: %s)", self.irc.name, command, args)
try: try:
func = getattr(self, 'handle_'+command.lower()) text = args[1]
except AttributeError: # unhandled command except IndexError:
pass text = ''
else: return {'channel': channel, 'text': text}
parsed_args = func(numeric, command, args)
if parsed_args is not None:
return [numeric, command, parsed_args]
def handle_privmsg(self, source, command, args):
"""Handles incoming PRIVMSG/NOTICE."""
# <- :70MAAAAAA PRIVMSG #dev :afasfsa
# <- :70MAAAAAA NOTICE 0ALAAAAAA :afasfsa
target = args[0]
# Coerse =#channel from Charybdis op moderated +z to @#channel.
if target.startswith('='):
target = '@' + target[1:]
# We use lowercase channels internally, but uppercase UIDs.
# Strip the target of leading prefix modes (for targets like @#channel)
# before checking whether it's actually a channel.
stripped_target = target.lstrip(''.join(self.irc.prefixmodes.values()))
if utils.isChannel(stripped_target):
target = self.irc.toLower(target)
return {'target': target, 'text': args[1]}
handle_notice = handle_privmsg
def handle_kick(self, source, command, args):
"""Handles incoming KICKs."""
# :70MAAAAAA KICK #test 70MAAAAAA :some reason
channel = self.irc.toLower(args[0])
kicked = self._getUid(args[1])
self.handle_part(kicked, 'KICK', [channel, args[2]])
return {'channel': channel, 'target': kicked, 'text': args[2]}
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.irc.users[numeric].nick oldnick = self.users[numeric].nick
newnick = self.irc.users[numeric].nick = args[0] newnick = self.users[numeric].nick = args[0]
# Update the nick TS. # Update the nick TS.
self.irc.users[numeric].ts = ts = int(args[1]) self.users[numeric].ts = ts = int(args[1])
return {'newnick': newnick, 'oldnick': oldnick, 'ts': ts} return {'newnick': newnick, 'oldnick': oldnick, 'ts': ts}
@ -396,47 +236,32 @@ class TS6BaseProtocol(IRCS2SProtocol):
# -> :0AL000001 NICK Derp_ 1433728673 # -> :0AL000001 NICK Derp_ 1433728673
# <- :70M SAVE 0AL000001 1433728673 # <- :70M SAVE 0AL000001 1433728673
user = args[0] user = args[0]
oldnick = self.irc.users[user].nick oldnick = self.users[user].nick
self.irc.users[user].nick = user self.users[user].nick = user
# TS6 SAVE sets nick TS to 100. This is hardcoded in InspIRCd and # TS6 SAVE sets nick TS to 100. This is hardcoded in InspIRCd and
# charybdis. # charybdis.
self.irc.users[user].ts = 100 self.users[user].ts = 100
return {'target': user, 'ts': 100, 'oldnick': oldnick} return {'target': user, 'ts': 100, 'oldnick': oldnick}
def handle_topic(self, numeric, command, args): def handle_server(self, numeric, command, args):
"""Handles incoming TOPIC changes from clients. For topic bursts, """Handles the SERVER command, used for introducing older (TS5) servers."""
TB (TS6/charybdis) and FTOPIC (InspIRCd) are used instead.""" # <- :services.int SERVER a.bc 2 :(H) [jlu5] test jupe
# <- :70MAAAAAA TOPIC #test :test servername = args[0].lower()
channel = self.irc.toLower(args[0]) sdesc = args[-1]
topic = args[1] self.servers[servername] = Server(self, numeric, servername, desc=sdesc)
return {'name': servername, 'sid': None, 'text': sdesc}
oldtopic = self.irc.channels[channel].topic def handle_sid(self, numeric, command, args):
self.irc.channels[channel].topic = topic """Handles the SID command, used for introducing remote servers by our uplink."""
self.irc.channels[channel].topicset = True # <- SID services.int 2 00A :Shaltúre IRC Services
# parameters: server name, hopcount, sid, server description
return {'channel': channel, 'setter': numeric, 'text': topic, sname = args[0].lower()
'oldtopic': oldtopic} sid = args[2]
sdesc = args[-1]
def handle_part(self, source, command, args): self.servers[sid] = Server(self, numeric, sname, desc=sdesc)
"""Handles incoming PART commands.""" return {'name': sname, 'sid': sid, 'text': sdesc}
channels = self.irc.toLower(args[0]).split(',')
for channel in channels:
# We should only get PART commands for channels that exist, right??
self.irc.channels[channel].removeuser(source)
try:
self.irc.users[source].channels.discard(channel)
except KeyError:
log.debug("(%s) handle_part: KeyError trying to remove %r from %r's channel list?", self.irc.name, channel, source)
try:
reason = args[1]
except IndexError:
reason = ''
# Clear empty non-permanent channels.
if not (self.irc.channels[channel].users or ((self.irc.cmodes.get('permanent'), None) in self.irc.channels[channel].modes)):
del self.irc.channels[channel]
return {'channels': channels, 'text': reason}
def handle_svsnick(self, source, command, args): def handle_svsnick(self, source, command, args):
"""Handles SVSNICK (forced nickname change attempts).""" """Handles SVSNICK (forced nickname change attempts)."""
@ -445,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._getUid(args[0]), 'newnick': args[1]} return {'target': self._get_UID(args[0]), 'newnick': args[1]}

File diff suppressed because it is too large Load Diff

75
pylink
View File

@ -1,71 +1,14 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
PyLink IRC Services launcher.
"""
import os
import sys import sys
try: try:
from pylinkirc import world from pylinkirc import launcher
except ImportError: except ImportError:
sys.stderr.write("ERROR: Failed to import PyLink main module (pylinkirc.world).\n\nIf you are " print("ERROR: Failed to import PyLink launcher module (pylinkirc.launcher).\n\nIf you are "
"running PyLink from source, please install PyLink first using 'python3 " "running PyLink from source, please install PyLink first using 'python3 "
"setup.py install' (global install) or 'python3 setup.py install --user'" "setup.py install' (global install) or 'python3 setup.py install --user'"
" (local install)\n") " (local install)\n")
sys.exit(1) sys.exit(1)
from pylinkirc import conf, __version__ else:
if __name__ == '__main__':
if __name__ == '__main__': launcher.main()
import argparse
parser = argparse.ArgumentParser(description='Starts an instance of PyLink IRC Services.')
parser.add_argument('config', help='specifies the path to the config file (defaults to pylink.yml)', nargs='?', default='pylink.yml')
parser.add_argument("-v", "--version", help="displays the program version and exits", action='store_true')
parser.add_argument("-n", "--no-pid", help="skips generating PID files", action='store_true')
args = parser.parse_args()
if args.version: # Display version and exit
print('PyLink ' + __version__)
sys.exit()
# Load the config
conf.loadConf(args.config)
from pylinkirc.log import log
from pylinkirc import classes, utils, coremods
log.info('PyLink %s starting...', __version__)
# Write a PID file unless specifically told not to.
if not args.no_pid:
with open('%s.pid' % conf.confname, 'w') as f:
f.write(str(os.getpid()))
# Import plugins first globally, because they can listen for events
# that happen before the connection phase.
to_load = conf.conf['plugins']
# Here, we override the module lookup and import the plugins
# dynamically depending on which were configured.
for plugin in to_load:
try:
world.plugins[plugin] = pl = utils.loadPlugin(plugin)
except (OSError, ImportError) as e:
log.exception('Failed to load plugin %r: %s: %s', plugin, type(e).__name__, str(e))
else:
if hasattr(pl, 'main'):
log.debug('Calling main() function of plugin %r', pl)
pl.main()
# Initialize all the networks one by one
for network, sdata in conf.conf['servers'].items():
try:
protoname = sdata['protocol']
except (KeyError, TypeError):
log.error("(%s) Configuration error: No protocol module specified, aborting.", network)
else:
# Fetch the correct protocol module
proto = utils.getProtocolModule(protoname)
world.networkobjects[network] = irc = classes.Irc(network, proto, conf.conf)
world.started.set()
log.info("Loaded plugins: %s", ', '.join(sorted(world.plugins.keys())))

27
pylink-mkpasswd Executable file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""
Password hashing utility for PyLink IRC Services.
"""
import getpass
from pylinkirc.coremods.login import pwd_context
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='Hashes a password for use with PyLink IRC Services.')
parser.add_argument('password', help='specifies the password to hash', nargs='?', default='')
args = parser.parse_args()
assert pwd_context, 'Cannot hash passwords because passlib is missing! Install it via "pip3 install passlib".'
password = args.password
# If no password was given, enter one on the command line
if not password:
password = getpass.getpass()
password = password.strip()
assert password, "Password cannot be empty!"
print(pwd_context.hash(password))

4
requirements-docker.txt Normal file
View File

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

54
selectdriver.py Normal file
View File

@ -0,0 +1,54 @@
"""
Socket handling driver using the selectors module. epoll, kqueue, and devpoll
are used internally when available.
"""
import selectors
import threading
from pylinkirc import world
from pylinkirc.log import log
__all__ = ['register', 'unregister', 'start']
SELECT_TIMEOUT = 0.5
selector = selectors.DefaultSelector()
def _process_conns():
"""Main loop which processes connected sockets."""
while not world.shutting_down.is_set():
for socketkey, mask in selector.select(timeout=SELECT_TIMEOUT):
irc = socketkey.data
try:
if mask & selectors.EVENT_READ and not irc._aborted.is_set():
irc._run_irc()
except:
log.exception('Error in select driver loop:')
continue
def register(irc):
"""
Registers a network to the global selectors instance.
"""
log.debug('selectdriver: registering %s for network %s', irc._socket, irc.name)
selector.register(irc._socket, selectors.EVENT_READ, data=irc)
def unregister(irc):
"""
Removes a network from the global selectors instance.
"""
if irc._socket.fileno() != -1:
log.debug('selectdriver: de-registering %s for network %s', irc._socket, irc.name)
selector.unregister(irc._socket)
else:
log.debug('selectdriver: skipping de-registering %s for network %s', irc._socket, irc.name)
def start():
"""
Starts a thread to process connections.
"""
t = threading.Thread(target=_process_conns, name="Selector driver loop")
t.start()

View File

@ -1,26 +1,27 @@
"""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
# Get version from Git tags.
with open('VERSION', encoding='utf-8') as f: with open('VERSION', encoding='utf-8') as f:
version = f.read().strip() version = f.read().strip()
# Try to fetch the current commit hash from Git.
try: try:
real_version = subprocess.check_output(['git', 'describe', '--tags']).decode('utf-8').strip() real_version = subprocess.check_output(['git', 'describe', '--tags']).decode('utf-8').strip()
except Exception as e: except Exception as e:
print('ERROR: Failed to get Git version from "git describe --tags": %s: %s' % (type(e).__name__, e)) print('WARNING: Failed to get Git version from "git describe --tags": %s: %s' % (type(e).__name__, e))
real_version = version + '-dirty' print("If you're installing from PyPI or a tarball, ignore the above message.")
real_version = version + '-nogit'
# Write the version to disk. # Write the version to disk.
with open('__init__.py', 'w') as f: with open('__init__.py', 'w') as f:
@ -28,12 +29,11 @@ with open('__init__.py', 'w') as f:
f.write('__version__ = %r\n' % version) f.write('__version__ = %r\n' % version)
f.write('real_version = %r\n' % real_version) f.write('real_version = %r\n' % real_version)
# Convert Markdown to RST for PyPI
try: try:
import pypandoc with open('README.md') as f:
long_description = pypandoc.convert('README.md', 'rst') long_description = f.read()
except ImportError: except OSError:
print('WARNING: PyPandoc not available; skipping writing long description.') print('WARNING: Failed to read readme, skipping writing long_description')
long_description = None long_description = None
setup( setup(
@ -42,19 +42,20 @@ setup(
description='PyLink IRC Services', description='PyLink IRC Services',
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown',
url='https://github.com/GLolol/PyLink', url='https://github.com/jlu5/PyLink',
# Author details # Author details
author='James Lu', author='James Lu',
author_email='GLolol@overdrivenetworks.com', author_email='james@overdrivenetworks.com',
# Choose your license # License
license='MPL 2.0', license='MPL 2.0',
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Intended Audience :: System Administrators', 'Intended Audience :: System Administrators',
@ -69,23 +70,35 @@ 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.9',
'Programming Language :: Python :: 3.10',
], ],
keywords='IRC services relay', keywords='IRC services relay',
install_requires=['pyyaml', 'ircmatch'], install_requires=['pyyaml', 'cachetools'],
extras_require={
'password-hashing': ['passlib>=1.7.0'],
'cron-support': ['psutil'],
'relay-unicode': ['unidecode'],
},
# Folders (packages of code) # Folders (packages of code)
packages=['pylinkirc', 'pylinkirc.protocols', 'pylinkirc.plugins', 'pylinkirc.coremods'], packages=['pylinkirc', 'pylinkirc.protocols', 'pylinkirc.plugins', 'pylinkirc.coremods'],
# Data files # Data files
package_data={ package_data={
'': ['example-conf.yml'], '': ['example-conf.yml', 'VERSION', 'README.md'],
}, },
package_dir = {'pylinkirc': '.'}, package_dir = {'pylinkirc': '.'},
# Executable scripts # Executable scripts
scripts=["pylink"], scripts=["pylink-mkpasswd"],
entry_points = {
'console_scripts': ['pylink=pylinkirc.launcher:main'],
}
) )

View File

@ -5,7 +5,25 @@ This module contains custom data structures that may be useful in various situat
""" """
import collections import collections
import collections.abc
import json import json
import os
import pickle
import string
import threading
from copy import copy, deepcopy
from . import conf
from .log import log
__all__ = ['KeyedDefaultdict', 'CopyWrapper', 'CaseInsensitiveFixedSet',
'CaseInsensitiveDict', 'IRCCaseInsensitiveDict',
'CaseInsensitiveSet', 'IRCCaseInsensitiveSet',
'CamelCaseToSnakeCase', 'DataStore', 'JSONDataStore',
'PickleDataStore']
_BLACKLISTED_COPY_TYPES = []
class KeyedDefaultdict(collections.defaultdict): class KeyedDefaultdict(collections.defaultdict):
""" """
@ -14,112 +32,283 @@ 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
class DataStore: class CopyWrapper():
# will come into play with subclassing and db version upgrading """
initial_version = 1 Base container class implementing copy methods.
"""
def __init__(self, name, filename, db_format='json', save_frequency={'seconds': 30}): def copy(self):
self.name = name """Returns a shallow copy of this object instance."""
return copy(self)
self._filename = os.path.abspath(os.path.expanduser(filename)) def __deepcopy__(self, memo):
self._tmp_filename = self._filename + '.tmp' """Returns a deep copy of the channel object."""
log.debug('(db:{}) database path set to {}'.format(self.name, self._filename)) newobj = copy(self)
#log.debug('CopyWrapper: _BLACKLISTED_COPY_TYPES = %s', _BLACKLISTED_COPY_TYPES)
for attr, val in self.__dict__.items():
# We can't pickle IRCNetwork, so just return a reference of it.
if not isinstance(val, tuple(_BLACKLISTED_COPY_TYPES)):
#log.debug('CopyWrapper: copying attr %r', attr)
setattr(newobj, attr, deepcopy(val))
self._format = db_format memo[id(self)] = newobj
log.debug('(db:{}) format set to {}'.format(self.name, self._format))
self._save_frequency = timedelta(**save_frequency).total_seconds() return newobj
log.debug('(db:{}) saving every {} seconds'.format(self.name, self._save_frequency))
def create_or_load(self): def deepcopy(self):
log.debug('(db:{}) creating/loading datastore using {}'.format(self.name, self._format)) """Returns a deep copy of this object instance."""
return deepcopy(self)
if self._format == 'json': class CaseInsensitiveFixedSet(collections.abc.Set, CopyWrapper):
self._store = {} """
self._store_lock = threading.Lock() Implements a fixed set storing items case-insensitively.
"""
log.debug('(db:{}) loading json data store from {}'.format(self.name, self._filename)) def __init__(self, *, data=None):
try: if data is not None:
self._store = json.loads(open(self._filename, 'r').read()) self._data = data
except (ValueError, IOError, FileNotFoundError):
log.exception('(db:{}) failed to load existing db, creating new one in memory'.format(self.name))
self.put('db.version', self.initial_version)
else: else:
raise Exception('(db:{}) Data store format [{}] not recognised'.format(self.name, self._format)) self._data = set()
@staticmethod
def _keymangle(key):
"""Converts the given key to lowercase."""
if isinstance(key, str):
return key.lower()
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):
return "%s(%s)" % (self.__class__.__name__, self._data)
def __iter__(self):
return iter(self._data)
def __len__(self):
return len(self._data)
def __contains__(self, key):
return self._data.__contains__(self._keymangle(key))
def __copy__(self):
return self.__class__(data=self._data.copy())
class CaseInsensitiveDict(collections.abc.MutableMapping, CaseInsensitiveFixedSet):
"""
A dictionary storing items case insensitively.
"""
def __init__(self, *, data=None):
if data is not None:
self._data = data
else:
self._data = {}
def __getitem__(self, key):
key = self._keymangle(key)
return self._data[key]
def __setitem__(self, key, value):
self._data[self._keymangle(key)] = value
def __delitem__(self, key):
del self._data[self._keymangle(key)]
class IRCCaseInsensitiveDict(CaseInsensitiveDict):
"""
A dictionary storing items case insensitively, using IRC case mappings.
"""
def __init__(self, irc, *, data=None):
super().__init__(data=data)
self._irc = irc
def _keymangle(self, key):
"""Converts the given key to lowercase."""
if isinstance(key, str):
return self._irc.to_lower(key)
return key
def _from_iterable(self, it):
"""Returns a new iterable instance given the data in 'it'."""
return self.__class__(self._irc, data=it)
def __copy__(self):
return self.__class__(self._irc, data=self._data.copy())
class CaseInsensitiveSet(collections.abc.MutableSet, CaseInsensitiveFixedSet):
"""
A mutable set storing items case insensitively.
"""
def add(self, key):
self._data.add(self._keymangle(key))
def discard(self, key):
self._data.discard(self._keymangle(key))
class IRCCaseInsensitiveSet(CaseInsensitiveSet):
"""
A set storing items case insensitively, using IRC case mappings.
"""
def __init__(self, irc, *, data=None):
super().__init__(data=data)
self._irc = irc
def _keymangle(self, key):
"""Converts the given key to lowercase."""
if isinstance(key, str):
return self._irc.to_lower(key)
return key
def _from_iterable(self, it):
"""Returns a new iterable instance given the data in 'it'."""
return self.__class__(self._irc, data=it)
def __copy__(self):
return self.__class__(self._irc, data=self._data.copy())
class CamelCaseToSnakeCase():
"""
Class which automatically converts missing attributes from camel case to snake case.
"""
def __getattr__(self, attr):
"""
Attribute fetching fallback function which normalizes camel case attributes to snake case.
"""
assert isinstance(attr, str), "Requested attribute %r is not a string!" % attr
normalized_attr = '' # Start off with the first letter, which is ignored when processing
for char in attr:
if char in string.ascii_uppercase:
char = '_' + char.lower()
normalized_attr += char
classname = self.__class__.__name__
if normalized_attr == attr:
# __getattr__ only fires if normal attribute fetching fails, so we can assume that
# the attribute was tried already and failed.
raise AttributeError('%s object has no attribute with normalized name %r' % (classname, attr))
target = getattr(self, normalized_attr)
log.warning('%s.%s is deprecated, considering migrating to %s.%s!', classname, attr, classname, normalized_attr)
return target
class DataStore:
"""
Generic database class. Plugins should use a subclass of this such as JSONDataStore or
PickleDataStore.
"""
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.filename = filename
self.tmp_filename = filename + '.tmp'
log.debug('(DataStore:%s) using implementation %s', self.name, self.__class__.__name__)
log.debug('(DataStore:%s) database path set to %s', self.name, self.filename)
self.save_frequency = save_frequency or conf.conf['pylink'].get('save_delay', 300)
log.debug('(DataStore:%s) saving every %s seconds', self.name, self.save_frequency)
if default_db is not None:
self.store = default_db
else:
self.store = {}
self.store_lock = threading.Lock()
self.exportdb_timer = None
self.load()
if self.save_frequency > 0:
# If autosaving is enabled, start the save_callback loop.
self.save_callback(starting=True)
def load(self):
"""
DataStore load stub. Database implementations should subclass DataStore
and implement this.
"""
raise NotImplementedError
def save_callback(self, starting=False): def save_callback(self, starting=False):
"""Start the DB save loop.""" """Start the DB save loop."""
if self._format == 'json': # don't actually save the first time
# don't actually save the first time if not starting:
if not starting: self.save()
self.save()
# schedule saving in a loop. # schedule saving in a loop.
self.exportdb_timer = threading.Timer(self._save_frequency, self.save_callback) self.exportdb_timer = threading.Timer(self.save_frequency, self.save_callback)
self.exportdb_timer.name = 'PyLink {} save_callback Loop'.format(self.name) self.exportdb_timer.name = 'DataStore {} save_callback loop'.format(self.name)
self.exportdb_timer.start() self.exportdb_timer.start()
else:
raise Exception('(db:{}) Data store format [{}] not recognised'.format(self.name, self._format))
def save(self): def save(self):
log.debug('(db:{}) saving datastore'.format(self.name)) """
if self._format == 'json': DataStore save stub. Database implementations should subclass DataStore
with open(self._tmp_filename, 'w') as store_file: and implement this.
store_file.write(json.dumps(self._store)) """
os.rename(self._tmp_filename, self._filename) raise NotImplementedError
# single keys def die(self):
def __contains__(self, key): """
if self._format == 'json': Saves the database and stops any save loops.
return key in self._store """
if self.exportdb_timer:
self.exportdb_timer.cancel()
def get(self, key, default=None): self.save()
if self._format == 'json':
return self._store.get(key, default)
def put(self, key, value): class JSONDataStore(DataStore):
if self._format == 'json': def load(self):
# make sure we can serialize the given data """Loads the database given via JSON."""
# so we don't choke later on saving the db out with self.store_lock:
json.dumps(value)
self._store[key] = value
return True
def delete(self, key):
if self._format == 'json':
try: try:
with self._store_lock: with open(self.filename, "r") as f:
del self._store[key] self.store.clear()
except KeyError: self.store.update(json.load(f))
# key is already gone, nothing to do except (ValueError, IOError, OSError):
... log.info("(DataStore:%s) failed to load database %s; creating a new one in "
"memory", self.name, self.filename)
return True def save(self):
"""Saves the database given via JSON."""
with self.store_lock:
with open(self.tmp_filename, 'w') as f:
# Pretty print the JSON output for better readability.
json.dump(self.store, f, indent=4)
# multiple keys os.rename(self.tmp_filename, self.filename)
def list_keys(self, prefix=None):
"""Return all key names. If prefix given, return only keys that start with it."""
if self._format == 'json':
keys = []
with self._store_lock: class PickleDataStore(DataStore):
for key in self._store: def load(self):
if prefix is None or key.startswith(prefix): """Loads the database given via pickle."""
keys.append(key) with self.store_lock:
try:
with open(self.filename, "rb") as f:
self.store.clear()
self.store.update(pickle.load(f))
except (ValueError, IOError, OSError):
log.info("(DataStore:%s) failed to load database %s; creating a new one in "
"memory", self.name, self.filename)
return keys def save(self):
"""Saves the database given via pickle."""
with self.store_lock:
with open(self.tmp_filename, 'wb') as f:
# Force protocol version 4 as that is the lowest Python 3.4 supports.
pickle.dump(self.store, f, protocol=4)
def delete_keys(self, prefix): os.rename(self.tmp_filename, self.filename)
"""Delete all keys with the given prefix."""
if self._format == 'json':
with self._store_lock:
for key in tuple(self._store):
if key.startswith(prefix):
del self._store[key]

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()

Some files were not shown because too many files have changed in this diff Show More