4
0
mirror of https://github.com/jesopo/ircstates synced 2024-06-21 00:07:11 +00:00

Compare commits

...

224 Commits

Author SHA1 Message Date
jesopo
ab30dbe658 v0.12.1 release 2023-08-17 22:42:39 +00:00
jesopo
d370c67373 remove cachetools from requirements.txt 2023-08-17 22:42:00 +00:00
jesopo
0a844bd90d remove freezegun from non-dev requirements 2023-08-17 22:37:48 +00:00
jesopo
3dc56da30e add RPL_MONONLINE to numerics.py 2022-05-23 23:29:57 +00:00
jesopo
c21545e2c2 extraneous space 2022-01-29 21:19:43 +00:00
jesopo
275b2c7f3d slightly neater casemap.py 2022-01-29 21:19:31 +00:00
jesopo
1333228fd1 upgrade irctokens to v2.0.2 2022-01-29 21:18:41 +00:00
jesopo
9166d82359 v0.12.0 release 2022-01-07 19:03:18 +00:00
jesopo
ea9c0c2d1f str.maketrans is a much faster casefold; make casemaps an Enum 2022-01-07 18:59:37 +00:00
jesopo
bc7c4d75a8 v0.11.11 release 2022-01-07 11:42:07 +00:00
jesopo
a389c6f3cb remove python3.6; add python3.9 2022-01-07 11:39:30 +00:00
jesopo
83215e996b don't use --install-types. install types-cachetools specifically 2022-01-07 11:36:51 +00:00
jesopo
e0cbaa4519 .travis.yml before_script mypy --install-types 2022-01-07 11:30:37 +00:00
jesopo
e3884c7505 put a cachetools LRUCache on casefold() 2022-01-07 11:27:01 +00:00
jesopo
1e187db35f v0.11.10 release 2021-09-18 17:10:37 +00:00
jesopo
3a95bf4bca add RPL_YOUREOPER, RPL_RSACHALLENGE2, RPL_ENDOFRSACHALLENGE2 2021-09-18 16:47:28 +00:00
jesopo
5c50167d96 v0.11.9 release 2021-09-06 03:11:49 +00:00
jesopo
e5a7871fd9 record when we first saw a user in a channel and optionally when they JOINed 2021-09-06 03:10:25 +00:00
jesopo
3565259791 only make a new channel_user on NAMES when we don't have one 2021-09-06 03:04:49 +00:00
jesopo
8c16b73414 test topic_setter and topic_time in TOPIC test too 2021-09-06 02:51:34 +00:00
jesopo
806c6e4bf3 combine ChannelTestTopic.test_text and test_set_by_at 2021-09-06 02:50:22 +00:00
jesopo
d865ea3253 server.modes and channel_user.modes should be sets 2021-08-16 20:21:09 +00:00
jesopo
038c59659f freenode is dead long live libera.chat 2021-05-24 17:27:10 +00:00
jesopo
22552c5e3d v0.11.8 release 2021-04-10 13:50:16 +00:00
jesopo
ddcacabfda parse NICKLEN from ISUPPORT 2021-04-10 13:47:44 +00:00
jesopo
cb8aa4495a implement \xHH (hex) escapes in ISUPPORT token values 2021-02-28 15:27:22 +00:00
jesopo
3136d2b85c add missing return on RENAME handler 2021-02-18 14:58:14 +00:00
jesopo
4d14d67d4b support RENAME
closes #4
2021-02-16 22:10:10 +00:00
jesopo
566b8ec8cd unknown account status is None, known not-logged-in is empty string 2021-01-08 16:50:55 +00:00
jesopo
9b407b666d v0.11.7 release 2020-12-20 00:19:39 +00:00
jesopo
a69fd01766 add RPL_LOGOFF (WATCH) and RPL_MONOFFLINE (MONITOR) 2020-12-20 00:15:03 +00:00
jesopo
1f8dfe700f add RPL_ENDOFMOTD and RPL_NOMOTD 2020-12-20 00:14:48 +00:00
jesopo
202cf8227b v0.11.6 release 2020-12-01 21:45:49 +00:00
jesopo
2552e1cb54 change irctokens dependency from ==2.0.0 to ~=2.0.0 2020-12-01 16:00:15 +00:00
jesopo
17957798bb v0.11.5 release 2020-11-09 03:40:47 +00:00
jesopo
f44bbe41e4 don't try to parse info from :source-less PRIVMSG/NOTICE/TAGMSG 2020-11-08 20:02:34 +00:00
jesopo
f253159873 v0.11.4 release 2020-10-13 15:05:52 +00:00
jesopo
cfdcc8d7e7 requirements.txt: 'pendulum ==2.1.0' -> 'pendulum ~=2.1.0' 2020-10-12 22:05:28 +00:00
jesopo
f51f1b689e change pendulum dep from "==2.1.0" to ">=2.1.0" 2020-10-03 23:08:34 +00:00
jesopo
eb216e9abf v0.11.3 release 2020-10-03 21:22:39 +00:00
jesopo
58f83ad3de parse_tokens() was split out from recv() 2020-10-03 17:48:50 +00:00
jesopo
b4f91148eb slim down README.md socket-to-state example 2020-10-03 17:37:35 +00:00
jesopo
2b6d2bf7af add a simpler example to README.md 2020-10-03 17:29:33 +00:00
jesopo
412f829cb4 POST -> PORT typo 2020-10-03 17:26:49 +00:00
jesopo
4849010938 upgrade irctokens to v2.0.0 2020-09-30 20:06:25 +00:00
jesopo
76e29d7bad v0.11.2 release 2020-09-30 09:10:56 +00:00
jesopo
5a85e53485 channel.list_modes should always have keys, even if empty
closes #1
2020-09-29 11:55:15 +00:00
jesopo
e062b7b71f +kli are not list modes 2020-09-29 11:54:50 +00:00
jesopo
c841d1d6dd some test numerics were missing args 2020-09-29 11:44:45 +00:00
jesopo
07ed0bf13c WHOX IP must parse correctly (and we'll compress them) 2020-09-29 11:02:07 +00:00
jesopo
ca9abfc34b fix tests for casefolding now that we've swapped ^ and ~ 2020-08-17 17:00:37 +00:00
jesopo
66d6bba298 RFC2812 says []\~ is lower of {}|^, irc2 disagrees on ~ vs ^ 2020-08-17 16:56:05 +00:00
jesopo
ee5b0ceb4f v0.11.1 release 2020-08-07 14:53:30 +00:00
jesopo
dabb59d05f fix recv() typehinting 2020-08-07 14:53:12 +00:00
jesopo
5165573133 v0.11.0 release 2020-08-07 14:50:09 +00:00
jesopo
bf0f2fdc9f recv() doesn't call parse_tokens anymore - batch lines change state 2020-08-07 14:49:20 +00:00
jesopo
2c1468295e simplify parsing channel MODE & RPL_CHANNELMODEIS 2020-08-03 21:03:45 +00:00
jesopo
c27c48af54 IPs are a static connection property, dont overwrite if gone 2020-07-14 12:16:43 +00:00
jesopo
bf16308455 v0.10.3 release 2020-07-13 11:34:33 +01:00
jesopo
8a31f0190d save numeric ip result from WHOX 2020-07-12 23:16:33 +01:00
jesopo
2fb81e7aef parse out RPL_BANLIST and RPL_QUIETLIST 2020-07-11 15:36:21 +01:00
jesopo
87f85ba57c update README.md contact section to point to freenode 2020-07-10 12:09:11 +01:00
jesopo
9806a6407b pull server from WHO and WHOX 2020-07-09 11:04:44 +01:00
jesopo
01d8b8d111 remove probably copy-pasted CHGHOST from UserTestWHOIS 2020-07-08 23:42:23 +01:00
jesopo
875e912896 v0.10.2 release 2020-07-08 23:19:50 +01:00
jesopo
50a63ce12c parse out RPL_AWAY 2020-07-07 14:27:40 +01:00
jesopo
74490f616a pull away state out of WHO/WHOX 2020-07-06 20:53:09 +01:00
jesopo
adacb19c77 use line.source, that's what str(hostmask) does, null source throws on hostmask 2020-07-03 23:15:39 +01:00
jesopo
d76f50dac0 update irctokens to v1.1.0 2020-07-03 23:13:20 +01:00
jesopo
98823298e6 v0.10.1 release 2020-07-01 17:56:07 +01:00
jesopo
20d2f1a1db v0.10.0 release 2020-06-24 10:07:30 +01:00
jesopo
114688e266 update README.md away from channel_users/user_channels 2020-06-21 22:08:58 +01:00
jesopo
7a87b7b448 add ChannelUser.__repr__ 2020-06-21 22:04:26 +01:00
jesopo
c32b4bdd62 python3.6 doesn't have native dataclasses! 2020-06-21 18:47:45 +01:00
jesopo
83b31b6b2b add missing names.py file 2020-06-21 18:45:46 +01:00
jesopo
ea421f09af pass around nickname/channelname as Name objects, give to ChannelUser 2020-06-21 18:43:55 +01:00
jesopo
8b91dc09e3 slightly more efficient casefolding 2020-06-21 18:23:18 +01:00
jesopo
37227b6463 remove Named, give (nick)name/_lower to User/Channel ctors 2020-06-21 18:12:37 +01:00
jesopo
40ec25de2b don't casefold twice for self NICK 2020-06-21 00:24:45 +01:00
jesopo
e1286f16c6 python 3.6 doesn't have native dataclasses 2020-06-21 00:12:37 +01:00
jesopo
fdcf216255 rename isupport.chanmodes groups 2020-06-21 00:08:43 +01:00
jesopo
46a1d2bda8 ISupport.tokens(List[str]) -> ISupport.from_tokens(List[str]) 2020-06-21 00:08:22 +01:00
jesopo
c75a62f5d8 refactor CHANMODES logic. less redundant bool checking 2020-06-21 00:01:03 +01:00
jesopo
3290c33106 v0.9.19 release 2020-06-14 19:54:01 +01:00
jesopo
85794909d0 fix usermode change iterate typehints 2020-06-14 19:51:19 +01:00
jesopo
40839c1755 change how channel mode emit.tokens works ("+b" -> "+b mask") 2020-06-14 19:47:52 +01:00
jesopo
251d588ee8 v0.9.18 release 2020-06-07 20:39:30 +01:00
jesopo
122fe23da6 add ERR_NOSUCHSERVER 2020-06-07 20:39:11 +01:00
jesopo
4dbf2c1981 v0.9.17 release 2020-06-07 20:20:13 +01:00
jesopo
c68c59a534 update irctokens to v1.0.2 2020-06-07 20:19:56 +01:00
jesopo
6f79d97967 v0.9.16 release 2020-06-07 20:07:32 +01:00
jesopo
92c883ded2 v0.9.15 release (bad pypi package) 2020-06-07 18:49:38 +01:00
jesopo
80ef9a9edb add RPL_ENDOFWHOWAS 2020-06-07 17:51:57 +01:00
jesopo
6ee5c75790 v0.9.14 release 2020-06-07 17:41:52 +01:00
jesopo
191e8fdba3 add RPL_WHOWASUSER 2020-06-07 17:34:49 +01:00
jesopo
274b76ba56 add ERR_NOSUCHNICK 2020-06-05 13:09:13 +01:00
jesopo
bd3fd12a84 user.hostmask() will never be None 2020-06-04 00:14:46 +01:00
jesopo
232aa3bb61 v0.9.13 release 2020-06-03 21:31:43 +01:00
jesopo
e7d14e6f67 add user.hostmask():str and user.userhost():Optional[str] 2020-06-03 21:30:05 +01:00
jesopo
d212fbc11c v0.9.12 release 2020-06-02 20:36:59 +10:00
jesopo
5878f946e8 add RPL_TRYAGAIN and ERR_BADCHANNEL 2020-06-02 20:36:13 +10:00
jesopo
a434f19b9e v0.9.11 release 2020-05-07 08:40:05 +10:00
jesopo
56d047de8d switch datetimes to pendulum because stdlib doesnt handle timezones well 2020-05-07 08:38:24 +10:00
jesopo
271cadf666 use utcfromtimestamp for unit test datetimes. timezones are hard 2020-05-05 22:45:34 +01:00
jesopo
a1e5c07dbc parse unix timestamps as utc 2020-05-05 20:43:09 +01:00
jesopo
178f08d5b0 v0.9.10 release 2020-04-29 14:54:31 +01:00
jesopo
2355b42fbb update irctokens to v1.0.0 2020-04-29 14:52:02 +01:00
jesopo
d0a3aed19f support RPL_LOGGEDIN and RPL_LOGGEDOUT (with tests) 2020-04-29 14:48:03 +01:00
jesopo
5c5c6fca2b add RPL_LOGGEDIN/RPL_LOGGEDOUT 2020-04-29 14:35:25 +01:00
jesopo
f74294b7bb v0.9.9 release 2020-04-28 01:38:04 +01:00
jesopo
39694beff4 add ERR_TOOMANYCHANNELS 2020-04-28 01:37:29 +01:00
jesopo
8e4bbeb790 v0.9.8 release 2020-04-28 01:35:44 +01:00
jesopo
0818400221 add JOIN ERR_ numerics 2020-04-28 01:34:00 +01:00
jesopo
8fbf66fe71 v0.9.7 release 2020-04-28 00:29:40 +01:00
jesopo
fb4045f2f8 add NICK ERR_ numerics 2020-04-28 00:25:04 +01:00
jesopo
500859e9c0 import ServerDisconnectedError to __init__.py 2020-04-28 00:24:20 +01:00
jesopo
09acc74412 v0.9.6 release 2020-04-23 14:33:15 +01:00
jesopo
e02a535cd0 actually initialise emits 2020-04-23 14:32:23 +01:00
jesopo
febc891c45 v0.9.5 release 2020-04-22 18:00:06 +01:00
jesopo
31ab106e25 only return 1 emit (or None) 2020-04-22 17:58:28 +01:00
jesopo
909f72ece6 v0.9.4 release 2020-04-21 20:39:39 +01:00
jesopo
5a8ac14896 update irctokens to v0.9.6 2020-04-21 20:38:40 +01:00
jesopo
db9c6d48d1 user.channels is now Set[str], CUser has no .user/.channel now 2020-04-21 20:35:17 +01:00
jesopo
79ee3d8874 rename channel users on NICK 2020-04-21 15:36:06 +01:00
jesopo
959b288b3c store channel membership on Channel and User objects, change tests 2020-04-21 14:28:04 +01:00
jesopo
b587936c7f add ERR_NOSUCHCHANNEL 2020-04-20 17:10:56 +01:00
jesopo
985d800982 add test case for WHOX without account 2020-04-20 13:54:20 +01:00
jesopo
6eb429d6d5 v0.9.3 release 2020-04-19 14:13:48 +01:00
jesopo
ba2410c539 update irctokens to v0.9.5 2020-04-19 14:12:15 +01:00
jesopo
548bd722a8 v0.9.2 release 2020-04-19 02:14:15 +01:00
jesopo
c0339c11cb add return type to prepare_whox() 2020-04-19 02:13:08 +01:00
jesopo
0d9f8dd268 add WHOX support, add tests for WHO and WHOX 2020-04-19 02:12:04 +01:00
jesopo
df3aba9521 RPL_ENDOFBANLIST should be 368 not 367 2020-04-19 02:11:35 +01:00
jesopo
dc878df2f4 remove NUMERIC_NUMBERS; i dont think we'll need it 2020-04-19 01:44:47 +01:00
jesopo
0ed79f18f5 v0.9.1 release 2020-04-18 15:11:04 +01:00
jesopo
09ea845d2d use RPL_/ERR_ consts, not magic strings 2020-04-18 15:08:54 +01:00
jesopo
e62e22d663 add numerics.RPL_ENDOFWHO 2020-04-18 15:07:43 +01:00
jesopo
872cb84c0e v0.9.0 release 2020-04-17 20:59:59 +01:00
jesopo
21a6c4c47d change numerics.py in to consts, not just dict lookups 2020-04-17 20:58:17 +01:00
jesopo
32e7ba230e v0.8.9 release 2020-04-13 17:37:38 +01:00
jesopo
3fdbe04e0f update irctokens reference to v0.9.4 2020-04-13 17:35:18 +01:00
jesopo
25408c7e5e v0.8.8 release 2020-04-11 14:06:35 +01:00
jesopo
517737a921 update irctokens reference to v0.9.3 2020-04-11 14:05:14 +01:00
jesopo
f705f20e94 add unit tests for MODE emits 2020-04-10 16:05:25 +01:00
jesopo
f9034d6a1b put tokens on to MODE emits 2020-04-10 10:55:57 +01:00
jesopo
05644d3ff1 v0.8.7 release 2020-04-08 22:03:59 +01:00
jesopo
c902c1026c add RPL_WHOISACCOUNT 2020-04-08 22:01:53 +01:00
jesopo
fe505ce4d2 v0.8.6 release 2020-04-08 21:44:02 +01:00
jesopo
0257a88f87 add RPL_WHOISCHANNELS 2020-04-08 21:43:38 +01:00
jesopo
90def4aba5 add WHOIS numerics 2020-04-08 21:23:02 +01:00
jesopo
0817726d3a v0.8.5 release 2020-04-08 17:54:54 +01:00
jesopo
22361071d9 import NUMERICS_ in to __init__.py 2020-04-08 17:53:32 +01:00
jesopo
5df1888ead handle numerics by their names, less confusing 2020-04-08 17:50:38 +01:00
jesopo
22b8488198 v0.8.4 release 2020-04-08 17:21:38 +01:00
jesopo
5f62d22076 update tests for 211/221 typo 2020-04-08 17:20:05 +01:00
jesopo
8089ab4a55 211 -> 221 (RPL_UMODEIS rather than RPL_STATSLINKINFO) 2020-04-08 17:16:57 +01:00
jesopo
3c3a56aada v0.8.3 release 2020-04-05 13:11:34 +01:00
jesopo
796988fabe add ircserver.registered:bool to tell if we've seen 001 or not 2020-04-05 13:08:26 +01:00
jesopo
4c3cd3d453 v0.8.2 release 2020-04-05 12:29:14 +01:00
jesopo
048ae4252e make CAP tokens without "=value" be key:"", avoiding None checks 2020-04-02 17:17:09 +01:00
jesopo
8b18a696be add contact section to README.md 2020-04-01 23:39:07 +01:00
jesopo
c12a327ab9 v0.8.1 release 2020-04-01 23:19:11 +01:00
jesopo
b874aeb295 add Emit.tokens, use it for CAP tokens 2020-04-01 23:18:22 +01:00
jesopo
99a2a50d23 v0.8.0 release 2020-04-01 22:43:24 +01:00
jesopo
14caea8f07 add Emit.subcommand and Emit.finished, use for CAP 2020-04-01 22:41:50 +01:00
jesopo
c7b4eb7a32 server.caps->server.available_caps (now {} by default, add self.has_cap) 2020-04-01 22:41:15 +01:00
jesopo
552db49d76 add ->Emit: return hints to @line_handler functions 2020-04-01 16:51:22 +01:00
jesopo
a31e7f6a49 Emits -> Emit 2020-04-01 16:47:11 +01:00
jesopo
13dcad454e return both Emits and Lines from server.recv() 2020-04-01 16:34:00 +01:00
jesopo
aad5e0d363 v0.7.0 release 2020-04-01 01:59:39 +01:00
jesopo
f4be365bc6 update license in setup.py 2020-04-01 01:59:20 +01:00
jesopo
66c2655df2 switch LICENCE from GPL-3.0 to MIT 2020-04-01 01:58:20 +01:00
jesopo
d62d0f6ce5 v0.6.3 release 2020-04-01 00:54:31 +01:00
jesopo
a2fa19e346 '_create_channel'->'create_channel', '_create_user'->'create_user' 2020-04-01 00:52:53 +01:00
jesopo
660b2b8bcd v0.6.2 release 2020-03-31 23:58:44 +01:00
jesopo
833a22fa2f Named.name shouldn't be Optional. things need names! 2020-03-31 23:58:09 +01:00
jesopo
ddecc26d5e v0.6.1 release 2020-03-28 12:08:47 +00:00
jesopo
d90cb90bdc update irctokens to 0.9.2 2020-03-28 12:08:17 +00:00
jesopo
d2b7282d86 v0.6.0 release 2020-03-28 12:05:50 +00:00
jesopo
740d926c7a IRC objects should be geared towards being inheritable 2020-03-28 12:05:36 +00:00
jesopo
5d2a76872d v0.5.3 release 2020-03-28 11:50:10 +00:00
jesopo
9aa6eae021 self.nickname_lower doesn't exist on on-connect NOTICE 2020-03-28 11:33:00 +00:00
jesopo
a486ce7f10 v0.5.2 release 2020-03-19 16:09:07 +00:00
jesopo
6b883e9b67 fix package_data pointing at irctokens (copypaste!) 2020-03-19 16:08:07 +00:00
jesopo
1adcdb484e v0.5.1 release 2020-03-19 12:11:23 +00:00
jesopo
6b12f3290f decorators.line_handler_decorator -> decorators.handler_decorator 2020-03-19 11:47:18 +00:00
jesopo
2626cdb7a1 0.5.0 release 2020-03-19 11:35:04 +00:00
jesopo
e332a91cce fix parse_tokens() all_emits typehints 2020-03-19 11:29:37 +00:00
jesopo
35b2e46945 change Emits to just be one object 2020-03-19 11:28:44 +00:00
jesopo
c674a34c37 emit a EmitUsers object from 353, not multiple EmitUser objects 2020-03-18 16:42:17 +00:00
jesopo
9e73252a8e "emit" important attributes from parsing tokens (who sent it, where, etc) 2020-03-18 15:59:42 +00:00
jesopo
1a7626e7d4 handle NOTICE and TAGMSG, like PRIVMSG, to grab hostmask 2020-03-17 09:55:31 +00:00
jesopo
bfdea91ec3 remove more extraneous spaces 2020-03-16 19:29:19 +00:00
jesopo
3d3759218d add 005: network, statusmsg, callerid, invex, excepts, monitor, watch, whox 2020-03-16 00:38:14 +00:00
jesopo
9534293500 extraneous spaces 2020-03-16 00:05:43 +00:00
jesopo
628b79cb92 ctor Users with nickname_lower, add tests for NICK casefold 2020-03-15 23:53:21 +00:00
jesopo
ce248c64cd keep a casefolded copy of our nickname, use it for ==self.nickname 2020-03-15 23:40:02 +00:00
jesopo
3e55f39bb7 test nicknames being casefolded on JOIN 2020-03-15 23:28:04 +00:00
jesopo
d8e4909017 casemap_lower() -> casefold(), casemap_equals() -> casefold_equals() 2020-03-15 23:16:06 +00:00
jesopo
b089e8b6f6 add ISUPPORT CASEMAPPING tests, fallback to rfc1459 on unknown 2020-03-15 23:12:34 +00:00
jesopo
af34486711 add support for casemappings (using 005 CASEMAPPING) 2020-03-15 23:05:42 +00:00
jesopo
097df312b4 add tests for ISUPPORT (005) 2020-03-15 22:43:33 +00:00
jesopo
5a0569a22f remove anything related to sending; we're just a state machine. 2020-03-15 21:25:17 +00:00
jesopo
a379539af3 add tests for PREFIX channel modes 2020-03-15 21:11:44 +00:00
jesopo
2ad24c6b33 v0.4.0 release 2020-03-15 20:36:06 +00:00
jesopo
40de45e068 support userhost-in-names, support MODE for PREFIX modes 2020-03-15 20:29:39 +00:00
jesopo
510d160dc0 upgrade irctokens to v0.8.M0 2020-03-15 20:29:15 +00:00
jesopo
5af787bb56 handle IRCv3 account-notify 2020-03-15 19:40:50 +00:00
jesopo
3bebb384cc support IRCv3 extended-join 2020-03-15 19:32:23 +00:00
jesopo
77aaeb8db6 subtly improve socket to state README.md section 2020-03-14 22:09:02 +00:00
jesopo
1bc8d0adae remove PING handler (we shouldn't be sending stuff. we're a state machine) 2020-03-14 21:56:12 +00:00
jesopo
f464b4a6fd v0.3.0 release 2020-03-13 13:22:17 +00:00
jesopo
8e24e46493 add SETNAME support 2020-03-13 13:18:27 +00:00
jesopo
ccfd416867 support AWAY against ourself too 2020-03-13 13:13:28 +00:00
jesopo
9617919eb1 add AWAY support 2020-03-13 13:08:58 +00:00
jesopo
34293b261e add RPL_WHOISUSER (311) support 2020-03-13 11:23:54 +00:00
jesopo
0778bf84dc use "is None" not "== None" to unconfuse mypy 2020-03-13 11:11:33 +00:00
jesopo
0db0a5715e Revert "server.caps shouldn't be None before cap_ls, add bool server.cap_ls for this"
This reverts commit 04c8fd8aeb.
2020-03-13 11:10:46 +00:00
jesopo
04c8fd8aeb server.caps shouldn't be None before cap_ls, add bool server.cap_ls for this 2020-03-13 11:08:13 +00:00
jesopo
e8d4f24238 add CAP LS/ACK/NEW/DEL support 2020-03-13 10:56:48 +00:00
jesopo
782ec2f921 add CHGHOST support 2020-03-13 10:19:51 +00:00
jesopo
25014835b4 PRIVMSG user@host parsing should be able to do self and user simultaneously 2020-03-13 10:19:20 +00:00
jesopo
244de5fd43 combine some self/other tests in test/user.py 2020-03-13 09:41:26 +00:00
jesopo
e787acb480 parse user/host/real from RPL_WHOREPLY 2020-03-13 09:29:49 +00:00
31 changed files with 2098 additions and 1105 deletions

View File

@ -1,14 +1,13 @@
language: python
cache: pip
python:
- "3.6"
- "3.7"
- "3.8"
- "3.8-dev"
- "3.9"
install:
- pip3 install mypy -r requirements.txt
script:
- pip3 install mypy types-cachetools -r requirements-dev.txt
before_script:
- pip3 freeze
- mypy ircstates
script:
- python3 -m unittest test

695
LICENSE
View File

@ -1,674 +1,21 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
MIT License
Copyright (c) 2020 jesopo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -13,40 +13,50 @@ additional arbitrary functionality on top of it.
## usage
### simple
```python
import ircstates
server = ircstates.Server("freenode")
lines = server.recv(b":server 001 nick :hello world!\r\n")
lines += server.recv(b":nick JOIN #chan\r\n")
for line in lines:
server.parse_tokens(line)
chan = server.channels["#chan"]
```
### socket to state
```python
import ircstates, irctokens, socket
NICK = "nickname"
CHAN = "#chan"
HOST = "127.0.0.1"
POST = 6667
PORT = 6667
server = ircstates.Server("freenode")
sock = socket.socket()
server = ircstates.Server("freenode")
sock = socket.socket()
sock.connect((HOST, POST))
sock.connect((HOST, PORT))
def _send(raw: str):
sock.sendall(f"{raw}\r\n".encode("utf8"))
def _send(s):
line = irctokens.tokenise(s)
server.send(line)
_send("USER test 0 * :test")
_send("NICK test321")
_send("USER test 0 * test")
_send(f"NICK {NICK}")
while True:
while server.pending():
send_lines = server.sent(sock.send(server.pending()))
for line in send_lines:
print(f"> {line.format()}")
recv_lines = server.recv(sock.recv(1024))
recv_data = sock.recv(1024)
recv_lines = server.recv(recv_data)
for line in recv_lines:
server.parse_tokens(line)
print(f"< {line.format()}")
# user defined behaviors...
if line.command == "001" and not "#test321" in server.channels:
_send("JOIN #test321")
if line.command == "PING":
_send(f"PONG :{line.params[0]}")
```
### get a user's channels
@ -56,8 +66,8 @@ while True:
>>> user = server.users["nickname"]
>>> user
User(nickname='nickname')
>>> server.user_channels[user]
{Channel(name='#chan')}
>>> user.channels
{'#chan'}
```
### get a channel's users
@ -67,17 +77,20 @@ User(nickname='nickname')
>>> channel = server.channels["#chan"]
>>> channel
Channel(name='#chan')
>>> server.channel_users[channel]
{User(nickname='nickname'): ChannelUser(user='nickname', channel='#chan', modes='ov')}
>>> channel.users
{'jess': ChannelUser(#chan jess)}
```
### get a user's modes in channel
```python
>>> user = server.users["nickname"]
>>> channel = server.channels["#chan"]
>>> channel_user = server.channel_users[channel][user]
>>> channel_user = channel.users["nickname"]
>>> channel_user
ChannelUser(user='nickname', channel='#chan', modes='ov')
ChannelUser(#chan jess +ov)
>>> channel_user.modes
{'o', 'v'}
```
## contact
Come say hi at `#irctokens` on irc.libera.chat

View File

@ -1 +1 @@
0.2.0
0.12.1

View File

@ -1,3 +1,6 @@
from .server import Server
from .user import User
from .channel import Channel
from .server import Server, ServerDisconnectedException
from .user import User
from .channel import Channel
from .channel_user import ChannelUser
from .casemap import casefold, CaseMap
from .emit import *

21
ircstates/casemap.py Normal file
View File

@ -0,0 +1,21 @@
from enum import Enum
from string import ascii_lowercase, ascii_uppercase
from typing import Dict, List, Optional
class CaseMap(Enum):
ASCII = "ascii"
RFC1459 = "rfc1459"
CASEMAPS: Dict[CaseMap, Dict[int, Optional[int]]] = {
CaseMap.ASCII: str.maketrans(
r"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
r"abcdefghijklmnopqrstuvwxyz"
),
CaseMap.RFC1459: str.maketrans(
r"ABCDEFGHIJKLMNOPQRSTUVWXYZ\[]^",
r"abcdefghijklmnopqrstuvwxyz|{}~"
)
}
def casefold(casemap_name: CaseMap, s: str):
casemap = CASEMAPS[casemap_name]
return s.translate(casemap)

View File

@ -1,32 +1,53 @@
from datetime import datetime
from typing import Dict, List, Optional, Set
from .named import Named
from typing import Dict, List, Optional, Set
from pendulum import DateTime
class Channel(Named):
def __init__(self, name: str):
self.name = name
from .channel_user import ChannelUser
from .names import Name
class Channel(object):
def __init__(self, name: Name):
self._name = name
self.users: Dict[str, ChannelUser] = {}
self.topic: Optional[str] = None
self.topic_setter: Optional[str] = None
self.topic_time: Optional[datetime] = None
self.topic_time: Optional[DateTime] = None
self.created: Optional[datetime] = None
self.created: Optional[DateTime] = None
self.list_modes: Dict[str, List[str]] = {}
self.modes: Dict[str, Optional[str]] = {}
self._list_modes_temp: Dict[str, List[str]] = {}
self.list_modes: Dict[str, List[str]] = {}
self.modes: Dict[str, Optional[str]] = {}
def __repr__(self) -> str:
return f"Channel(name={self.name!r})"
def get_name(self) -> Name:
return self._name
@property
def name(self) -> str:
return self._name.normal
@property
def name_lower(self) -> str:
return self._name.folded
def change_name(self,
normal: str,
folded: str):
self._name.normal = normal
self._name.folded = folded
def add_mode(self,
char: str,
param: Optional[str],
list_mode: bool):
if list_mode:
if not char in self.list_modes:
self.list_modes[char] = []
if not param in self.list_modes[char]:
self.list_modes[char].append(param or "")
if param is not None:
if not char in self.list_modes:
self.list_modes[char] = []
if not param in self.list_modes[char]:
self.list_modes[char].append(param)
else:
self.modes[char] = param
@ -34,9 +55,8 @@ class Channel(Named):
char: str,
param: Optional[str]):
if char in self.list_modes:
if param in self.list_modes[char]:
if (param is not None and
param in self.list_modes[char]):
self.list_modes[char].remove(param)
if not self.list_modes[char]:
del self.list_modes[char]
elif char in self.modes:
del self.modes[char]

