# built-ins
from abc import ABC, abstractmethod
from base64 import b64encode
-from dataclasses import dataclass, InitVar
+from dataclasses import dataclass, asdict as dc_asdict, InitVar
from enum import Enum, auto
from getpass import getuser
from threading import Thread
from typing import Any, Callable, NamedTuple, Optional, Self
+from uuid import uuid4
# ourselves
from ircplom.events import (
AffectiveEvent, CrashingException, ExceptionEvent, QueueMixin)
ClientsDb = dict[str, 'Client']
_NAMES_DESIRED_SERVER_CAPS = ('sasl',)
+_ILLEGAL_NICK_FIRSTCHARS = '~&@+# '
class _MsgTok(Enum):
_EXPECTATIONS += [
_MsgParseExpectation(_MsgTok.SERVER,
'001', # RPL_WELCOME
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY)),
_MsgParseExpectation(_MsgTok.SERVER,
'002', # RPL_YOURHOST
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY)),
_MsgParseExpectation(_MsgTok.SERVER,
'003', # RPL_CREATED
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY)),
_MsgParseExpectation(_MsgTok.SERVER,
'004', # RPL_MYINFO
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY,
_MsgTok.ANY,
_MsgTok.ANY,
_MsgTok.ANY)),
_MsgParseExpectation(_MsgTok.SERVER,
'250', # RPL_STATSDLINE / RPL_STATSCONN
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY)),
_MsgParseExpectation(_MsgTok.SERVER,
'251', # RPL_LUSERCLIENT
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY)),
_MsgParseExpectation(_MsgTok.SERVER,
'252', # RPL_LUSEROP
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY,
_MsgTok.ANY)),
_MsgParseExpectation(_MsgTok.SERVER,
'253', # RPL_LUSERUNKNOWN
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY,
_MsgTok.ANY)),
_MsgParseExpectation(_MsgTok.SERVER,
'254', # RPL_LUSERCHANNELS
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY,
_MsgTok.ANY)),
_MsgParseExpectation(_MsgTok.SERVER,
'255', # RPL_LUSERME
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY)),
_MsgParseExpectation(_MsgTok.SERVER,
'265', # RPL_LOCALUSERS
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY)),
_MsgParseExpectation(_MsgTok.SERVER,
- '265', # RPL_GLOBALUSERS
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ '265', # RPL_LOCALUSERS
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY,
_MsgTok.ANY,
_MsgTok.ANY)),
_MsgParseExpectation(_MsgTok.SERVER,
- '266',
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ '266', # RPL_GLOBALUSERS
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY)),
_MsgParseExpectation(_MsgTok.SERVER,
- '266',
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ '266', # RPL_GLOBALUSERS
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY,
_MsgTok.ANY,
_MsgTok.ANY)),
_MsgParseExpectation(_MsgTok.SERVER,
'375', # RPL_MOTDSTART already implied by 1st 372
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY)),
]
_EXPECTATIONS += [
_MsgParseExpectation(_MsgTok.SERVER,
'005', # RPL_ISUPPORT
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
(_MsgTok.ANY, ':isupports'),
_MsgTok.ANY), # comment
idx_into_list=1),
_MsgParseExpectation(_MsgTok.SERVER,
'372', # RPL_MOTD
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
(_MsgTok.ANY, ':line'))),
_MsgParseExpectation(_MsgTok.SERVER,
'376', # RPL_ENDOFMOTD
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
_MsgTok.ANY)), # comment
_MsgParseExpectation(_MsgTok.SERVER,
'396', # RPL_VISIBLEHOST
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
- (_MsgTok.SERVER, 'set_db_attr:client_host'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
+ (_MsgTok.SERVER, 'set_me_attr:host'),
_MsgTok.ANY)), # comment
]
_EXPECTATIONS += [
_MsgParseExpectation(_MsgTok.SERVER,
'900', # RPL_LOGGEDIN
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
- (_MsgTok.NICK_USER_HOST,
- 'set_db_attr:_nick_user_host'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
+ (_MsgTok.NICK_USER_HOST, 'set_me_attr:nickuserhost'),
(_MsgTok.ANY, 'set_db_attr:sasl_account'),
_MsgTok.ANY)), # comment
_MsgParseExpectation(_MsgTok.SERVER,
'903', # RPL_SASLSUCCESS
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
(_MsgTok.ANY, 'set_db_attr:sasl_auth_state'))),
_MsgParseExpectation(_MsgTok.SERVER,
'904', # ERR_SASLFAIL
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
(_MsgTok.ANY, 'set_db_attr:sasl_auth_state'))),
_MsgParseExpectation(_MsgTok.NONE,
'AUTHENTICATE',
_EXPECTATIONS += [
_MsgParseExpectation(_MsgTok.SERVER,
'CAP',
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
('NEW', ':subverb'),
(_MsgTok.LIST, ':items'))),
_MsgParseExpectation(_MsgTok.SERVER,
'CAP',
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
('DEL', ':subverb'),
(_MsgTok.LIST, ':items'))),
_MsgParseExpectation(_MsgTok.SERVER,
(_MsgTok.LIST, ':items'))),
_MsgParseExpectation(_MsgTok.SERVER,
'CAP',
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
('ACK', ':subverb'),
(_MsgTok.LIST, ':items'))),
_MsgParseExpectation(_MsgTok.SERVER,
(_MsgTok.LIST, ':items'))),
_MsgParseExpectation(_MsgTok.SERVER,
'CAP',
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
('NAK', ':subverb'),
(_MsgTok.LIST, ':items'))),
_MsgParseExpectation(_MsgTok.SERVER,
(_MsgTok.LIST, ':items'))),
_MsgParseExpectation(_MsgTok.SERVER,
'CAP',
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
('LS', ':subverb'),
(_MsgTok.LIST, ':items'))),
_MsgParseExpectation(_MsgTok.SERVER,
'CAP',
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
('LS', ':subverb'),
('*', ':tbc'),
(_MsgTok.LIST, ':items'))),
(_MsgTok.LIST, ':items'))),
_MsgParseExpectation(_MsgTok.SERVER,
'CAP',
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
('LIST', ':subverb'),
(_MsgTok.LIST, ':items'))),
_MsgParseExpectation(_MsgTok.SERVER,
'CAP',
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
('LIST', ':subverb'),
('*', ':tbc'),
(_MsgTok.LIST, ':items'))),
_MsgParseExpectation(_MsgTok.SERVER,
'432', # ERR_ERRONEOUSNICKNAME
('*',
- _MsgTok.NICKNAME,
+ _MsgTok.NICKNAME, # no need to re-use the bad one
_MsgTok.ANY)), # comment
_MsgParseExpectation(_MsgTok.SERVER,
'432', # ERR_ERRONEOUSNICKNAME
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
- _MsgTok.NICKNAME,
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
+ _MsgTok.NICKNAME, # no need to re-use the bad one
_MsgTok.ANY)), # comment
_MsgParseExpectation(_MsgTok.SERVER,
'433', # ERR_NICKNAMEINUSE
- (_MsgTok.NICKNAME,
+ (_MsgTok.NICKNAME, # we rather go for incrementation
(_MsgTok.NICKNAME, ':used'),
_MsgTok.ANY)), # comment
_MsgParseExpectation((_MsgTok.NICK_USER_HOST, ':named'),
'NICK',
- ((_MsgTok.NICKNAME, ':nickname'),)),
+ ((_MsgTok.NICKNAME, ':nick'),)),
]
# joining/leaving
_EXPECTATIONS += [
_MsgParseExpectation(_MsgTok.SERVER,
'353', # RPL_NAMREPLY
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
'=',
(_MsgTok.CHANNEL, ':channel'),
(_MsgTok.LIST, ':names'))),
_MsgParseExpectation(_MsgTok.SERVER,
'366', # RPL_ENDOFNAMES
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
(_MsgTok.CHANNEL, ':channel'),
_MsgTok.ANY)), # comment
_MsgParseExpectation((_MsgTok.NICK_USER_HOST, ':joiner'),
_EXPECTATIONS += [
_MsgParseExpectation(_MsgTok.SERVER,
'401', # ERR_NOSUCKNICK
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
(_MsgTok.NICKNAME, ':target'),
_MsgTok.ANY)), # comment
_MsgParseExpectation(_MsgTok.SERVER,
(_MsgTok.ANY, ':message'))),
_MsgParseExpectation(_MsgTok.SERVER,
'NOTICE',
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
(_MsgTok.ANY, ':message'))),
- _MsgParseExpectation((_MsgTok.NICK_USER_HOST, ':sender'),
+ _MsgParseExpectation((_MsgTok.NICK_USER_HOST, 'set_user:sender'),
'NOTICE',
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
(_MsgTok.ANY, ':message'))),
- _MsgParseExpectation((_MsgTok.NICK_USER_HOST, ':sender'),
+ _MsgParseExpectation((_MsgTok.NICK_USER_HOST, 'set_user:sender'),
'NOTICE',
((_MsgTok.CHANNEL, ':channel'),
(_MsgTok.ANY, ':message'))),
- _MsgParseExpectation((_MsgTok.NICK_USER_HOST, ':sender'),
+ _MsgParseExpectation((_MsgTok.NICK_USER_HOST, 'set_user:sender'),
'PRIVMSG',
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
(_MsgTok.ANY, ':message'))),
- _MsgParseExpectation((_MsgTok.NICK_USER_HOST, ':sender'),
+ _MsgParseExpectation((_MsgTok.NICK_USER_HOST, 'set_user:sender'),
'PRIVMSG',
((_MsgTok.CHANNEL, ':channel'),
(_MsgTok.ANY, ':message'))),
_MsgParseExpectation(_MsgTok.NONE,
'ERROR',
((_MsgTok.ANY, 'set_db_attr:connection_state'),)),
- _MsgParseExpectation(_MsgTok.NICKNAME,
+ _MsgParseExpectation((_MsgTok.NICK_USER_HOST, 'set_me_attr:nickuserhost'),
'MODE',
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
(_MsgTok.ANY, 'set_db_attr:user_modes'))),
- _MsgParseExpectation(_MsgTok.NICK_USER_HOST,
+ _MsgParseExpectation(_MsgTok.NICKNAME,
'MODE',
- ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+ ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
(_MsgTok.ANY, 'set_db_attr:user_modes'))),
_MsgParseExpectation(_MsgTok.NONE,
'PING',
def __init__(self) -> None:
self._dict: dict[str, Any] = {}
+ @property
+ def keys(self) -> tuple[str, ...]:
+ 'Keys of item registrations.'
+ return tuple(self._dict.keys())
+
def set_on_update(self, name: str, on_update: Callable) -> None:
'Caller of on_update with path= set to name.'
self._on_update = lambda k: on_update(name, k)
return self._dict[key]
def __setitem__(self, key: str, val: Any) -> None:
+ if isinstance(val, _NickUserHost):
+ val.set_on_update(lambda: self._on_update(key))
self._dict[key] = val
self._on_update(key)
class _ChannelDb(_Db):
- _completable_users: _CompletableStringsList
+ _completable_user_ids: _CompletableStringsList
# topic: str
# channel_modes: str
class SharedClientDbFields(IrcConnSetup):
'API for fields shared directly in name and type with TUI.'
connection_state: str
- client_host: str
- nickname: str
sasl_account: str
sasl_auth_state: str
user_modes: str
- username: str
+ users: Any
+ _channels: dict[str, Any]
+
+ def _purge_users(self) -> None:
+ to_keep = {'me'}
+ for chan in self._channels.values():
+ to_keep |= set(chan.user_ids)
+ for user_id in [id_ for id_ in self.users.keys if id_ not in to_keep]:
+ del self.users[user_id]
+ def chans_of_user(self, user_id: str) -> tuple[str, ...]:
+ 'Return names of channels user of user_id currently participates in.'
+ return tuple(k for k, v in self._channels.items()
+ if user_id in v.user_ids)
-class _NickUserHost(NamedTuple):
- nick: str
- user: str
- host: str
+
+@dataclass
+class NickUserHost:
+ 'Combination of nickname, username on host, and host.'
+ nick: str = '?'
+ user: str = '?'
+ host: str = '?'
+
+ def copy(self) -> Self:
+ 'Produce copy not subject to later attribute changes on original.'
+ return self.__class__(**dc_asdict(self))
+
+
+class _NickUserHost(NickUserHost):
+ _on_update: Callable
def __str__(self) -> str:
return f'{self.nick}!{self.user}@{self.host}'
assert len(toks) == 3
return cls(*toks)
+ def set_on_update(self, on_update: Callable) -> None:
+ 'Caller of on_update with path= set to name.'
+ self._on_update = on_update
+
+ def __setattr__(self, key: str, value: Any) -> None:
+ if key == 'nickuserhost' and isinstance(value, _NickUserHost):
+ self.nick = value.nick
+ self.user = value.user
+ self.host = value.host
+ else:
+ super().__setattr__(key, value)
+ if key != '_on_update' and hasattr(self, '_on_update'):
+ self._on_update()
+
class _ClientDb(_Db, SharedClientDbFields):
caps: _UpdatingDict
isupports: _UpdatingDict
+ users: _UpdatingDict
_completable_motd: _CompletableStringsList
_channels: dict[str, _ChannelDb]
- def __init__(self, **kwargs) -> None:
- super().__init__(**kwargs)
- self._types['_nick_user_host'] = _NickUserHost
-
- def __setattr__(self, key: str, value) -> None:
- super().__setattr__(key, value)
- if key == 'nickname':
- self.nick_wanted = value
-
- @property
- def _nick_user_host(self) -> _NickUserHost:
- return _NickUserHost(self.nickname, self.username, self.client_host)
-
- @_nick_user_host.setter
- def _nick_user_host(self, nick_user_host: _NickUserHost) -> None:
- self.nickname = nick_user_host.nick
- self.username = nick_user_host.user
- self.client_host = nick_user_host.host
+ def user_id(self, query: str | _NickUserHost) -> str:
+ 'Return user_id for nickname of entire NickUserHost, create if none.'
+ nick = query if isinstance(query, str) else query.nick
+ matches = [id_ for id_ in self.users.keys
+ if self.users[id_].nick == nick]
+ assert len(matches) < 2
+ id_ = matches[0] if matches else str(uuid4())
+ if isinstance(query, _NickUserHost):
+ self.users[id_] = query
+ elif not matches:
+ self.users[id_] = _NickUserHost(query)
+ return id_
+
+ def remove_user_from_channel(self, user_id: str, chan_name: str) -> None:
+ 'Remove user from channel, check that user deleted if that was last.'
+ self.chan(chan_name).remove_completable('user_ids', user_id, True)
+ if user_id == 'me':
+ self.del_chan(chan_name)
+ self._purge_users()
+
+ def remove_user(self, user_id: str) -> tuple[str, ...]:
+ 'Run remove_user_from_channel on all channels user is in.'
+ affected_chans = self.chans_of_user(user_id)
+ for chan_name in affected_chans:
+ self.remove_user_from_channel(user_id, chan_name)
+ return affected_chans
def needs_arg(self, key: str) -> bool:
'Reply if attribute of key may reasonably be addressed without an arg.'
def del_chan(self, name: str) -> None:
'Remove DB for channel of name.'
del self._channels[name]
+ self._purge_users()
self._on_update(name)
def chan(self, name: str) -> _ChannelDb:
on_update=lambda k: self._on_update(name, k))
return self._channels[name]
- def chans_of_user(self, user: str) -> dict[str, _ChannelDb]:
- 'Return part of channels dictionary for channels user is currently in.'
- return {k: v for k, v in self._channels.items() if user in v.users}
-
class Client(ABC, ClientQueueMixin):
'Abstracts socket connection, loop over it, and handling messages from it.'
self.client_id = conn_setup.hostname
super().__init__(client_id=self.client_id, **kwargs)
self._db = _ClientDb(on_update=self._on_update)
- self._db.username = getuser()
+ self._db.users['me'] = _NickUserHost('?', getuser(), '?')
self._caps = _CapsManager(self.send, self._db.caps)
for k in conn_setup.__annotations__:
setattr(self._db, k, getattr(conn_setup, k))
assert self.conn is not None
self._db.connection_state = 'connected'
self._caps.start_negotation()
- self.send(IrcMessage(verb='USER', params=(self._db.username, '0', '*',
- self._db.realname)))
+ self.send(IrcMessage(verb='USER', params=(
+ self._db.users['me'].user.lstrip('~'),
+ '0', '*', self._db.realname)))
self.send(IrcMessage(verb='NICK', params=(self._db.nick_wanted,)))
@abstractmethod
for name in self._db.chan_names:
self._db.del_chan(name)
self._db.isupports.clear()
- self._db.nickname = ''
+ self._db.users['me'].nick = '?'
self._db.sasl_auth_state = ''
def on_handled_loop_exception(self, e: IrcConnAbortException) -> None:
return {'id': msg_tok, 'db': self._db.chan(msg_tok)
} if msg_tok[0] == '#' else None
if ex_tok is _MsgTok.NICKNAME:
- return msg_tok if msg_tok[0] not in '~&@%+# ' else None
+ return (msg_tok if msg_tok[0] not in _ILLEGAL_NICK_FIRSTCHARS
+ else None)
if ex_tok is _MsgTok.NICK_USER_HOST:
try:
return _NickUserHost.from_str(msg_tok)
for idx, ex_tok in enumerate(ex_tok_fields):
ex_tok, key = ((ex_tok[0], ex_tok[1])
if isinstance(ex_tok, tuple) else (ex_tok, ''))
- task, key = key.split(':', maxsplit=1) if key else ('', '')
- if task:
+ tasks_, key = key.split(':', maxsplit=1) if key else ('', '')
+ for task in tasks_.split(','):
tasks[task] = tasks.get(task, []) + [key]
to_return[key] = param_match(ex_tok, msg_tok_fields[idx])
if to_return[key] is None:
for arg in args:
if task == 'set_db_attr':
setattr(self._db, arg, ret[arg])
+ if task == 'set_me_attr':
+ setattr(self._db.users['me'], arg, ret[arg])
+ if task == 'set_user':
+ self._db.user_id(ret[arg])
if ret['verb'] == '005': # RPL_ISUPPORT
for item in ret['isupports']:
toks = item.split('=', maxsplit=1)
self._db.isupports[toks[0]] = (toks[1] if len(toks) > 1
else '')
elif ret['verb'] == '353': # RPL_NAMREPLY
- for name in ret['names']:
- ret['channel']['db'].append_completable('users',
- name.lstrip('~&@%+'))
+ for id_ in [self._db.user_id(name.lstrip(_ILLEGAL_NICK_FIRSTCHARS))
+ for name in ret['names']]:
+ ret['channel']['db'].append_completable('user_ids', id_)
elif ret['verb'] == '366': # RPL_ENDOFNAMES
- ret['channel']['db'].declare_complete('users')
+ ret['channel']['db'].declare_complete('user_ids')
elif ret['verb'] == '372': # RPL_MOTD
self._db.append_completable('motd', ret['line'])
elif ret['verb'] == '376': # RPL_ENDOFMOTD
target=ret['target'], alert=True)
elif ret['verb'] == '432': # ERR_ERRONEOUSNICKNAME
alert = 'nickname refused for bad format'
- if 'nickname' not in ret:
+ if 'nick' not in ret:
alert += ', giving up'
self.close()
self._log(alert, alert=True)
self._caps.end_negotiation()
elif ret['verb'] == 'ERROR':
self.close()
- elif ret['verb'] == 'JOIN' and ret['joiner'].nick != self._db.nickname:
+ elif ret['verb'] == 'JOIN' and ret['joiner'] != self._db.users['me']:
ret['channel']['db'].append_completable(
- 'users', ret['joiner'].nick, True)
+ 'user_ids', self._db.user_id(ret['joiner']), True)
elif ret['verb'] == 'NICK':
- if ret['named'].nick == self._db.nickname:
- self._db.nickname = ret['nickname']
- else:
- for id_, ch in self._db.chans_of_user(ret['named'].nick
- ).items():
- ch.remove_completable('users', ret['named'].nick, True)
- ch.append_completable('users', ret['nickname'], True)
- self._log(f'{ret["named"]} becomes {ret["nickname"]}',
- scope=LogScope.CHAT, target=id_)
+ user_id = self._db.user_id(ret['named'])
+ self._db.users[user_id].nick = ret['nick']
+ if user_id == 'me':
+ self._db.nick_wanted = ret['nick']
elif ret['verb'] in {'NOTICE', 'PRIVMSG'}:
kw: dict[str, bool | str | LogScope] = {
'as_notice': msg.verb == 'NOTICE'}
if 'sender' in ret: # not just server message
kw |= {'sender': ret['sender'].nick, 'scope': LogScope.CHAT,
- 'target': (ret['sender'].nick if 'nickname' in ret
+ 'target': (ret['sender'].nick if 'nick' in ret
else ret['channel']['id'])}
self._log(ret['message'], out=False, **kw)
elif ret['verb'] == 'PART':
- if ret['parter'].nick == self._db.nickname:
- self._db.del_chan(ret['channel']['id'])
- else:
- ret['channel']['db'].remove_completable(
- 'users', ret['parter'].nick, True)
+ self._db.remove_user_from_channel(self._db.user_id(ret['parter']),
+ ret['channel']['id'])
elif ret['verb'] == 'PING':
self.send(IrcMessage(verb='PONG', params=(ret['reply'],)))
elif ret['verb'] == 'QUIT':
- for id_, ch in self._db.chans_of_user(ret['quitter'].nick).items():
- ch.remove_completable('users', ret['quitter'].nick, True)
+ for chan in self._db.remove_user(self._db.user_id(ret['quitter'])):
self._log(f'{ret["quitter"]} quits: {ret["message"]}',
- LogScope.CHAT, target=id_)
+ LogScope.CHAT, target=chan)
@dataclass