Skip to content

Commit 2d1c0ae

Browse files
authored
Merge pull request CloudBotIRC#97 from linuxdaemon/gonzobot+chan-track-refactor
General cleanup + bugfixes for chan_track.py
2 parents 72438d2 + 45f9f69 commit 2d1c0ae

1 file changed

Lines changed: 156 additions & 75 deletions

File tree

plugins/chan_track.py

Lines changed: 156 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
server_info.py
66
"""
77
import weakref
8-
from contextlib import suppress
98
from operator import attrgetter
109
from 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

3543
class KeyFoldDict(KeyFoldMixin, dict):
3644
pass
3745

3846

3947
class 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

5051
class 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

5960
class 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

7369
def 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)
8591
def 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
90101
def get_chan_data(bot):
91102
for conn in bot.connections.values():
@@ -111,21 +122,19 @@ def init_chan_data(conn, _clear=True):
111122

112123

113124
def add_user_membership(user, chan, membership):
114-
chans = user.setdefault('channels', KeyFoldWeakValueDict())
115-
chans[chan] = membership
125+
user["channels"][chan] = membership
116126

117127

118128
def 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)
167176
def 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")
204213
def 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')
274288
def 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

Comments
 (0)