55server_info.py
66"""
77import weakref
8- from contextlib import suppress
98from operator import attrgetter
109from weakref import WeakValueDictionary
1110
@@ -26,52 +25,49 @@ def __getitem__(self, item):
2625 return super ().__getitem__ (item .casefold ())
2726
2827 def __setitem__ (self , key , value ):
29- super ().__setitem__ (key .casefold (), value )
28+ return super ().__setitem__ (key .casefold (), value )
3029
3130 def __delitem__ (self , key ):
32- super ().__delitem__ (key .casefold ())
31+ return super ().__delitem__ (key .casefold ())
32+
33+ def pop (self , key , * args , ** kwargs ):
34+ return super ().pop (key .casefold (), * args , ** kwargs )
35+
36+ def get (self , key , default = None ):
37+ return super ().get (key , default )
38+
39+ def setdefault (self , key , default = None ):
40+ return super ().setdefault (key , default )
3341
3442
3543class KeyFoldDict (KeyFoldMixin , dict ):
3644 pass
3745
3846
3947class KeyFoldWeakValueDict (KeyFoldMixin , WeakValueDictionary ):
40- def pop (self , key , * args ):
41- return super ().pop (key .casefold (), * args )
42-
43- def get (self , key , default = None ):
44- return super ().get (key .casefold (), default = default )
45-
46- def setdefault (self , key , default = None ):
47- return super ().setdefault (key .casefold (), default = default )
48+ pass
4849
4950
5051class ChanDict (KeyFoldDict ):
51- __slots__ = ()
52-
53- def __missing__ ( self , key ):
54- data = WeakDict ( name = key , users = KeyFoldDict ())
55- self [key ] = data
56- return data
52+ def getchan ( self , name ):
53+ try :
54+ return self [ name ]
55+ except KeyError :
56+ self [name ] = value = WeakDict ( name = name , users = KeyFoldDict ())
57+ return value
5758
5859
5960class UsersDict (KeyFoldWeakValueDict ):
60- __slots__ = ()
61-
62- def __getitem__ (self , item ):
61+ def getuser (self , nick ):
6362 try :
64- return super (). __getitem__ ( item )
63+ return self [ nick ]
6564 except KeyError :
66- return self .__missing__ (item )
67-
68- def __missing__ (self , key ):
69- self [key ] = value = WeakDict (nick = key , channels = KeyFoldWeakValueDict ())
70- return value
65+ self [nick ] = value = WeakDict (nick = nick , channels = KeyFoldWeakValueDict ())
66+ return value
7167
7268
7369def update_chan_data (conn , chan ):
74- chan_data = conn .memory ["chan_data" ][ chan ]
70+ chan_data = conn .memory ["chan_data" ]. getchan ( chan )
7571 chan_data ["receiving_names" ] = False
7672 conn .cmd ("NAMES" , chan )
7773
@@ -81,11 +77,26 @@ def update_conn_data(conn):
8177 update_chan_data (conn , chan )
8278
8379
84- @hook .on_cap_available ("userhost-in-names" , "multi-prefix" )
80+ SUPPORTED_CAPS = frozenset ({
81+ "userhost-in-names" ,
82+ "multi-prefix" ,
83+ "extended-join" ,
84+ "account-notify" ,
85+ "away-notify" ,
86+ "chghost" ,
87+ })
88+
89+
90+ @hook .on_cap_available (* SUPPORTED_CAPS )
8591def do_caps ():
8692 return True
8793
8894
95+ def is_cap_available (conn , cap ):
96+ caps = conn .memory .get ("server_caps" , {})
97+ return bool (caps .get (cap , False ))
98+
99+
89100@hook .on_start
90101def get_chan_data (bot ):
91102 for conn in bot .connections .values ():
@@ -111,21 +122,19 @@ def init_chan_data(conn, _clear=True):
111122
112123
113124def add_user_membership (user , chan , membership ):
114- chans = user .setdefault ('channels' , KeyFoldWeakValueDict ())
115- chans [chan ] = membership
125+ user ["channels" ][chan ] = membership
116126
117127
118128def replace_user_data (conn , chan_data ):
119129 statuses = {status .prefix : status for status in set (conn .memory ["server_info" ]["statuses" ].values ())}
120130 users = conn .memory ["users" ]
121- old_users = chan_data [' users' ]
131+ old_users = chan_data [" users" ]
122132 new_data = chan_data .pop ("new_users" , [])
123133 new_users = KeyFoldDict ()
124- caps = conn .memory .get ("server_caps" , {})
125- has_uh_i_n = caps .get ("userhost-in-names" , False )
126- has_multi_pfx = caps .get ("multi-prefix" , False )
134+ has_uh_i_n = is_cap_available (conn , "userhost-in-names" )
135+ has_multi_pfx = is_cap_available (conn , "multi-prefix" )
127136 for name in new_data :
128- user_data = WeakDict ()
137+ user_data = WeakDict (channels = KeyFoldWeakValueDict () )
129138 memb_data = WeakDict (user = user_data , chan = weakref .proxy (chan_data ))
130139 user_statuses = []
131140 while name [:1 ] in statuses :
@@ -142,7 +151,7 @@ def replace_user_data(conn, chan_data):
142151
143152 if has_uh_i_n :
144153 pfx = Prefix .parse (name )
145- user_data .update ({ " nick" : pfx .nick , " ident" : pfx .user , " host" : pfx .host } )
154+ user_data .update (nick = pfx .nick , ident = pfx .user , host = pfx .host )
146155 else :
147156 user_data ["nick" ] = name
148157
@@ -153,7 +162,7 @@ def replace_user_data(conn, chan_data):
153162 old_data .update (memb_data ) # New data takes priority over old data
154163 memb_data .update (old_data )
155164
156- old_user_data = users .setdefault (nick , user_data )
165+ old_user_data = users .getuser (nick )
157166 old_user_data .update (user_data )
158167 user_data = old_user_data
159168 memb_data ["user" ] = user_data
@@ -166,7 +175,7 @@ def replace_user_data(conn, chan_data):
166175@hook .irc_raw (['353' , '366' ], singlethread = True )
167176def on_names (conn , irc_paramlist , irc_command ):
168177 chan = irc_paramlist [2 if irc_command == '353' else 1 ]
169- chan_data = conn .memory ["chan_data" ][ chan ]
178+ chan_data = conn .memory ["chan_data" ]. getchan ( chan )
170179 if irc_command == '366' :
171180 chan_data ["receiving_names" ] = False
172181 replace_user_data (conn , chan_data )
@@ -202,18 +211,20 @@ def dump_dict(data, indent=2, level=0, _objects=None):
202211
203212@hook .permission ("chanop" )
204213def perm_check (chan , conn , nick ):
205- if not chan :
214+ if not ( chan and conn ) :
206215 return False
207216
208217 chans = conn .memory ["chan_data" ]
209- if chan not in chans :
218+ try :
219+ chan_data = chans [chan ]
220+ except KeyError :
210221 return False
211222
212- chan_data = chans [chan ]
213- if nick not in chan_data ["users" ]:
223+ try :
224+ memb = chan_data ["users" ][nick ]
225+ except KeyError :
214226 return False
215227
216- memb = chan_data ["users" ][nick ]
217228 status = memb ["status" ]
218229 if status and status [0 ].level > 1 :
219230 return True
@@ -246,31 +257,34 @@ def updateusers(bot):
246257 return "Updating all channel data"
247258
248259
249- @hook .irc_raw (['JOIN' , 'MODE' ], singlethread = True )
250- def on_join_mode (chan , nick , user , host , conn , irc_command , irc_paramlist ):
251- """
252- Both JOIN and MODE are handled in one hook with Hook:singlethread=True
253- to ensure they are handled in order, avoiding a possible race condition
254- """
255- if irc_command == 'JOIN' :
256- return on_join (chan , nick , user , host , conn )
257- elif irc_command == 'MODE' :
258- return on_mode (chan , irc_paramlist , conn )
260+ @hook .irc_raw ('JOIN' )
261+ def on_join (nick , user , host , conn , irc_paramlist ):
262+ chan , * other_data = irc_paramlist
259263
260-
261- def on_join (chan , nick , user , host , conn ):
262264 if chan .startswith (':' ):
263265 chan = chan [1 :]
264266
267+ data = {'ident' : user , 'host' : host }
268+
269+ if is_cap_available (conn , "extended-join" ) and other_data :
270+ acct , realname = other_data
271+ if acct == "*" :
272+ acct = None
273+
274+ data .update (account = acct , realname = realname )
275+
265276 users = conn .memory ['users' ]
266- user_data = users [nick ]
267- user_data .update (user = user , host = host )
268- chan_data = conn .memory ["chan_data" ][chan ]
277+
278+ user_data = users .getuser (nick )
279+ user_data .update (data )
280+
281+ chan_data = conn .memory ["chan_data" ].getchan (chan )
269282 memb_data = WeakDict (chan = weakref .proxy (chan_data ), user = user_data , status = [])
270283 chan_data ["users" ][nick ] = memb_data
271284 add_user_membership (user_data , chan , memb_data )
272285
273286
287+ @hook .irc_raw ('MODE' )
274288def on_mode (chan , irc_paramlist , conn ):
275289 if chan .startswith (':' ):
276290 chan = chan [1 :]
@@ -280,11 +294,13 @@ def on_mode(chan, irc_paramlist, conn):
280294 status_modes = {status .mode for status in statuses .values ()}
281295 mode_types = serv_info ["channel_modes" ]
282296 chans = conn .memory ["chan_data" ]
283- if chan not in chans :
284- return
285297
286- chan_data = chans [chan ]
298+ try :
299+ chan_data = chans [chan ]
300+ except KeyError :
301+ return
287302
303+ chan_users = chan_data ["users" ]
288304 modes = irc_paramlist [1 ]
289305 mode_params = irc_paramlist [2 :]
290306 new_modes = {}
@@ -307,14 +323,14 @@ def on_mode(chan, irc_paramlist, conn):
307323 param = mode_params .pop (0 )
308324
309325 if is_status :
310- memb = chan_data [ "users" ] [param ]
326+ memb = chan_users [param ]
311327 status = statuses [c ]
328+ memb_status = memb ["status" ]
312329 if adding :
313- memb [ "status" ] .append (status )
314- memb [ "status" ] .sort (key = attrgetter ("level" ), reverse = True )
330+ memb_status .append (status )
331+ memb_status .sort (key = attrgetter ("level" ), reverse = True )
315332 else :
316- if status in memb ["status" ]:
317- memb ["status" ].remove (status )
333+ memb_status .remove (status )
318334
319335
320336@hook .irc_raw ('PART' )
@@ -324,12 +340,10 @@ def on_part(chan, nick, conn):
324340
325341 channels = conn .memory ["chan_data" ]
326342 if nick .casefold () == conn .nick .casefold ():
327- with suppress (KeyError ):
328- del channels [chan ]
343+ del channels [chan ]
329344 else :
330345 chan_data = channels [chan ]
331- with suppress (KeyError ):
332- del chan_data ["users" ][nick ]
346+ del chan_data ["users" ][nick ]
333347
334348
335349@hook .irc_raw ('KICK' )
@@ -344,8 +358,7 @@ def on_quit(nick, conn):
344358 user = users [nick ]
345359 for memb in user .get ("channels" , {}).values ():
346360 chan = memb ["chan" ]
347- with suppress (KeyError ):
348- del chan ["users" ][nick ]
361+ del chan ["users" ][nick ]
349362
350363
351364@hook .irc_raw ('NICK' )
@@ -357,8 +370,76 @@ def on_nick(nick, irc_paramlist, conn):
357370
358371 user = users .pop (nick )
359372 users [new_nick ] = user
360- user ["nick" ] = nick
373+ user ["nick" ] = new_nick
361374 for memb in user .get ("channels" , {}).values ():
362375 chan_users = memb ["chan" ]["users" ]
363- with suppress (KeyError ):
364- chan_users [new_nick ] = chan_users .pop (nick )
376+ chan_users [new_nick ] = chan_users .pop (nick )
377+
378+
379+ @hook .irc_raw ('ACCOUNT' )
380+ def on_account (conn , nick , irc_paramlist ):
381+ conn .memory ["users" ][nick ]["account" ] = irc_paramlist [0 ]
382+
383+
384+ @hook .irc_raw ('CHGHOST' )
385+ def on_chghost (conn , nick , irc_paramlist ):
386+ ident , host = irc_paramlist
387+ conn .memory ["users" ][nick ].update (ident = ident , host = host )
388+
389+
390+ @hook .irc_raw ('AWAY' )
391+ def on_away (conn , nick , irc_paramlist ):
392+ if irc_paramlist :
393+ reason = irc_paramlist [0 ]
394+ else :
395+ reason = None
396+
397+ conn .memory ["users" ][nick ].update (is_away = (reason is not None ), away_message = reason )
398+
399+
400+ @hook .irc_raw ('352' )
401+ def on_who (conn , irc_paramlist ):
402+ _ , ident , host , server , nick , status , realname = irc_paramlist
403+ realname = realname .split (None , 1 )[1 ]
404+ user = conn .memory ["users" ][nick ]
405+ status = list (status )
406+ is_away = status .pop (0 ) == "G"
407+ is_oper = status [:1 ] == "*"
408+ user .update (
409+ ident = ident ,
410+ host = host ,
411+ server = server ,
412+ realname = realname ,
413+ is_away = is_away ,
414+ is_oper = is_oper ,
415+ )
416+
417+
418+ @hook .irc_raw ('311' )
419+ def on_whois_name (conn , irc_paramlist ):
420+ _ , nick , ident , host , _ , realname = irc_paramlist
421+ conn .memory ["users" ][nick ].update (ident = ident , host = host , realname = realname )
422+
423+
424+ @hook .irc_raw ('330' )
425+ def on_whois_acct (conn , irc_paramlist ):
426+ _ , nick , acct = irc_paramlist [:2 ]
427+ conn .memory ["users" ][nick ]["account" ] = acct
428+
429+
430+ @hook .irc_raw ('301' )
431+ def on_whois_away (conn , irc_paramlist ):
432+ _ , nick , msg = irc_paramlist
433+ conn .memory ["users" ][nick ].update (is_away = True , away_message = msg )
434+
435+
436+ @hook .irc_raw ('312' )
437+ def on_whois_server (conn , irc_paramlist ):
438+ _ , nick , server , _ = irc_paramlist
439+ conn .memory ["users" ][nick ].update (server = server )
440+
441+
442+ @hook .irc_raw ('313' )
443+ def on_whois_oper (conn , irc_paramlist ):
444+ nick = irc_paramlist [1 ]
445+ conn .memory ["users" ][nick ].update (is_oper = True )
0 commit comments