View File

@ -1,17 +1,31 @@
from typing import Set
from .user import User
from .channel import Channel
from typing import List, Optional, Set
from .names import Name
from pendulum import DateTime, now
class ChannelUser(object):
def __init__(self,
channel: Channel,
user: User):
self.channel = channel
self.user = user
nickname: Name,
channel_name: Name):
self._nickname = nickname
self._channel_name = channel_name
self.modes: Set[str] = set([])
self.modes: Set[str] = set()
self.since = now("utc")
self.joined: Optional[DateTime] = None
def __repr__(self) -> str:
return (f"ChannelUser(user={self.user.nickname!r},"
f" channel={self.channel.name!r},"
f" modes={''.join(self.modes)!r})")
outs: List[str] = [self.channel, self.nickname]
if self.modes:
outs.append(f"+{''.join(self.modes)}")
return f"ChannelUser({' '.join(outs)})"
@property
def nickname(self) -> str:
return self._nickname.normal
@property
def nickname_lower(self) -> str:
return self._nickname.folded
@property
def channel(self) -> str:
return self._channel_name.normal

View File

@ -1,11 +1,11 @@
from typing import Any, Dict, List
def line_handler_decorator(d: Dict[str, List[Any]]):
def line_handler(command: str):
def handler_decorator(d: Dict[str, List[Any]]):
def _handler(command: str):
def _(func: Any):
if not command in d:
d[command] = []
d[command].append(func)
return func
return _
return line_handler
return _handler

