44Requires:
55server_info.py
66"""
7- import re
87import weakref
98from collections import defaultdict
109from operator import attrgetter
1110from weakref import WeakValueDictionary
1211
1312from cloudbot import hook
14-
15- NUH_RE = re .compile (r'(?P<nick>.+?)(?:!(?P<user>.+?))?(?:@(?P<host>.+?))?' )
13+ from cloudbot .util .parsers .irc import Prefix
1614
1715
1816class WeakDict (dict ):
1917 # Subclass dict to allow it to be weakly referenced
2018 pass
2119
2220
23- class DataDict (defaultdict ):
24- def __init__ (self , * args , ** kwargs ):
25- super ().__init__ (* args , ** kwargs )
26-
21+ class KeyFoldMixin :
2722 def __contains__ (self , item ):
2823 return super ().__contains__ (item .casefold ())
2924
@@ -36,8 +31,18 @@ def __setitem__(self, key, value):
3631 def __delitem__ (self , key ):
3732 super ().__delitem__ (key .casefold ())
3833
34+
35+ class KeyFoldDict (dict , KeyFoldMixin ):
36+ pass
37+
38+
39+ class KeyFoldWeakValueDict (WeakValueDictionary , KeyFoldMixin ):
40+ pass
41+
42+
43+ class ChanDict (defaultdict , KeyFoldMixin ):
3944 def __missing__ (self , key ):
40- data = WeakDict ({ " name" : key , " users" : {}} )
45+ data = WeakDict (name = key , users = KeyFoldDict () )
4146 self [key ] = data
4247 return data
4348
@@ -55,7 +60,7 @@ def update_conn_data(conn):
5560
5661@hook .on_cap_available ("userhost-in-names" , "multi-prefix" )
5762def do_caps ():
58- pass
63+ return True
5964
6065
6166@hook .on_start
@@ -67,36 +72,30 @@ def get_chan_data(bot):
6772
6873@hook .connect
6974def init_chan_data (conn ):
70- chan_data = conn .memory .setdefault ("chan_data" , DataDict ())
75+ chan_data = conn .memory .setdefault ("chan_data" , ChanDict ())
7176 chan_data .clear ()
7277
73- users = conn .memory .setdefault ("users" , WeakValueDictionary ())
78+ users = conn .memory .setdefault ("users" , KeyFoldWeakValueDict ())
7479 users .clear ()
7580
7681
77- def parse_nuh (mask ):
78- match = NUH_RE .fullmatch (mask )
79- if not match :
80- return None , None , None
81-
82- nick = match .group ('nick' )
83- user = match .group ('user' )
84- host = match .group ('host' )
85- return nick , user , host
82+ def add_user_membership (user , chan , membership ):
83+ chans = user .setdefault ("channels" , KeyFoldWeakValueDict ())
84+ chans [chan ] = membership
8685
8786
8887def replace_user_data (conn , chan_data ):
8988 statuses = {status .prefix : status for status in set (conn .memory ["server_info" ]["statuses" ].values ())}
9089 users = conn .memory ["users" ]
9190 old_users = chan_data ['users' ]
9291 new_data = chan_data .pop ("new_users" , [])
93- new_users = {}
92+ new_users = KeyFoldDict ()
9493 caps = conn .memory .get ("server_caps" , {})
9594 has_uh_i_n = caps .get ("userhost-in-names" , False )
9695 has_multi_pfx = caps .get ("multi-prefix" , False )
9796 for name in new_data :
9897 user_data = WeakDict ()
99- memb_data = WeakDict ({ " user" : user_data , " chan" : weakref .proxy (chan_data )} )
98+ memb_data = WeakDict (user = user_data , chan = weakref .proxy (chan_data ))
10099 user_statuses = []
101100 while name [:1 ] in statuses :
102101 status , name = name [:1 ], name [1 :]
@@ -111,24 +110,23 @@ def replace_user_data(conn, chan_data):
111110 memb_data ["status" ] = user_statuses
112111
113112 if has_uh_i_n :
114- nick , user , host = parse_nuh (name )
115- user_data .update ({"nick" : nick , "ident" : user , "host" : host })
113+ pfx = Prefix . parse (name )
114+ user_data .update ({"nick" : pfx . nick , "ident" : pfx . user , "host" : pfx . host })
116115 else :
117116 user_data ["nick" ] = name
118117
119- nick_cf = user_data ["nick" ]. casefold ()
120- new_users [nick_cf ] = memb_data
121- if nick_cf in old_users :
122- old_data = old_users [nick_cf ]
118+ nick = user_data ["nick" ]
119+ new_users [nick ] = memb_data
120+ if nick in old_users :
121+ old_data = old_users [nick ]
123122 old_data .update (memb_data ) # New data takes priority over old data
124123 memb_data .update (old_data )
125124
126- old_user_data = users .setdefault (nick_cf , user_data )
125+ old_user_data = users .setdefault (nick , user_data )
127126 old_user_data .update (user_data )
128127 user_data = old_user_data
129128 memb_data ["user" ] = user_data
130- user_chans = user_data .setdefault ("channels" , WeakValueDictionary ())
131- user_chans [chan_data ["name" ].casefold ()] = memb_data
129+ add_user_membership (user_data , chan_data ["name" ], memb_data )
132130
133131 old_users .clear ()
134132 old_users .update (new_users ) # Reassigning the dict would break other references to the data, so just update instead
@@ -181,11 +179,10 @@ def perm_check(chan, conn, nick):
181179 return False
182180
183181 chan_data = chans [chan ]
184- nick_cf = nick .casefold ()
185- if nick_cf not in chan_data ["users" ]:
182+ if nick not in chan_data ["users" ]:
186183 return False
187184
188- memb = chan_data ["users" ][nick_cf ]
185+ memb = chan_data ["users" ][nick ]
189186 status = memb ["status" ]
190187 if status and status [0 ].level > 1 :
191188 return True
@@ -218,58 +215,31 @@ def updateusers(bot):
218215 return "Updating all channel data"
219216
220217
221- @hook .irc_raw ('JOIN' )
218+ @hook .irc_raw (['JOIN' , 'MODE' ], singlethread = True )
219+ def on_join_mode (chan , nick , user , host , conn , irc_command , irc_paramlist ):
220+ """
221+ Both JOIN and MODE are handled in one hook with Hook:singlethread=True
222+ to ensure they are handled in order, avoiding a possible race condition
223+ """
224+ if irc_command == 'JOIN' :
225+ return on_join (chan , nick , user , host , conn )
226+ elif irc_command == 'MODE' :
227+ return on_mode (chan , irc_paramlist , conn )
228+
229+
222230def on_join (chan , nick , user , host , conn ):
223231 if chan .startswith (':' ):
224232 chan = chan [1 :]
225233
226234 users = conn .memory ['users' ]
227- user_data = WeakDict ({ " nick" : nick , " user" : user , " host" : host } )
228- user_data = users .setdefault (nick . casefold () , user_data )
235+ user_data = WeakDict (nick = nick , user = user , host = host )
236+ user_data = users .setdefault (nick , user_data )
229237 chan_data = conn .memory ["chan_data" ][chan ]
230- memb_data = WeakDict ({"chan" : weakref .proxy (chan_data ), "user" : user_data , "status" : []})
231- chan_data ["users" ][nick .casefold ()] = memb_data
232- user_chans = user_data .setdefault ("channels" , WeakValueDictionary ())
233- user_chans [chan .casefold ()] = memb_data
234-
235-
236- @hook .irc_raw ('PART' )
237- def on_part (chan , nick , conn ):
238- if chan .startswith (':' ):
239- chan = chan [1 :]
240-
241- channels = conn .memory ["chan_data" ]
242- nick_cf = nick .casefold ()
243- if nick_cf == conn .nick .casefold ():
244- try :
245- del channels [chan ]
246- except KeyError :
247- pass
248- else :
249- chan_data = channels [chan ]
250- try :
251- del chan_data ["users" ][nick_cf ]
252- except KeyError :
253- pass
238+ memb_data = WeakDict (chan = weakref .proxy (chan_data ), user = user_data , status = [])
239+ chan_data ["users" ][nick ] = memb_data
240+ add_user_membership (user_data , chan , memb_data )
254241
255242
256- @hook .irc_raw ('KICK' )
257- def on_kick (chan , target , conn ):
258- on_part (chan , target , conn )
259-
260-
261- @hook .irc_raw ('QUIT' )
262- def on_quit (nick , conn ):
263- nick_cf = nick .casefold ()
264- users = conn .memory ["users" ]
265- if nick_cf in users :
266- user = users [nick_cf ]
267- for memb in user .get ("channels" , {}).values ():
268- chan = memb ["chan" ]
269- chan ["users" ].pop (nick_cf )
270-
271-
272- @hook .irc_raw ('MODE' )
273243def on_mode (chan , irc_paramlist , conn ):
274244 if chan .startswith (':' ):
275245 chan = chan [1 :]
@@ -306,7 +276,7 @@ def on_mode(chan, irc_paramlist, conn):
306276 param = mode_params .pop (0 )
307277
308278 if is_status :
309- memb = chan_data ["users" ][param . casefold () ]
279+ memb = chan_data ["users" ][param ]
310280 status = statuses [c ]
311281 if adding :
312282 memb ["status" ].append (status )
@@ -316,18 +286,50 @@ def on_mode(chan, irc_paramlist, conn):
316286 memb ["status" ].remove (status )
317287
318288
289+ @hook .irc_raw ('PART' )
290+ def on_part (chan , nick , conn ):
291+ if chan .startswith (':' ):
292+ chan = chan [1 :]
293+
294+ channels = conn .memory ["chan_data" ]
295+ if nick .casefold () == conn .nick .casefold ():
296+ try :
297+ del channels [chan ]
298+ except KeyError :
299+ pass
300+ else :
301+ chan_data = channels [chan ]
302+ try :
303+ del chan_data ["users" ][nick ]
304+ except KeyError :
305+ pass
306+
307+
308+ @hook .irc_raw ('KICK' )
309+ def on_kick (chan , target , conn ):
310+ on_part (chan , target , conn )
311+
312+
313+ @hook .irc_raw ('QUIT' )
314+ def on_quit (nick , conn ):
315+ users = conn .memory ["users" ]
316+ if nick in users :
317+ user = users [nick ]
318+ for memb in user .get ("channels" , {}).values ():
319+ chan = memb ["chan" ]
320+ chan ["users" ].pop (nick )
321+
322+
319323@hook .irc_raw ('NICK' )
320324def on_nick (nick , irc_paramlist , conn ):
321325 users = conn .memory ["users" ]
322- nick_cf = nick .casefold ()
323326 new_nick = irc_paramlist [0 ]
324327 if new_nick .startswith (':' ):
325328 new_nick = new_nick [1 :]
326329
327- new_nick_cf = new_nick .casefold ()
328- user = users .pop (nick_cf )
329- users [new_nick_cf ] = user
330+ user = users .pop (nick )
331+ users [new_nick ] = user
330332 user ["nick" ] = nick
331- for memb in user [ "channels" ] .values ():
333+ for memb in user . get ( "channels" , {}) .values ():
332334 chan_users = memb ["chan" ]["users" ]
333- chan_users [new_nick_cf ] = chan_users .pop (nick_cf )
335+ chan_users [new_nick ] = chan_users .pop (nick )
0 commit comments