1111from operator import attrgetter
1212from weakref import WeakValueDictionary
1313
14+ import cloudbot .bot
1415from cloudbot import hook
1516from cloudbot .util .parsers .irc import Prefix
1617
18+ logger = cloudbot .bot .logger
19+
1720
1821class WeakDict (dict ):
1922 # Subclass dict to allow it to be weakly referenced
@@ -69,12 +72,54 @@ def getuser(self, nick):
6972 return value
7073
7174
75+ # region util functions
76+
77+
78+ def get_users (conn ):
79+ """
80+ :type conn: cloudbot.client.Client
81+ :rtype: UsersDict
82+ """
83+ return conn .memory .setdefault ("users" , UsersDict )
84+
85+
86+ def get_chans (conn ):
87+ """
88+ :type conn: cloudbot.client.Client
89+ :rtype: ChanDict
90+ """
91+ return conn .memory .setdefault ("chan_data" , ChanDict ())
92+
93+
94+ def get_channel_member (chan_data , user_data ):
95+ """
96+ :type chan_data: dict
97+ :type user_data: dict
98+ :rtype: dict
99+ """
100+ nick = user_data ['nick' ]
101+ try :
102+ data = chan_data ['users' ][nick ]
103+ except KeyError :
104+ chan_data ['users' ][nick ] = data = WeakDict (
105+ user = user_data , chan = weakref .proxy (chan_data ), status = []
106+ )
107+
108+ # make sure the membership is stored on the user too
109+ # just in case
110+ user_data ['channels' ][chan_data ['name' ]] = data
111+ return data
112+
113+
114+ # endregion util functions
115+
116+
72117def update_chan_data (conn , chan ):
73118 """
74119 :type conn: cloudbot.client.Client
75120 :type chan: str
76121 """
77- chan_data = conn . memory [ "chan_data" ] .getchan (chan )
122+ chan_data = get_chans ( conn ) .getchan (chan )
78123 chan_data ["receiving_names" ] = False
79124 conn .cmd ("NAMES" , chan )
80125
@@ -147,10 +192,10 @@ def clean_conn_data(conn):
147192 """
148193 :type conn: cloudbot.client.Client
149194 """
150- for user in conn . memory . get ( "users" , {} ).values ():
195+ for user in get_users ( conn ).values ():
151196 clean_user_data (user )
152197
153- for chan in conn . memory . get ( "chan_data" , {} ).values ():
198+ for chan in get_chans ( conn ).values ():
154199 clean_chan_data (chan )
155200
156201
@@ -168,8 +213,8 @@ def init_chan_data(conn, _clear=True):
168213 :type conn: cloudbot.client.Client
169214 :type _clear: bool
170215 """
171- chan_data = conn . memory . setdefault ( "chan_data" , ChanDict () )
172- users = conn . memory . setdefault ( "users" , UsersDict () )
216+ chan_data = get_chans ( conn )
217+ users = get_users ( conn )
173218
174219 if not (isinstance (chan_data , ChanDict ) and isinstance (users , UsersDict )):
175220 del conn .memory ["chan_data" ]
@@ -182,13 +227,23 @@ def init_chan_data(conn, _clear=True):
182227 users .clear ()
183228
184229
185- def add_user_membership (user , chan , membership ):
186- """
187- :type user: dict
188- :type chan: str
189- :type membership: dict
190- """
191- user ["channels" ][chan ] = membership
230+ def parse_names_item (item , statuses , has_multi_prefix , has_userhost ):
231+ user_status = []
232+ while item [:1 ] in statuses :
233+ status , item = item [:1 ], item [1 :]
234+ user_status .append (statuses [status ])
235+ if not has_multi_prefix :
236+ # Only remove one status prefix if we don't have multi prefix enabled
237+ break
238+
239+ user_status .sort (key = attrgetter ('level' ), reverse = True )
240+
241+ if has_userhost :
242+ prefix = Prefix .parse (item )
243+ else :
244+ prefix = Prefix (item )
245+
246+ return prefix .nick , prefix .user , prefix .host , user_status
192247
193248
194249def replace_user_data (conn , chan_data ):
@@ -197,48 +252,23 @@ def replace_user_data(conn, chan_data):
197252 :type chan_data: dict
198253 """
199254 statuses = {status .prefix : status for status in set (conn .memory ["server_info" ]["statuses" ].values ())}
200- users = conn .memory ["users" ]
201- old_users = chan_data ["users" ]
202255 new_data = chan_data .pop ("new_users" , [])
203256 new_users = KeyFoldDict ()
204257 has_uh_i_n = is_cap_available (conn , "userhost-in-names" )
205258 has_multi_pfx = is_cap_available (conn , "multi-prefix" )
206259 for name in new_data :
207- user_data = WeakDict (channels = KeyFoldWeakValueDict ())
208- memb_data = WeakDict (user = user_data , chan = weakref .proxy (chan_data ))
209- user_statuses = []
210- while name [:1 ] in statuses :
211- status , name = name [:1 ], name [1 :]
212- user_statuses .append (statuses [status ])
213- if not has_multi_pfx :
214- # Only run once if we don't have multi-prefix enabled
215- break
216-
217- user_statuses .sort (key = attrgetter ("level" ), reverse = True )
218- # At this point, user_status[0] will the the Status object representing the highest status the user has
219- # in the channel
220- memb_data ["status" ] = user_statuses
221-
222- if has_uh_i_n :
223- pfx = Prefix .parse (name )
224- user_data .update (nick = pfx .nick , ident = pfx .user , host = pfx .host )
225- else :
226- user_data ["nick" ] = name
227-
228- nick = user_data ["nick" ]
229- new_users [nick ] = memb_data
230- if nick in old_users :
231- old_data = old_users [nick ] # Old membership object
232- old_data .update (memb_data ) # New data takes priority over old data
233- memb_data .update (old_data )
234-
235- old_user_data = users .getuser (nick )
236- user_data ["channels" ] = old_user_data ["channels" ]
237- add_user_membership (user_data , chan_data ["name" ], memb_data )
238- old_user_data .update (user_data )
239- user_data = old_user_data
240- memb_data ["user" ] = user_data
260+ nick , ident , host , status = parse_names_item (name , statuses , has_multi_pfx , has_uh_i_n )
261+ user_data = get_users (conn ).getuser (nick )
262+ user_data .update (
263+ nick = nick ,
264+ ident = ident ,
265+ host = host ,
266+ )
241267
268+ new_users [nick ] = memb_data = get_channel_member (chan_data , user_data )
269+ memb_data ['status' ] = status
270+
271+ old_users = chan_data ["users" ]
242272 old_users .clear ()
243273 old_users .update (new_users ) # Reassigning the dict would break other references to the data, so just update instead
244274
@@ -251,7 +281,7 @@ def on_names(conn, irc_paramlist, irc_command):
251281 :type irc_command: str
252282 """
253283 chan = irc_paramlist [2 if irc_command == '353' else 1 ]
254- chan_data = conn . memory [ "chan_data" ] .getchan (chan )
284+ chan_data = get_chans ( conn ) .getchan (chan )
255285 if irc_command == '366' :
256286 chan_data ["receiving_names" ] = False
257287 replace_user_data (conn , chan_data )
@@ -301,7 +331,7 @@ def perm_check(chan, conn, nick):
301331 if not (chan and conn ):
302332 return False
303333
304- chans = conn . memory [ "chan_data" ]
334+ chans = get_chans ( conn )
305335 try :
306336 chan_data = chans [chan ]
307337 except KeyError :
@@ -324,7 +354,7 @@ def dumpchans(conn):
324354 """- Dumps all stored channel data for this connection to the console
325355 :type conn: cloudbot.client.Client
326356 """
327- data = conn . memory [ "chan_data" ]
357+ data = get_chans ( conn )
328358 lines = list (dump_dict (data ))
329359 print ('\n ' .join (lines ))
330360 return "Printed {} channel records totalling {} lines of data to the console." .format (len (data ), len (lines ))
@@ -335,7 +365,7 @@ def dumpusers(conn):
335365 """- Dumps all stored user data for this connection to the console
336366 :type conn: cloudbot.client.Client
337367 """
338- data = conn . memory [ "users" ]
368+ data = get_users ( conn )
339369 lines = list (dump_dict (data ))
340370 print ('\n ' .join (lines ))
341371 return "Printed {} user records totalling {} lines of data to the console." .format (len (data ), len (lines ))
@@ -395,15 +425,13 @@ def on_join(nick, user, host, conn, irc_paramlist):
395425
396426 data .update (account = acct , realname = realname )
397427
398- users = conn . memory [ 'users' ]
428+ users = get_users ( conn )
399429
400430 user_data = users .getuser (nick )
401431 user_data .update (data )
402432
403- chan_data = conn .memory ["chan_data" ].getchan (chan )
404- memb_data = WeakDict (chan = weakref .proxy (chan_data ), user = user_data , status = [])
405- chan_data ["users" ][nick ] = memb_data
406- add_user_membership (user_data , chan , memb_data )
433+ chan_data = get_chans (conn ).getchan (chan )
434+ get_channel_member (chan_data , user_data )
407435
408436
409437@hook .irc_raw ('MODE' )
@@ -420,14 +448,9 @@ def on_mode(chan, irc_paramlist, conn):
420448 statuses = serv_info ["statuses" ]
421449 status_modes = {status .mode for status in statuses .values ()}
422450 mode_types = serv_info ["channel_modes" ]
423- chans = conn .memory ["chan_data" ]
424451
425- try :
426- chan_data = chans [chan ]
427- except KeyError :
428- return
452+ chan_data = get_chans (conn ).getchan (chan )
429453
430- chan_users = chan_data ["users" ]
431454 modes = irc_paramlist [1 ]
432455 mode_params = irc_paramlist [2 :]
433456 new_modes = {}
@@ -450,14 +473,21 @@ def on_mode(chan, irc_paramlist, conn):
450473 param = mode_params .pop (0 )
451474
452475 if is_status :
453- memb = chan_users [param ]
476+ user = get_users (conn ).getuser (param )
477+ memb = get_channel_member (chan_data , user )
454478 status = statuses [c ]
455479 memb_status = memb ["status" ]
456480 if adding :
457481 memb_status .append (status )
458482 memb_status .sort (key = attrgetter ("level" ), reverse = True )
459483 else :
460- memb_status .remove (status )
484+ if status in memb_status :
485+ memb_status .remove (status )
486+ else :
487+ logger .debug (
488+ "[%s|chantrack] Attempt to remove status %s from user %s in channel %s" ,
489+ conn .name , status , user ['nick' ], chan
490+ )
461491
462492
463493@hook .irc_raw ('PART' )
@@ -470,7 +500,7 @@ def on_part(chan, nick, conn):
470500 if chan .startswith (':' ):
471501 chan = chan [1 :]
472502
473- channels = conn . memory [ "chan_data" ]
503+ channels = get_chans ( conn )
474504 if nick .casefold () == conn .nick .casefold ():
475505 del channels [chan ]
476506 else :
@@ -494,10 +524,10 @@ def on_quit(nick, conn):
494524 :type nick: str
495525 :type conn: cloudbot.client.Client
496526 """
497- users = conn . memory [ "users" ]
527+ users = get_users ( conn )
498528 if nick in users :
499- user = users [ nick ]
500- for memb in user . get ( " channels" , {}) .values ():
529+ user = users . pop ( nick )
530+ for memb in user [ ' channels' ] .values ():
501531 chan = memb ["chan" ]
502532 del chan ["users" ][nick ]
503533
@@ -509,15 +539,15 @@ def on_nick(nick, irc_paramlist, conn):
509539 :type irc_paramlist: cloudbot.util.parsers.irc.ParamList
510540 :type conn: cloudbot.client.Client
511541 """
512- users = conn . memory [ "users" ]
542+ users = get_users ( conn )
513543 new_nick = irc_paramlist [0 ]
514544 if new_nick .startswith (':' ):
515545 new_nick = new_nick [1 :]
516546
517547 user = users .pop (nick )
518548 users [new_nick ] = user
519549 user ["nick" ] = new_nick
520- for memb in user . get ( " channels" , {}) .values ():
550+ for memb in user [ ' channels' ] .values ():
521551 chan_users = memb ["chan" ]["users" ]
522552 chan_users [new_nick ] = chan_users .pop (nick )
523553
@@ -529,7 +559,7 @@ def on_account(conn, nick, irc_paramlist):
529559 :type irc_paramlist: cloudbot.util.parsers.irc.ParamList
530560 :type conn: cloudbot.client.Client
531561 """
532- conn . memory [ "users" ][ nick ] ["account" ] = irc_paramlist [0 ]
562+ get_users ( conn ). getuser ( nick ) ["account" ] = irc_paramlist [0 ]
533563
534564
535565@hook .irc_raw ('CHGHOST' )
@@ -540,7 +570,7 @@ def on_chghost(conn, nick, irc_paramlist):
540570 :type conn: cloudbot.client.Client
541571 """
542572 ident , host = irc_paramlist
543- conn . memory [ "users" ][ nick ] .update (ident = ident , host = host )
573+ get_users ( conn ). getuser ( nick ) .update (ident = ident , host = host )
544574
545575
546576@hook .irc_raw ('AWAY' )
@@ -555,7 +585,7 @@ def on_away(conn, nick, irc_paramlist):
555585 else :
556586 reason = None
557587
558- conn . memory [ "users" ][ nick ] .update (is_away = (reason is not None ), away_message = reason )
588+ get_users ( conn ). getuser ( nick ) .update (is_away = (reason is not None ), away_message = reason )
559589
560590
561591@hook .irc_raw ('352' )
@@ -566,7 +596,7 @@ def on_who(conn, irc_paramlist):
566596 """
567597 _ , _ , ident , host , server , nick , status , realname = irc_paramlist
568598 realname = realname .split (None , 1 )[1 ]
569- user = conn . memory [ "users" ][ nick ]
599+ user = get_users ( conn ). getuser ( nick )
570600 status = list (status )
571601 is_away = status .pop (0 ) == "G"
572602 is_oper = status [:1 ] == "*"
@@ -587,7 +617,7 @@ def on_whois_name(conn, irc_paramlist):
587617 :type conn: cloudbot.client.Client
588618 """
589619 _ , nick , ident , host , _ , realname = irc_paramlist
590- conn . memory [ "users" ][ nick ] .update (ident = ident , host = host , realname = realname )
620+ get_users ( conn ). getuser ( nick ) .update (ident = ident , host = host , realname = realname )
591621
592622
593623@hook .irc_raw ('330' )
@@ -597,7 +627,7 @@ def on_whois_acct(conn, irc_paramlist):
597627 :type conn: cloudbot.client.Client
598628 """
599629 _ , nick , acct = irc_paramlist [:2 ]
600- conn . memory [ "users" ][ nick ] ["account" ] = acct
630+ get_users ( conn ). getuser ( nick ) ["account" ] = acct
601631
602632
603633@hook .irc_raw ('301' )
@@ -607,7 +637,7 @@ def on_whois_away(conn, irc_paramlist):
607637 :type conn: cloudbot.client.Client
608638 """
609639 _ , nick , msg = irc_paramlist
610- conn . memory [ "users" ][ nick ] .update (is_away = True , away_message = msg )
640+ get_users ( conn ). getuser ( nick ) .update (is_away = True , away_message = msg )
611641
612642
613643@hook .irc_raw ('312' )
@@ -617,7 +647,7 @@ def on_whois_server(conn, irc_paramlist):
617647 :type conn: cloudbot.client.Client
618648 """
619649 _ , nick , server , _ = irc_paramlist
620- conn . memory [ "users" ][ nick ] .update (server = server )
650+ get_users ( conn ). getuser ( nick ) .update (server = server )
621651
622652
623653@hook .irc_raw ('313' )
@@ -627,4 +657,4 @@ def on_whois_oper(conn, irc_paramlist):
627657 :type conn: cloudbot.client.Client
628658 """
629659 nick = irc_paramlist [1 ]
630- conn . memory [ "users" ][ nick ] .update (is_oper = True )
660+ get_users ( conn ). getuser ( nick ) .update (is_oper = True )
0 commit comments