28
ircstates/emit.py Normal file
View File

@ -0,0 +1,28 @@
from typing import List, Optional
from .user import User
from .channel import Channel
class Emit(object):
command: Optional[str] = None
subcommand: Optional[str] = None
text: Optional[str] = None
tokens: Optional[List[str]] = None
finished: Optional[bool] = None
self: Optional[bool] = None
self_source: Optional[bool] = None
self_target: Optional[bool] = None
user: Optional[User] = None
user_source: Optional[User] = None
user_target: Optional[User] = None
users: Optional[List[User]] = None
channel: Optional[Channel] = None
channel_source: Optional[Channel] = None
channel_target: Optional[Channel] = None
target: Optional[str] = None

View File

@ -1,19 +1,63 @@
from typing import List, Optional
from typing import Dict, List, Optional
from .tokens import ChanModes, Prefix
from ..casemap import CaseMap
CASEMAPPINGS = ["rfc1459", "ascii"]
def _parse_escapes(s: str):
idx = 0
out = ""
while idx < (len(s)):
if s[idx] == "\\":
if s[idx+1:]:
if (s[idx+1] == "x" and
len(s[idx+2:]) >= 2):
out += chr(int(s[idx+2:idx+4], 16))
idx += 4
else:
out += s[idx+1]
idx += 2
else:
out += s[idx]
idx += 1
return out
class ISupport(object):
raw: Dict[str, Optional[str]]
network: Optional[str] = None
chanmodes = ChanModes(["b"], ["k"], ["l"], ["i", "m", "n", "p", "s", "t"])
prefix = Prefix(["o", "v"], ["@", "+"])
modes: int = 3 # -1 if "no limit"
casemapping: str = "rfc1459"
modes: int = 3 # -1 if "no limit"
casemapping: CaseMap = CaseMap.RFC1459
chantypes: List[str] = ["#"]
statusmsg: List[str] = []
def tokens(self, tokens: List[str]):
callerid: Optional[str] = None
excepts: Optional[str] = None
invex: Optional[str] = None
monitor: Optional[int] = None # -1 if "no limit"
watch: Optional[int] = None # -1 if "no limit"
whox: bool = False
nicklen: int = 9 # from RFC1459
def __init__(self):
self.raw = {}
def from_tokens(self, tokens: List[str]):
for token in tokens:
key, sep, value = token.partition("=")
value = _parse_escapes(value)
self.raw[key] = value if sep else None
if key == "CHANMODES":
if key == "NETWORK":
self.network = value
elif key == "CHANMODES":
a, b, c, d = value.split(",")
self.chanmodes = ChanModes(list(a), list(b), list(c), list(d))
@ -21,11 +65,31 @@ class ISupport(object):
modes, prefixes = value[1:].split(")")
self.prefix = Prefix(list(modes), list(prefixes))
elif key == "STATUSMSG":
self.statusmsg = list(value)
elif key == "MODES":
self.modes = int(value) if value else -1
self.modes = int(value) if value else -1
elif key == "MONITOR":
self.monitor = int(value) if value else -1
elif key == "WATCH":
self.watch = int(value) if value else -1
elif key == "CASEMAPPING":
self.casemapping = value
self.casemapping = CaseMap(value)
elif key == "CHANTYPES":
self.chantypes = list(value)
elif key == "CALLERID":
self.callerid = value or "g"
elif key == "EXCEPTS":
self.excepts = value or "e"
elif key == "INVEX":
self.invex = value or "I"
elif key == "WHOX":
self.whox = True
elif key == "NICKLEN":
self.nicklen = int(value)

