Skip to content

Commit ad4420d

Browse files
author
Tony Young
committed
Add support for User and Channel models (#15).
This is extremely backwards-incompatible with v0.8 as it changes many interfaces for interacting with user and channel information, namely: - All previous dictionary fields have been converted to attributes, e.g. user['username'] is now user.username. This extends to results from WHOIS (WhoisInfo) and WHOWAS (WhowasInfo). - Instead of nicknames and channel names, User and Channel models are now passed to on_* hooks (BREAKING). - _sync_user has been completely removed and replaced with _parse_and_sync_user (and, to some extent, _get_or_create_user). Everything that made use of _parse_user now also synchronizes with the user database for consistency (may be breaking). - A new metaclass, ClientMeta, has been introduced to allow dynamic composition of the User and Channel classes on the feature classes (internal only). - User/Channel objects have the message() method when RFC1459 support is active (feature).
1 parent ac2c7de commit ad4420d

File tree

10 files changed

+344
-309
lines changed

10 files changed

+344
-309
lines changed

pydle/client.py

+50-36
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from . import async
99
from . import connection
10+
from . import models
1011
from . import protocol
1112

1213
__all__ = [ 'Error', 'AlreadyInChannel', 'NotInChannel', 'BasicClient' ]
@@ -29,7 +30,20 @@ def __init__(self, channel):
2930
self.channel = channel
3031

3132

32-
class BasicClient:
33+
class ClientMeta(type):
34+
@staticmethod
35+
def _compose(name, cls, bases):
36+
return type(name, tuple({base for base in bases if not issubclass(cls, base)}) + (cls,), {})
37+
38+
def __new__(cls, name, bases, attrs):
39+
if "User" in attrs:
40+
attrs["User"] = cls._compose("_User", attrs["User"], [base.User for base in bases])
41+
if "Channel" in attrs:
42+
attrs["Channel"] = cls._compose("_Channel", attrs["Channel"], [base.Channel for base in bases])
43+
return super().__new__(cls, name, bases, attrs)
44+
45+
46+
class BasicClient(metaclass=ClientMeta):
3347
"""
3448
Base IRC client class.
3549
This class on its own is not complete: in order to be able to run properly, _has_message, _parse_message and _create_message have to be overloaded.
@@ -39,11 +53,13 @@ class BasicClient:
3953
RECONNECT_DELAYED = True
4054
RECONNECT_DELAYS = [0, 5, 10, 30, 120, 600]
4155

56+
User = models.User
57+
Channel = models.Channel
58+
4259
def __init__(self, nickname, fallback_nicknames=[], username=None, realname=None, **kwargs):
4360
""" Create a client. """
4461
self._nicknames = [nickname] + fallback_nicknames
45-
self.username = username or nickname.lower()
46-
self.realname = realname or nickname
62+
self.user = models.User(self, nickname, realname or nickname, username or nickname.lower())
4763
self.eventloop = None
4864
self.own_eventloop = True
4965
self._reset_connection_attributes()
@@ -52,6 +68,18 @@ def __init__(self, nickname, fallback_nicknames=[], username=None, realname=None
5268
if kwargs:
5369
self.logger.warning('Unused arguments: %s', ', '.join(kwargs.keys()))
5470

71+
@property
72+
def nickname(self):
73+
return self.user.nickname
74+
75+
@property
76+
def username(self):
77+
return self.user.username
78+
79+
@property
80+
def realname(self):
81+
return self.user.realname
82+
5583
def _reset_attributes(self):
5684
""" Reset attributes. """
5785
# Record-keeping.
@@ -69,7 +97,7 @@ def _reset_attributes(self):
6997
self.logger = logging.getLogger(__name__)
7098

7199
# Public connection attributes.
72-
self.nickname = DEFAULT_NICKNAME
100+
self.user.nickname = DEFAULT_NICKNAME
73101
self.network = None
74102

75103
def _reset_connection_attributes(self):
@@ -164,42 +192,28 @@ def _check_ping_timeout(self):
164192
## Internal database management.
165193

166194
def _create_channel(self, channel):
167-
self.channels[channel] = {
168-
'users': set(),
169-
}
195+
self.channels[channel] = self.Channel(self, channel)
170196

171197
def _destroy_channel(self, channel):
172198
# Copy set to prevent a runtime error when destroying the user.
173-
for user in set(self.channels[channel]['users']):
199+
for user in set(self.channels[channel].users):
174200
self._destroy_user(user, channel)
175201
del self.channels[channel]
176202

177203

178204
def _create_user(self, nickname):
179-
# Servers are NOT users.
180-
if not nickname or '.' in nickname:
181-
return
205+
self.users[nickname] = self.User(self, nickname)
182206

183-
self.users[nickname] = {
184-
'nickname': nickname,
185-
'username': None,
186-
'realname': None,
187-
'hostname': None
188-
}
189-
190-
def _sync_user(self, nick, metadata):
191-
# Create user in database.
192-
if nick not in self.users:
193-
self._create_user(nick)
194-
if nick not in self.users:
195-
return
207+
def _get_or_create_user(self, nickname):
208+
if nickname not in self.users:
209+
self._create_user(nickname)
196210

197-
self.users[nick].update(metadata)
211+
return self.users[nickname]
198212

199213
def _rename_user(self, user, new):
200214
if user in self.users:
201215
self.users[new] = self.users[user]
202-
self.users[new]['nickname'] = new
216+
self.users[new].nickname = new
203217
del self.users[user]
204218
else:
205219
self._create_user(new)
@@ -208,9 +222,9 @@ def _rename_user(self, user, new):
208222

209223
for ch in self.channels.values():
210224
# Rename user in channel list.
211-
if user in ch['users']:
212-
ch['users'].discard(user)
213-
ch['users'].add(new)
225+
if user in ch.users:
226+
ch.users.discard(user)
227+
ch.users.add(new)
214228

215229
def _destroy_user(self, nickname, channel=None):
216230
if channel:
@@ -220,23 +234,23 @@ def _destroy_user(self, nickname, channel=None):
220234

221235
for ch in channels:
222236
# Remove from nicklist.
223-
ch['users'].discard(nickname)
237+
ch.users.discard(nickname)
224238

225239
# If we're not in any common channels with the user anymore, we have no reliable way to keep their info up-to-date.
226240
# Remove the user.
227-
if not channel or not any(nickname in ch['users'] for ch in self.channels.values()):
241+
if not channel or not any(nickname in ch.users for ch in self.channels.values()):
228242
del self.users[nickname]
229243

230244
def _parse_user(self, data):
231245
""" Parse user and return nickname, metadata tuple. """
232246
raise NotImplementedError()
233247

234248
def _format_user_mask(self, nickname):
235-
user = self.users.get(nickname, { "nickname": nickname, "username": "*", "hostname": "*" })
236-
return self._format_host_mask(user['nickname'], user['username'] or '*', user['hostname'] or '*')
237-
238-
def _format_host_mask(self, nick, user, host):
239-
return '{n}!{u}@{h}'.format(n=nick, u=user, h=host)
249+
if nickname in self.users:
250+
user = self.users[nickname]
251+
else:
252+
user = self.User(self, nickname)
253+
return user.hostmask
240254

241255

242256
## IRC helpers.

pydle/features/account.py

+15-22
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,12 @@ class AccountSupport(rfc1459.RFC1459Support):
66

77
## Internal.
88

9-
def _create_user(self, nickname):
10-
super()._create_user(nickname)
11-
if nickname in self.users:
12-
self.users[nickname].update({
13-
'account': None,
14-
'identified': False
15-
})
16-
179
def _rename_user(self, user, new):
1810
super()._rename_user(user, new)
1911
# Unset account info.
20-
self._sync_user(new, { 'account': None, 'identified': False })
12+
user = self.users[new]
13+
user.account = None
14+
user.identified = False
2115

2216

2317
## IRC API.
@@ -27,8 +21,9 @@ def whois(self, nickname):
2721

2822
# Add own info.
2923
if nickname in self._whois_info:
30-
self._whois_info[nickname].setdefault('account', None)
31-
self._whois_info[nickname].setdefault('identified', False)
24+
whois_info = self._whois_info[nickname]
25+
whois_info.account = None
26+
whois_info.identified = False
3227

3328
return future
3429

@@ -38,24 +33,22 @@ def whois(self, nickname):
3833
def on_raw_307(self, message):
3934
""" WHOIS: User has identified for this nickname. (Anope) """
4035
target, nickname = message.params[:2]
41-
info = {
42-
'identified': True
43-
}
4436

4537
if nickname in self.users:
46-
self._sync_user(nickname, info)
38+
user = self._get_or_create_user(nickname)
39+
user.identified = True
4740
if nickname in self._pending['whois']:
48-
self._whois_info[nickname].update(info)
41+
whois_info = self._whois_info[nickname]
42+
whois_info.identified = True
4943

5044
def on_raw_330(self, message):
5145
""" WHOIS account name (Atheme). """
5246
target, nickname, account = message.params[:3]
53-
info = {
54-
'account': account,
55-
'identified': True
56-
}
5747

5848
if nickname in self.users:
59-
self._sync_user(nickname, info)
49+
user = self._get_or_create_user(nickname)
50+
user.account = nickname
51+
user.identified = True
6052
if nickname in self._pending['whois']:
61-
self._whois_info[nickname].update(info)
53+
whois_info.account = account
54+
whois_info.identified = True

pydle/features/ctcp.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -60,29 +60,27 @@ def ctcp_reply(self, target, query, response):
6060

6161
def on_raw_privmsg(self, message):
6262
""" Modify PRIVMSG to redirect CTCP messages. """
63-
nick, metadata = self._parse_user(message.source)
63+
user = self._parse_and_sync_user(message.source)
6464
target, msg = message.params
6565

6666
if is_ctcp(msg):
67-
self._sync_user(nick, metadata)
6867
type, contents = parse_ctcp(msg)
6968

7069
# Find dedicated handler if it exists.
7170
attr = 'on_ctcp_' + pydle.protocol.identifierify(type)
7271
if hasattr(self, attr):
73-
getattr(self, attr)(nick, target, contents)
72+
getattr(self, attr)(user.nickname, target, contents)
7473
# Invoke global handler.
75-
self.on_ctcp(nick, target, type, contents)
74+
self.on_ctcp(user.nickname, target, type, contents)
7675
else:
7776
super().on_raw_privmsg(message)
7877

7978
def on_raw_notice(self, message):
8079
""" Modify NOTICE to redirect CTCP messages. """
81-
nick, metadata = self._parse_user(message.source)
80+
user = self._parse_and_sync_user(message.source)
8281
target, msg = message.params
8382

8483
if is_ctcp(msg):
85-
self._sync_user(nick, metadata)
8684
type, response = parse_ctcp(msg)
8785

8886
# Find dedicated handler if it exists.

pydle/features/ircv3_1/ircv3_1.py

+21-22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## ircv3_1.py
22
# IRCv3.1 full spec support.
3+
from pydle import models
34
from pydle.features import account, tls
45
from . import sasl
56

@@ -9,9 +10,19 @@
910

1011
NO_ACCOUNT = '*'
1112

13+
14+
class IRCv3_1User(models.User):
15+
def __init__(self, *args, **kwargs):
16+
super().__init__(*args, **kwargs)
17+
self.account = None
18+
self.identified = False
19+
20+
1221
class IRCv3_1Support(sasl.SASLSupport, account.AccountSupport, tls.TLSSupport):
1322
""" Support for IRCv3.1's base and optional extensions. """
1423

24+
User = IRCv3_1User
25+
1526
## IRC callbacks.
1627

1728
def on_capability_account_notify_available(self):
@@ -42,46 +53,34 @@ def on_raw_account(self, message):
4253
if 'account-notify' not in self._capabilities or not self._capabilities['account-notify']:
4354
return
4455

45-
nick, metadata = self._parse_user(message.source)
56+
user = self._parse_and_sync_user(message.source)
4657
account = message.params[0]
47-
48-
if nick not in self.users:
49-
return
50-
51-
self._sync_user(nick, metadata)
52-
if account == NO_ACCOUNT:
53-
self._sync_user(nick, { 'account': None, 'identified': False })
54-
else:
55-
self._sync_user(nick, { 'account': account, 'identified': True })
58+
if account != NO_ACCOUNT:
59+
user.account = account
60+
user.identified = True
5661

5762
def on_raw_away(self, message):
5863
""" Process AWAY messages. """
5964
if 'away-notify' not in self._capabilities or not self._capabilities['away-notify']:
6065
return
6166

62-
nick, metadata = self._parse_user(message.source)
63-
if nick not in self.users:
64-
return
65-
66-
self._sync_user(nick, metadata)
67-
self.users[nick]['away'] = len(message.params) > 0
68-
self.users[nick]['away_message'] = message.params[0] if len(message.params) > 0 else None
67+
user = self._parse_and_syn_user(message.source)
68+
user.away_message = message.params[0] if len(message.params) > 0 else None
6969

7070
def on_raw_join(self, message):
7171
""" Process extended JOIN messages. """
7272
if 'extended-join' in self._capabilities and self._capabilities['extended-join']:
73-
nick, metadata = self._parse_user(message.source)
73+
user = self._parse_and_sync_user(message.source)
7474
channels, account, realname = message.params
7575

76-
self._sync_user(nick, metadata)
77-
7876
# Emit a fake join message.
7977
fakemsg = self._create_message('JOIN', channels, source=message.source)
8078
super().on_raw_join(fakemsg)
8179

8280
if account == NO_ACCOUNT:
8381
account = None
84-
self.users[nick]['account'] = account
85-
self.users[nick]['realname'] = realname
82+
user.account = account
83+
user.identified = user.account is not None
84+
user.realname = realname
8685
else:
8786
super().on_raw_join(message)

pydle/features/ircv3_2/ircv3_2.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,5 @@ def on_raw_chghost(self, message):
4242
return
4343

4444
# Update user and host.
45-
metadata = {
46-
'username': message.params[0],
47-
'hostname': message.params[1]
48-
}
49-
self._sync_user(nick, metadata)
45+
user = self._get_or_create_user(nick)
46+
user.username, user.hostname = message.params[:2]

pydle/features/ircv3_2/monitor.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,16 @@ def _destroy_user(self, nickname, channel=None, monitor_override=False):
2121

2222
for ch in channels:
2323
# Remove from nicklist.
24-
ch['users'].discard(nickname)
24+
ch.users.discard(nickname)
2525

2626
# Remove from statuses.
2727
for status in self._nickname_prefixes.values():
28-
if status in ch['modes'] and nickname in ch['modes'][status]:
29-
ch['modes'][status].remove(nickname)
28+
if status in ch.modes and nickname in ch.modes[status]:
29+
ch.modes[status].remove(nickname)
3030

3131
# If we're not in any common channels with the user anymore, we have no reliable way to keep their info up-to-date.
3232
# Remove the user.
33-
if (monitor_override or not self.is_monitoring(nickname)) and (not channel or not any(nickname in ch['users'] for ch in self.channels.values())):
33+
if (monitor_override or not self.is_monitoring(nickname)) and (not channel or not any(nickname in ch.users for ch in self.channels.values())):
3434
del self.users[nickname]
3535

3636

0 commit comments

Comments
 (0)