View File

@ -2,21 +2,20 @@ from typing import List, Optional
class ChanModes(object):
def __init__(self,
list_modes: List[str],
setting_b_modes: List[str],
setting_c_modes: List[str],
setting_d_modes: List[str]):
self.list_modes = list_modes
self.setting_b_modes = setting_b_modes
self.setting_c_modes = setting_c_modes
self.setting_d_modes = setting_d_modes
a_modes: List[str],
b_modes: List[str],
c_modes: List[str],
d_modes: List[str]):
self.a_modes = a_modes
self.b_modes = b_modes
self.c_modes = c_modes
self.d_modes = d_modes
class Prefix(object):
def __init__(self,
modes: List[str],
prefixes: List[str]):
self.modes = modes
self.modes = modes
self.prefixes = prefixes
def from_mode(self, mode: str) -> Optional[str]:

View File

@ -1,4 +0,0 @@
from typing import Optional
class Named(object):
name: Optional[str] = None

7
ircstates/names.py Normal file
View File

@ -0,0 +1,7 @@
class Name(object):
def __init__(self,
normal: str,
folded: str):
self.normal = normal
self.folded = folded

83
ircstates/numerics.py Normal file
View File

@ -0,0 +1,83 @@
RPL_WELCOME = "001"
RPL_ISUPPORT = "005"
RPL_MOTD = "372"
RPL_MOTDSTART = "375"
RPL_ENDOFMOTD = "376"
ERR_NOMOTD = "422"
RPL_UMODEIS = "221"
RPL_VISIBLEHOST = "396"
RPL_TRYAGAIN = "263"
RPL_YOUREOPER = "381"
ERR_NOSUCHNICK = "401"
ERR_NOSUCHSERVER = "402"
RPL_CHANNELMODEIS = "324"
RPL_CREATIONTIME = "329"
RPL_TOPIC = "332"
RPL_TOPICWHOTIME = "333"
RPL_WHOREPLY = "352"
RPL_WHOSPCRPL = "354"
RPL_ENDOFWHO = "315"
RPL_NAMREPLY = "353"
RPL_ENDOFNAMES = "366"
RPL_WHOWASUSER = "314"
RPL_ENDOFWHOWAS = "369"
RPL_BANLIST = "367"
RPL_ENDOFBANLIST = "368"
RPL_QUIETLIST = "728"
RPL_ENDOFQUIETLIST = "729"
RPL_LOGGEDIN = "900"
RPL_LOGGEDOUT = "901"
RPL_SASLSUCCESS = "903"
ERR_SASLFAIL = "904"
ERR_SASLTOOLONG = "905"
ERR_SASLABORTED = "906"
ERR_SASLALREADY = "907"
RPL_SASLMECHS = "908"
RPL_WHOISUSER = "311"
RPL_WHOISSERVER = "312"
RPL_WHOISOPERATOR = "313"
RPL_WHOISIDLE = "317"
RPL_WHOISCHANNELS = "319"
RPL_WHOISACCOUNT = "330"
RPL_WHOISHOST = "378"
RPL_WHOISMODES = "379"
RPL_WHOISSECURE = "671"
RPL_AWAY = "301"
RPL_ENDOFWHOIS = "318"
ERR_ERRONEUSNICKNAME = "432"
ERR_NICKNAMEINUSE = "433"
ERR_BANNICKCHANGE = "435"
ERR_UNAVAILRESOURCE = "437"
ERR_NICKTOOFAST = "438"
ERR_CANTCHANGENICK = "447"
ERR_NOSUCHCHANNEL = "403"
ERR_TOOMANYCHANNELS = "405"
ERR_USERONCHANNEL = "443"
ERR_LINKCHANNEL = "470"
ERR_BADCHANNAME = "479"
ERR_BADCHANNEL = "926"
ERR_BANNEDFROMCHAN = "474"
ERR_INVITEONLYCHAN = "473"
ERR_BADCHANNELKEY = "475"
ERR_CHANNELISFULL = "471"
ERR_NEEDREGGEDNICK = "477"
ERR_THROTTLE = "480"
RPL_LOGOFF = "601"
RPL_MONONLINE = "730"
RPL_MONOFFLINE = "731"
RPL_RSACHALLENGE2 = "740"
RPL_ENDOFRSACHALLENGE2 = "741"

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,48 @@
from typing import Optional, Set
from .named import Named
from .names import Name
class User(object):
def __init__(self, nickname: Name):
self._nickname = nickname
class User(Named):
def __init__(self, nickname: str):
self.nickname = nickname
self.username: Optional[str] = None
self.hostname: Optional[str] = None
self.realname: Optional[str] = None
self.account: Optional[str] = None
self.server: Optional[str] = None
self.away: Optional[str] = None
self.ip: Optional[str] = None
self.channels: Set[str] = set([])
def __repr__(self) -> str:
return f"User(nickname={self.nickname!r})"
def set_nickname(self, nickname: str,
nickname_lower: str):
self.nickname = nickname
self.name = nickname_lower
def get_name(self) -> Name:
return self._nickname
@property
def nickname(self) -> str:
return self._nickname.normal
@property
def nickname_lower(self) -> str:
return self._nickname.folded
def change_nickname(self,
normal: str,
folded: str):
self._nickname.normal = normal
self._nickname.folded = folded
def hostmask(self) -> str:
hostmask = self.nickname
if self.username is not None:
hostmask += f"!{self.username}"
if self.hostname is not None:
hostmask += f"@{self.hostname}"
return hostmask
def userhost(self) -> Optional[str]:
if (self.username is not None and
self.hostname is not None):
return f"{self.username}@{self.hostname}"
else:
return None

2
requirements-dev.txt Normal file
View File

@ -0,0 +1,2 @@
-r requirements.txt
freezegun ~=1.1.0

View File

@ -1 +1,2 @@
irctokens ==0.7.2
irctokens ~=2.0.2
pendulum ~=2.1.0

View File

@ -17,15 +17,15 @@ setuptools.setup(
long_description_content_type="text/markdown",
url="https://github.com/jesopo/ircstates",
packages=setuptools.find_packages(),
package_data={"irctokens": ["py.typed"]},
package_data={"ircstates": ["py.typed"]},
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Operating System :: POSIX",
"Operating System :: Microsoft :: Windows",
"Topic :: Communications :: Chat :: Internet Relay Chat"
],
python_requires='>=3.6',
python_requires='>=3.7',
install_requires=install_requires
)

View File

@ -1,4 +1,10 @@
from .channel import *
from .user import *
from .mode import *
from .motd import *
from .channel import *
from .user import *
from .mode import *
from .motd import *
from .cap import *
from .isupport import *
from .casemap import *
from .emit import *
from .who import *
from .sasl import *

90
test/cap.py Normal file
View File

@ -0,0 +1,90 @@
import unittest
import ircstates, irctokens
class CapTestLS(unittest.TestCase):
def test_one_line(self):
server = ircstates.Server("test")
self.assertFalse(server.has_cap)
self.assertEqual(server.available_caps, {})
server.parse_tokens(irctokens.tokenise("CAP * LS :a b"))
self.assertEqual(server.available_caps, {"a": "", "b": ""})
def test_two_lines(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("CAP * LS * :a b"))
self.assertEqual(server.available_caps, {})
server.parse_tokens(irctokens.tokenise("CAP * LS :c"))
self.assertEqual(server.available_caps, {"a": "", "b": "", "c": ""})
def test_values(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("CAP * LS :a b= c=1"))
self.assertEqual(server.available_caps, {"a": "", "b": "", "c": "1"})
class CapTestACK(unittest.TestCase):
def test_one_line(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("CAP * LS :a b"))
server.parse_tokens(irctokens.tokenise("CAP * ACK :a b"))
self.assertEqual(server.agreed_caps, ["a", "b"])
def test_two_lines(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("CAP * LS :a b c"))
server.parse_tokens(irctokens.tokenise("CAP * ACK * :a b"))
server.parse_tokens(irctokens.tokenise("CAP * ACK :c"))
self.assertEqual(server.agreed_caps, ["a", "b", "c"])
def test_not_ls(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("CAP * LS a"))
server.parse_tokens(irctokens.tokenise("CAP * ACK b"))
self.assertEqual(server.agreed_caps, [])
class CapTestNEW(unittest.TestCase):
def test_no_ls(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("CAP * NEW :a"))
self.assertEqual(server.available_caps, {"a": ""})
def test_one(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("CAP * LS :a"))
server.parse_tokens(irctokens.tokenise("CAP * NEW :b"))
self.assertEqual(server.available_caps, {"a": "", "b": ""})
def test_two(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("CAP * LS :a"))
server.parse_tokens(irctokens.tokenise("CAP * NEW :b c"))
self.assertEqual(server.available_caps, {"a": "", "b": "", "c": ""})
class CapTestDEL(unittest.TestCase):
def test_not_acked(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("CAP * DEL a"))
def test_one_ls(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("CAP * LS :a"))
server.parse_tokens(irctokens.tokenise("CAP * ACK :a"))
server.parse_tokens(irctokens.tokenise("CAP * DEL :a"))
self.assertEqual(server.available_caps, {})
self.assertEqual(server.agreed_caps, [])
def test_two_ls(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("CAP * LS :a b"))
server.parse_tokens(irctokens.tokenise("CAP * ACK :a b"))
server.parse_tokens(irctokens.tokenise("CAP * DEL :a"))
self.assertEqual(server.available_caps, {"b": ""})
self.assertEqual(server.agreed_caps, ["b"])
def test_two_del(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("CAP * LS :a b"))
server.parse_tokens(irctokens.tokenise("CAP * ACK :a b"))
server.parse_tokens(irctokens.tokenise("CAP * DEL :a b"))
self.assertEqual(server.available_caps, {})
self.assertEqual(server.agreed_caps, [])

40
test/casemap.py Normal file
View File

@ -0,0 +1,40 @@
import unittest
import ircstates, irctokens
class CaseMapTestMethod(unittest.TestCase):
def test_rfc1459(self):
lower = ircstates.casefold(ircstates.CaseMap.RFC1459, "ÀTEST[]^\\")
self.assertEqual(lower, "Àtest{}~|")
def test_ascii(self):
lower = ircstates.casefold(ircstates.CaseMap.ASCII, "ÀTEST[]~\\")
self.assertEqual(lower, "Àtest[]~\\")
class CaseMapTestCommands(unittest.TestCase):
def test_join(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":Nickname JOIN #Chan"))
server.parse_tokens(irctokens.tokenise(":Other JOIN #Chan"))
self.assertIn("nickname", server.users)
self.assertNotIn("Nickname", server.users)
self.assertIn("other", server.users)
self.assertNotIn("Other", server.users)
self.assertIn("#chan", server.channels)
self.assertNotIn("#Chan", server.channels)
channel = server.channels["#chan"]
self.assertEqual(channel.name, "#Chan")
def test_nick(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
user = server.users["nickname"]
server.parse_tokens(irctokens.tokenise(":nickname NICK NewNickname"))
self.assertEqual(len(server.users), 1)
self.assertIn("newnickname", server.users)
self.assertEqual(user.nickname, "NewNickname")
self.assertEqual(user.nickname_lower, "newnickname")
self.assertEqual(server.nickname, "NewNickname")
self.assertEqual(server.nickname_lower, "newnickname")

View File

@ -1,122 +1,203 @@
import unittest
from datetime import datetime
import pendulum
import ircstates, irctokens
from freezegun import freeze_time
class ChannelTestJoin(unittest.TestCase):
def test_self_join(self):
dt = pendulum.datetime(2021, 9, 6, 2, 55, 22)
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
with freeze_time("2021-09-06 02:55:22"):
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
self.assertIn("#chan", server.channels)
self.assertIn("nickname", server.users)
self.assertEqual(len(server.users), 1)
self.assertEqual(len(server.channels), 1)
self.assertIn(server.channels["#chan"], server.channel_users)
self.assertIn(server.users["nickname"], server.user_channels)
self.assertEqual(len(server.user_channels), 1)
self.assertEqual(len(server.channel_users), 1)
user = server.users["nickname"]
channel = server.channels["#chan"]
self.assertIn(user.nickname_lower, channel.users)
channel_user = channel.users[user.nickname_lower]
self.assertEqual(user.channels, set([channel.name_lower]))
self.assertEqual(channel_user.since, dt)
self.assertEqual(channel_user.joined, dt)
def test_other_join(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
self.assertEqual(len(server.users), 2)
self.assertIn("other", server.users)
self.assertEqual(len(server.user_channels), 2)
self.assertEqual(len(server.channel_users), 1)
channel = server.channels["#chan"]
self.assertEqual(len(channel.users), 2)
user = server.users["other"]
self.assertEqual(user.channels, set([channel.name_lower]))
class ChannelTestPart(unittest.TestCase):
def test_self_part(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":nickname PART #chan"))
self.assertEqual(len(server.users), 0)
self.assertEqual(len(server.channels), 0)
self.assertEqual(len(server.user_channels), 0)
self.assertEqual(len(server.channel_users), 0)
def test_other_part(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":other PART #chan"))
user = server.users["nickname"]
channel = server.channels["#chan"]
channel_user = channel.users[user.nickname_lower]
self.assertEqual(len(server.users), 1)
self.assertEqual(len(server.channels), 1)
self.assertIn(user, server.user_channels)
self.assertEqual(len(server.user_channels[user]), 1)
self.assertIn(channel, server.channel_users)
self.assertEqual(len(server.channel_users), 1)
self.assertIn(user, server.channel_users[channel])
self.assertEqual(len(server.user_channels), 1)
self.assertEqual(server.users, {"nickname": user})
self.assertEqual(server.channels, {"#chan": channel})
self.assertEqual(user.channels, set([channel.name_lower]))
self.assertEqual(channel.users, {"nickname": channel_user})
class ChannelTestKick(unittest.TestCase):
def test_self_kick(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("KICK #chan nickname"))
server.parse_tokens(
irctokens.tokenise(":nickname KICK #chan nickname"))
self.assertEqual(len(server.users), 0)
self.assertEqual(len(server.channels), 0)
self.assertEqual(len(server.user_channels), 0)
self.assertEqual(len(server.channel_users), 0)
def test_other_kick(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
server.parse_tokens(irctokens.tokenise("KICK #chan other"))
server.parse_tokens(irctokens.tokenise(":nickname KICK #chan other"))
user = server.users["nickname"]
channel = server.channels["#chan"]
channel_user = channel.users[user.nickname_lower]
self.assertEqual(len(server.users), 1)
self.assertEqual(len(server.channels), 1)
self.assertIn(user, server.user_channels)
self.assertEqual(len(server.user_channels[user]), 1)
self.assertIn(channel, server.channel_users)
self.assertEqual(len(server.channel_users), 1)
self.assertIn(user, server.channel_users[channel])
self.assertEqual(len(server.user_channels), 1)
self.assertEqual(user.channels, set([channel.name_lower]))
self.assertEqual(channel.users, {user.nickname_lower: channel_user})
class ChannelTestTopic(unittest.TestCase):
def test_text(self):
dt = pendulum.datetime(2020, 3, 12, 14, 27, 57)
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("332 * #chan :test"))
self.assertEqual(server.channels["#chan"].topic, "test")
def test_set_by_at(self):
dt = datetime(2020, 3, 12, 14, 27, 57)
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("333 * #chan other 1584023277"))
channel = server.channels["#chan"]
self.assertEqual(channel.topic, "test")
self.assertEqual(channel.topic_setter, "other")
self.assertEqual(channel.topic_time, dt)
def test_topic_command(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("TOPIC #chan :hello there"))
self.assertEqual(server.channels["#chan"].topic, "hello there")
dt = pendulum.datetime(2021, 9, 6, 2, 43, 22)
with freeze_time("2021-09-06 02:43:22"):
server.parse_tokens(irctokens.tokenise(":other TOPIC #chan :hello there"))
channel = server.channels["#chan"]
self.assertEqual(channel.topic, "hello there")
self.assertEqual(channel.topic_setter, "other")
self.assertEqual(channel.topic_time, dt)
class ChannelTestCreation(unittest.TestCase):
def test(self):
dt = datetime(2020, 3, 12, 19, 38, 9)
dt = pendulum.datetime(2020, 3, 12, 19, 38, 9)
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("329 * #chan 1584041889"))
self.assertEqual(server.channels["#chan"].created, dt)
class ChannelTestNAMES(unittest.TestCase):
def test(self):
dt_1 = pendulum.datetime(2021, 9, 6, 2, 57, 22)
dt_2 = pendulum.datetime(2021, 9, 6, 2, 58, 22)
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
with freeze_time("2021-09-06 02:57:22"):
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
with freeze_time("2021-09-06 02:58:22"):
server.parse_tokens(irctokens.tokenise("353 * * #chan :nickname @+other"))
self.assertIn("nickname", server.users)
self.assertIn("other", server.users)
user = server.users["other"]
channel = server.channels["#chan"]
channel_user_1 = channel.users[server.nickname_lower]
channel_user_2 = channel.users[user.nickname_lower]
self.assertEqual(channel.users, {
server.nickname_lower: channel_user_1,
user.nickname_lower: channel_user_2
})
self.assertEqual(user.channels, set([channel.name_lower]))
self.assertEqual(channel_user_2.modes, {"o", "v"})
self.assertEqual(channel_user_1.since, dt_1)
self.assertEqual(channel_user_2.since, dt_2)
def test_userhost_in_names(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise(
"353 * * #chan :nickname!user@host other!user2@host2"))
self.assertEqual(server.username, "user")
self.assertEqual(server.hostname, "host")
user = server.users["other"]
self.assertEqual(user.username, "user2")
self.assertEqual(user.hostname, "host2")
class ChannelNICKAfterJoin(unittest.TestCase):
def test(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
user = server.users["nickname"]
channel = server.channels["#chan"]
channel_user = channel.users[user.nickname_lower]
server.parse_tokens(irctokens.tokenise(":nickname NICK Nickname2"))
self.assertEqual(channel.users, {user.nickname_lower: channel_user})
self.assertEqual(channel_user.nickname, "Nickname2")
self.assertEqual(channel_user.nickname_lower, "nickname2")
class ChannelRENAME(unittest.TestCase):
def test(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
user = server.users["nickname"]
channel = server.channels["#chan"]
server.parse_tokens(irctokens.tokenise(":nickname RENAME #chan #chan2 *"))
self.assertEqual(channel.name, "#chan2")
self.assertEqual(set(channel.users.keys()), {"nickname", "other"})
self.assertEqual(user.channels, {"#chan2"})
self.assertNotIn("#chan", server.channels)
self.assertIn("#chan2", server.channels)
self.assertEqual(len(server.channels), 1)

106
test/emit.py Normal file
View File

@ -0,0 +1,106 @@
import unittest
import ircstates, irctokens
class EmitTest(unittest.TestCase):
def test_join(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
emit = server.parse_tokens(
irctokens.tokenise(":nickname JOIN #chan"))
self.assertEqual(emit.command, "JOIN")
self.assertEqual(emit.self, True)
self.assertEqual(emit.user, server.users["nickname"])
self.assertEqual(emit.channel, server.channels["#chan"])
emit = server.parse_tokens(
irctokens.tokenise(":other JOIN #chan"))
self.assertIsNotNone(emit)
self.assertEqual(emit.command, "JOIN")
self.assertEqual(emit.self, None)
self.assertEqual(emit.user, server.users["other"])
self.assertEqual(emit.channel, server.channels["#chan"])
def test_privmsg(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
emit = server.parse_tokens(
irctokens.tokenise(":nickname PRIVMSG #chan :hello"))
self.assertIsNotNone(emit)
self.assertEqual(emit.command, "PRIVMSG")
self.assertEqual(emit.text, "hello")
self.assertEqual(emit.self_source, True)
self.assertEqual(emit.user, server.users["nickname"])
self.assertEqual(emit.channel, server.channels["#chan"])
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
emit = server.parse_tokens(
irctokens.tokenise(":other PRIVMSG #chan :hello2"))
self.assertIsNotNone(emit)
self.assertEqual(emit.command, "PRIVMSG")
self.assertEqual(emit.text, "hello2")
self.assertEqual(emit.self_source, None)
self.assertEqual(emit.user, server.users["other"])
self.assertEqual(emit.channel, server.channels["#chan"])
def test_privmsg_nojoin(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
emit = server.parse_tokens(
irctokens.tokenise(":other PRIVMSG #chan :hello"))
self.assertIsNotNone(emit)
self.assertEqual(emit.command, "PRIVMSG")
self.assertEqual(emit.text, "hello")
self.assertEqual(emit.self_source, None)
self.assertIsNotNone(emit.user)
channel = server.channels["#chan"]
self.assertEqual(emit.channel, channel)
def test_kick(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
user = server.users["nickname"]
channel = server.channels["#chan"]
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
user_other = server.users["other"]
emit = server.parse_tokens(
irctokens.tokenise(":nickname KICK #chan other :reason"))
self.assertIsNotNone(emit)
self.assertEqual(emit.command, "KICK")
self.assertEqual(emit.text, "reason")
self.assertEqual(emit.self_source, True)
self.assertEqual(emit.user_source, user)
self.assertEqual(emit.user_target, user_other)
self.assertEqual(emit.channel, channel)
def test_mode_self(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
emit = server.parse_tokens(
irctokens.tokenise("MODE nickname x+i-i+wi-wi"))
self.assertIsNotNone(emit)
self.assertEqual(emit.command, "MODE")
self.assertTrue(emit.self_target)
self.assertEqual(emit.tokens,
["+x", "+i", "-i", "+w", "+i", "-w", "-i"])
def test_mode_channel(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
channel = server.channels["#chan"]
emit = server.parse_tokens(
irctokens.tokenise(":server MODE #chan +im-m+b-k asd!*@* key"))
self.assertIsNotNone(emit)
self.assertEqual(emit.command, "MODE")
self.assertEqual(emit.channel, channel)
self.assertEqual(emit.tokens,
["+i", "+m", "-m", "+b asd!*@*", "-k key"])

152
test/isupport.py Normal file
View File

@ -0,0 +1,152 @@
import unittest
import ircstates, irctokens
class ISUPPORTTest(unittest.TestCase):
def test_escape(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(r"005 * TEST=a\x20b\\x20\x2 *"))
self.assertEqual(server.isupport.raw["TEST"], r"a b\x20x2")
def test_chanmodes(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
self.assertEqual(server.isupport.chanmodes.a_modes, ["b"])
self.assertEqual(server.isupport.chanmodes.b_modes, ["k"])
self.assertEqual(server.isupport.chanmodes.c_modes, ["l"])
self.assertEqual(server.isupport.chanmodes.d_modes,
["i", "m", "n", "p", "s", "t"])
server.parse_tokens(irctokens.tokenise("005 * CHANMODES=a,b,c,d *"))
self.assertEqual(server.isupport.chanmodes.a_modes, ["a"])
self.assertEqual(server.isupport.chanmodes.b_modes, ["b"])
self.assertEqual(server.isupport.chanmodes.c_modes, ["c"])
self.assertEqual(server.isupport.chanmodes.d_modes, ["d"])
def test_prefix(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
self.assertEqual(server.isupport.prefix.modes, ["o", "v"])
self.assertEqual(server.isupport.prefix.prefixes, ["@", "+"])
self.assertEqual(server.isupport.prefix.from_mode("o"), "@")
self.assertIsNone(server.isupport.prefix.from_mode("a"))
self.assertEqual(server.isupport.prefix.from_prefix("@"), "o")
self.assertIsNone(server.isupport.prefix.from_prefix("&"))
server.parse_tokens(irctokens.tokenise("005 * PREFIX=(qaohv)~&@%+ *"))
self.assertEqual(server.isupport.prefix.modes,
["q", "a", "o", "h", "v"])
self.assertEqual(server.isupport.prefix.prefixes,
["~", "&", "@", "%", "+"])
self.assertEqual(server.isupport.prefix.from_mode("a"), "&")
self.assertEqual(server.isupport.prefix.from_prefix("&"), "a")
def test_chantypes(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
self.assertEqual(server.isupport.chantypes, ["#"])
server.parse_tokens(irctokens.tokenise("005 * CHANTYPES=#& *"))
self.assertEqual(server.isupport.chantypes, ["#", "&"])
def test_modes(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
self.assertEqual(server.isupport.modes, 3)
server.parse_tokens(irctokens.tokenise("005 * MODES *"))
self.assertEqual(server.isupport.modes, -1)
server.parse_tokens(irctokens.tokenise("005 * MODES=5 *"))
self.assertEqual(server.isupport.modes, 5)
def test_network(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
self.assertIsNone(server.isupport.network)
server.parse_tokens(irctokens.tokenise("005 * NETWORK=testnet *"))
self.assertEqual(server.isupport.network, "testnet")
def test_statusmsg(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
self.assertEqual(server.isupport.statusmsg, [])
server.parse_tokens(irctokens.tokenise("005 * STATUSMSG=&@ *"))
self.assertEqual(server.isupport.statusmsg, ["&", "@"])
def test_callerid(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
self.assertIsNone(server.isupport.callerid)
server.parse_tokens(irctokens.tokenise("005 * CALLERID=U *"))
self.assertEqual(server.isupport.callerid, "U")
server.parse_tokens(irctokens.tokenise("005 * CALLERID *"))
self.assertEqual(server.isupport.callerid, "g")
def test_excepts(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
self.assertIsNone(server.isupport.excepts)
server.parse_tokens(irctokens.tokenise("005 * EXCEPTS=U *"))
self.assertEqual(server.isupport.excepts, "U")
server.parse_tokens(irctokens.tokenise("005 * EXCEPTS *"))
self.assertEqual(server.isupport.excepts, "e")
def test_invex(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
self.assertIsNone(server.isupport.invex)
server.parse_tokens(irctokens.tokenise("005 * INVEX=U *"))
self.assertEqual(server.isupport.invex, "U")
server.parse_tokens(irctokens.tokenise("005 * INVEX *"))
self.assertEqual(server.isupport.invex, "I")
def test_whox(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
self.assertFalse(server.isupport.whox)
server.parse_tokens(irctokens.tokenise("005 * WHOX *"))
self.assertTrue(server.isupport.whox)
def test_monitor(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
self.assertIsNone(server.isupport.monitor)
server.parse_tokens(irctokens.tokenise("005 * MONITOR=123 *"))
self.assertEqual(server.isupport.monitor, 123)
server.parse_tokens(irctokens.tokenise("005 * MONITOR *"))
self.assertEqual(server.isupport.monitor, -1)
def test_watch(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
self.assertIsNone(server.isupport.watch)
server.parse_tokens(irctokens.tokenise("005 * WATCH=123 *"))
self.assertEqual(server.isupport.watch, 123)
server.parse_tokens(irctokens.tokenise("005 * WATCH *"))
self.assertEqual(server.isupport.watch, -1)
def test_nicklen(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
self.assertEqual(server.isupport.nicklen, 9)
server.parse_tokens(irctokens.tokenise("005 * NICKLEN=16 *"))
self.assertEqual(server.isupport.nicklen, 16)
class ISupportTestCasemapping(unittest.TestCase):
def test_rfc1459(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
self.assertEqual(server.isupport.casemapping, ircstates.CaseMap.RFC1459)
server.parse_tokens(irctokens.tokenise("005 * CASEMAPPING=rfc1459 *"))
self.assertEqual(server.isupport.casemapping, ircstates.CaseMap.RFC1459)
def test_ascii(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise("005 * CASEMAPPING=ascii *"))
self.assertEqual(server.isupport.casemapping, ircstates.CaseMap.ASCII)
def test_unknown(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
with self.assertRaises(ValueError):
server.parse_tokens(irctokens.tokenise("005 * CASEMAPPING=asd *"))

View File

@ -5,39 +5,108 @@ import ircstates, irctokens
class ModeTestUMode(unittest.TestCase):
def test_add(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise("MODE nickname +i"))
self.assertEqual(server.modes, ["i"])
self.assertEqual(server.modes, {"i"})
def test_remove(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise("MODE nickname +i"))
server.parse_tokens(irctokens.tokenise("MODE nickname -i"))
self.assertEqual(server.modes, [])
self.assertEqual(server.modes, set())
class ModeTestChannelPrefix(unittest.TestCase):
def test_add(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(
irctokens.tokenise("MODE #chan +ov nickname nickname"))
user = server.users["nickname"]
channel = server.channels["#chan"]
channel_user = channel.users[user.nickname_lower]
self.assertEqual(channel_user.modes, {"o", "v"})
def test_remove(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(
irctokens.tokenise("MODE #chan +ov nickname nickname"))
server.parse_tokens(
irctokens.tokenise("MODE #chan -ov nickname nickname"))
user = server.users["nickname"]
channel = server.channels["#chan"]
channel_user = channel.users[user.nickname_lower]
self.assertEqual(channel_user.modes, set())
class ModeTestChannelList(unittest.TestCase):
def test_add(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("MODE #chan +b asd!*@*"))
channel = server.channels["#chan"]
self.assertEqual(channel.list_modes, {"b": []})
server.parse_tokens(irctokens.tokenise("MODE #chan +b asd!*@*"))
self.assertEqual(channel.list_modes, {"b": ["asd!*@*"]})
server.parse_tokens(irctokens.tokenise("MODE #chan -b asd!*@*"))
self.assertEqual(channel.list_modes, {"b": []})
def test_remove(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("MODE #chan +b asd!*@*"))
server.parse_tokens(irctokens.tokenise("MODE #chan +b dsa!*@*"))
server.parse_tokens(irctokens.tokenise("MODE #chan -b asd!*@*"))
channel = server.channels["#chan"]
self.assertEqual(channel.list_modes, {})
self.assertEqual(channel.list_modes, {"b": ["dsa!*@*"]})
def test_banlist(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(
irctokens.tokenise("367 * #chan *!*@host setby 1594477713"))
server.parse_tokens(
irctokens.tokenise("367 * #chan $a:account setby 1594477713"))
server.parse_tokens(
irctokens.tokenise("367 * #chan r:my*gecos"))
server.parse_tokens(irctokens.tokenise("368 * #chan *"))
channel = server.channels["#chan"]
self.assertEqual(
channel.list_modes["b"],
["*!*@host", "$a:account", "r:my*gecos"]
)
def test_quietlist(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(
irctokens.tokenise("728 * #chan q q!*@host setby 1594477713"))
server.parse_tokens(
irctokens.tokenise("728 * #chan q $a:qaccount setby 1594477713"))
server.parse_tokens(
irctokens.tokenise("728 * #chan q r:q*my*gecos setby 1594477713"))
server.parse_tokens(irctokens.tokenise("729 * #chan q *"))
channel = server.channels["#chan"]
self.assertEqual(
channel.list_modes["q"],
["q!*@host", "$a:qaccount", "r:q*my*gecos"]
)
class ModeTestChannelTypeB(unittest.TestCase):
def test_add(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("MODE #chan +k password"))
channel = server.channels["#chan"]
@ -45,17 +114,17 @@ class ModeTestChannelTypeB(unittest.TestCase):
def test_remove(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("MODE #chan +k password"))
server.parse_tokens(irctokens.tokenise("MODE #chan -k *"))
channel = server.channels["#chan"]
self.assertEqual(channel.list_modes, {})
self.assertEqual(channel.modes, {})
class ModeTestChannelTypeC(unittest.TestCase):
def test_add(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("MODE #chan +l 100"))
channel = server.channels["#chan"]
@ -63,17 +132,17 @@ class ModeTestChannelTypeC(unittest.TestCase):
def test_remove(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("MODE #chan +l 100"))
server.parse_tokens(irctokens.tokenise("MODE #chan -l"))
channel = server.channels["#chan"]
self.assertEqual(channel.list_modes, {})
self.assertEqual(channel.modes, {})
class ModeTestChannelTypeD(unittest.TestCase):
def test_add(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("MODE #chan +i"))
channel = server.channels["#chan"]
@ -81,17 +150,17 @@ class ModeTestChannelTypeD(unittest.TestCase):
def test_remove(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("MODE #chan +i"))
server.parse_tokens(irctokens.tokenise("MODE #chan -i"))
channel = server.channels["#chan"]
self.assertEqual(channel.list_modes, {})
self.assertEqual(channel.modes, {})
class ModeTestChannelNumeric(unittest.TestCase):
def test(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(
irctokens.tokenise("324 * #chan +bkli *!*@* pass 10"))
@ -101,7 +170,7 @@ class ModeTestChannelNumeric(unittest.TestCase):
def test_without_plus(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("324 * #chan il 10"))
channel = server.channels["#chan"]
@ -110,12 +179,12 @@ class ModeTestChannelNumeric(unittest.TestCase):
class ModeTestUserNumeric(unittest.TestCase):
def test(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("211 * +iw"))
self.assertEqual(server.modes, ["i", "w"])
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise("221 * +iw"))
self.assertEqual(server.modes, {"i", "w"})
def test_without_plus(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("211 * iw"))
self.assertEqual(server.modes, ["i", "w"])
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise("221 * iw"))
self.assertEqual(server.modes, {"i", "w"})

View File

@ -4,7 +4,7 @@ import ircstates, irctokens
class MOTDTest(unittest.TestCase):
def test(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise("375 * :start of motd"))
server.parse_tokens(irctokens.tokenise("372 * :first line of motd"))
server.parse_tokens(irctokens.tokenise("372 * :second line of motd"))

22
test/sasl.py Normal file
View File

@ -0,0 +1,22 @@
import unittest
import ircstates, irctokens
class SASLTestAccount(unittest.TestCase):
def test_loggedin(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("900 * nick!user@host account *"))
self.assertEqual(server.nickname, "nick")
self.assertEqual(server.username, "user")
self.assertEqual(server.hostname, "host")
self.assertEqual(server.account, "account")
def test_loggedout(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("900 * nick!user@host account *"))
server.parse_tokens(irctokens.tokenise("901 * nick1!user1@host1 *"))
self.assertEqual(server.nickname, "nick1")
self.assertEqual(server.username, "user1")
self.assertEqual(server.hostname, "host1")
self.assertEqual(server.account, None)

View File

@ -2,107 +2,135 @@ import unittest
import ircstates, irctokens
class UserTestNicknameChange(unittest.TestCase):
def test_self_change(self):
def test(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise(":nickname NICK nickname2"))
self.assertEqual(server.nickname, "nickname2")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname NICK Nickname2"))
self.assertEqual(server.nickname, "Nickname2")
self.assertEqual(server.nickname_lower, "nickname2")
def test_other_change(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":nickname2 JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
self.assertIn("other", server.users)
user = server.users["other"]
server.parse_tokens(irctokens.tokenise(":other NICK other2"))
server.parse_tokens(irctokens.tokenise(":other NICK Other2"))
self.assertNotIn("other", server.users)
self.assertIn("other2", server.users)
class UserTestUserHostSelf(unittest.TestCase):
self.assertEqual(user.nickname, "Other2")
self.assertEqual(user.nickname_lower, "other2")
class UserTestHostmaskJoin(unittest.TestCase):
def test_both(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(
irctokens.tokenise(":nickname!user@host JOIN #chan"))
self.assertEqual(server.username, "user")
self.assertEqual(server.hostname, "host")
def test_user(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise(":nickname!user JOIN #chan"))
self.assertEqual(server.username, "user")
self.assertIsNone(server.hostname)
def test_host(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise(":nickname@host JOIN #chan"))
self.assertIsNone(server.username)
self.assertEqual(server.hostname, "host")
class UserTestUserHostOther(unittest.TestCase):
def test_both(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":other!user@host JOIN #chan"))
user = server.users["other"]
self.assertEqual(user.username, "user")
self.assertEqual(user.hostname, "host")
self.assertEqual(user.userhost(), "user@host")
self.assertEqual(user.hostmask(), "other!user@host")
def test_user(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname!user JOIN #chan"))
self.assertEqual(server.username, "user")
self.assertIsNone(server.hostname)
server.parse_tokens(irctokens.tokenise(":other!user JOIN #chan"))
user = server.users["other"]
self.assertEqual(user.username, "user")
self.assertIsNone(user.hostname)
self.assertIsNone(user.userhost())
self.assertEqual(user.hostmask(), "other!user")
def test_host(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname@host JOIN #chan"))
self.assertIsNone(server.username)
self.assertEqual(server.hostname, "host")
server.parse_tokens(irctokens.tokenise(":other@host JOIN #chan"))
user = server.users["other"]
self.assertIsNone(user.username)
self.assertEqual(user.hostname, "host")
self.assertIsNone(user.userhost())
self.assertEqual(user.hostmask(), "other@host")
class UserTestPRIVMSGSelfHostmask(unittest.TestCase):
class UserTestExtendedJoin(unittest.TestCase):
def test_without_extended_join(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
self.assertIsNone(server.account)
self.assertIsNone(server.realname)
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
user = server.users["other"]
self.assertIsNone(user.account)
self.assertIsNone(user.realname)
def test_with_account(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan acc :realname"))
self.assertEqual(server.account, "acc")
self.assertEqual(server.realname, "realname")
server.parse_tokens(irctokens.tokenise(":other JOIN #chan acc2 :realname2"))
user = server.users["other"]
self.assertEqual(user.account, "acc2")
self.assertEqual(user.realname, "realname2")
def test_without_account(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan * :realname"))
self.assertEqual(server.account, "")
self.assertEqual(server.realname, "realname")
server.parse_tokens(irctokens.tokenise(":other JOIN #chan * :realname2"))
user = server.users["other"]
self.assertEqual(user.account, "")
self.assertEqual(user.realname, "realname2")
class UserTestAccountNotify(unittest.TestCase):
def test_with_account(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":nickname ACCOUNT acc"))
self.assertEqual(server.account, "acc")
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":other ACCOUNT acc2"))
user = server.users["other"]
self.assertEqual(user.account, "acc2")
def test_without_account(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":nickname ACCOUNT *"))
self.assertEqual(server.account, "")
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":other ACCOUNT *"))
user = server.users["other"]
self.assertEqual(user.account, "")
class UserTestHostmaskPRIVMSG(unittest.TestCase):
def test_both(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(
irctokens.tokenise(":nickname!user@host PRIVMSG #chan :hi"))
self.assertEqual(server.username, "user")
self.assertEqual(server.hostname, "host")
def test_user(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(
irctokens.tokenise(":nickname!user PRIVMSG #chan :hi"))
self.assertEqual(server.username, "user")
self.assertIsNone(server.hostname)
def test_host(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(
irctokens.tokenise(":nickname@host PRIVMSG #chan :hi"))
self.assertIsNone(server.username)
self.assertEqual(server.hostname, "host")
class UserTestPRIVMSGOtherHostmask(unittest.TestCase):
def test_both(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
server.parse_tokens(
irctokens.tokenise(":other!user@host PRIVMSG #chan :hi"))
@ -112,8 +140,12 @@ class UserTestPRIVMSGOtherHostmask(unittest.TestCase):
def test_user(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(
irctokens.tokenise(":nickname!user PRIVMSG #chan :hi"))
self.assertEqual(server.username, "user")
self.assertIsNone(server.hostname)
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
server.parse_tokens(
irctokens.tokenise(":other!user PRIVMSG #chan :hi"))
@ -123,8 +155,12 @@ class UserTestPRIVMSGOtherHostmask(unittest.TestCase):
def test_host(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(
irctokens.tokenise(":nickname@host PRIVMSG #chan :hi"))
self.assertIsNone(server.username)
self.assertEqual(server.hostname, "host")
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
server.parse_tokens(
irctokens.tokenise(":other@host PRIVMSG #chan :hi"))
@ -135,14 +171,122 @@ class UserTestPRIVMSGOtherHostmask(unittest.TestCase):
class UserTestVisibleHost(unittest.TestCase):
def test_without_username(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("396 * hostname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise("396 * hostname *"))
self.assertIsNone(server.username)
self.assertEqual(server.hostname, "hostname")
def test_with_username(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname"))
server.parse_tokens(irctokens.tokenise("396 * username@hostname"))
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise("396 * username@hostname *"))
self.assertEqual(server.username, "username")
self.assertEqual(server.hostname, "hostname")
class UserTestWHO(unittest.TestCase):
def test(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
server.parse_tokens(
irctokens.tokenise("352 * #chan user host * nickname * :0 real"))
server.parse_tokens(
irctokens.tokenise("352 * #chan user2 host2 * other * :0 real2"))
self.assertEqual(server.username, "user")
self.assertEqual(server.hostname, "host")
self.assertEqual(server.realname, "real")
user = server.users["other"]
self.assertEqual(user.username, "user2")
self.assertEqual(user.hostname, "host2")
self.assertEqual(user.realname, "real2")
class UserTestCHGHOST(unittest.TestCase):
def test(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(
irctokens.tokenise(":nickname!user@host JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":nickname CHGHOST u h"))
self.assertEqual(server.username, "u")
self.assertEqual(server.hostname, "h")
server.parse_tokens(
irctokens.tokenise(":other!user2@host2 JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":other CHGHOST u2 h2"))
user = server.users["other"]
self.assertEqual(user.username, "u2")
self.assertEqual(user.hostname, "h2")
class UserTestWHOIS(unittest.TestCase):
def test_user_line(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise("311 * nickname u h * :r"))
self.assertEqual(server.username, "u")
self.assertEqual(server.hostname, "h")
self.assertEqual(server.realname, "r")
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
server.parse_tokens(irctokens.tokenise("311 * other u2 h2 * :r2"))
user = server.users["other"]
self.assertEqual(user.username, "u2")
self.assertEqual(user.hostname, "h2")
self.assertEqual(user.realname, "r2")
class UserTestAway(unittest.TestCase):
def test_verb_set(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
user = server.users["nickname"]
self.assertIsNone(server.away)
self.assertIsNone(user.away)
server.parse_tokens(irctokens.tokenise(":nickname AWAY :ik ga weg"))
self.assertEqual(server.away, "ik ga weg")
self.assertEqual(user.away, "ik ga weg")
def test_verb_unset(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
user = server.users["nickname"]
server.parse_tokens(irctokens.tokenise(
":nickname AWAY :let's blow this popsicle stand"))
server.parse_tokens(irctokens.tokenise(":nickname AWAY"))
self.assertIsNone(server.away)
self.assertIsNone(user.away)
def test_numeric(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
user = server.users["nickname"]
self.assertIsNone(server.away)
self.assertIsNone(user.away)
server.parse_tokens(irctokens.tokenise(
"301 * nickname :i saw something shiny"))
self.assertEqual(server.away, "i saw something shiny")
self.assertEqual(user.away, "i saw something shiny")
class UserTestSETNAME(unittest.TestCase):
def test(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
server.parse_tokens(irctokens.tokenise(":other JOIN #chan"))
user = server.users["other"]
self.assertIsNone(user.realname)
self.assertIsNone(server.realname)
server.parse_tokens(
irctokens.tokenise(":nickname SETNAME :new now know how"))
server.parse_tokens(
irctokens.tokenise(":other SETNAME :tyrannosaurus hex"))
self.assertEqual(server.realname, "new now know how")
self.assertEqual(user.realname, "tyrannosaurus hex")

93
test/who.py Normal file
View File

@ -0,0 +1,93 @@
import unittest
import ircstates, irctokens
from ircstates.server import WHO_TYPE
class WHOTest(unittest.TestCase):
def test_who(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
user = server.users["nickname"]
server.parse_tokens(irctokens.tokenise(
"352 * #chan user host server nickname * :0 real"))
self.assertEqual(user.username, "user")
self.assertEqual(user.hostname, "host")
self.assertEqual(user.realname, "real")
self.assertEqual(user.account, None)
self.assertEqual(user.server, "server")
self.assertIsNone(user.away)
self.assertEqual(server.username, user.username)
self.assertEqual(server.hostname, user.hostname)
self.assertEqual(server.realname, user.realname)
self.assertEqual(server.server, user.server)
self.assertIsNone(server.away)
server.parse_tokens(irctokens.tokenise(
"352 * #chan user host server nickname G* :0 real"))
self.assertEqual(user.away, "")
self.assertEqual(server.away, "")
def test_whox(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
user = server.users["nickname"]
server.parse_tokens(irctokens.tokenise(
f"354 * {WHO_TYPE} user 1.2.3.4 host server nickname * account :real"))
self.assertEqual(user.username, "user")
self.assertEqual(user.hostname, "host")
self.assertEqual(user.realname, "real")
self.assertEqual(user.account, "account")
self.assertEqual(user.server, "server")
self.assertIsNone(user.away)
self.assertEqual(user.ip, "1.2.3.4")
self.assertEqual(server.username, user.username)
self.assertEqual(server.hostname, user.hostname)
self.assertEqual(server.realname, user.realname)
self.assertEqual(server.account, user.account)
self.assertEqual(server.server, user.server)
self.assertIsNone(server.away)
self.assertEqual(server.ip, user.ip)
server.parse_tokens(irctokens.tokenise(
f"354 * {WHO_TYPE} user realip host server nickname G account :real"))
self.assertEqual(user.away, "")
self.assertEqual(server.away, "")
def test_whox_no_account(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
user = server.users["nickname"]
user.account = "account"
server.account = "account"
server.parse_tokens(irctokens.tokenise(
f"354 * {WHO_TYPE} user realip host server nickname * 0 :real"))
self.assertEqual(user.account, "")
self.assertEqual(server.account, user.account)
def test_whox_ipv6(self):
server = ircstates.Server("test")
server.parse_tokens(irctokens.tokenise("001 nickname *"))
server.parse_tokens(irctokens.tokenise(":nickname JOIN #chan"))
user = server.users["nickname"]
server.parse_tokens(irctokens.tokenise(
f"354 * {WHO_TYPE} user 0::1 host server nickname * 0 :real"))
self.assertEqual(user.ip, "::1")
server.parse_tokens(irctokens.tokenise(
f"354 * {WHO_TYPE} user 00::2 host server nickname * 0 :real"))
self.assertEqual(user.ip, "::2")
server.parse_tokens(irctokens.tokenise(
f"354 * {WHO_TYPE} user fd00:0:0:0::1 host server nickname * 0 :real"))
self.assertEqual(user.ip, "fd00::1")