import datetime from functools import partial from typing import NamedTuple from quarg.quassel.types import MessageType class User(NamedTuple): nick: str host: str prefix: str @classmethod def from_sender(cls, sender, prefixes=''): nick, *host = sender.split('!', 1) prefix = prefixes[0] if prefixes else '' return cls(nick, host[0] if host else '', prefix) def __repr__(self): return f'{self.prefix}{self.nick}' class Message(NamedTuple): type: MessageType time: datetime.datetime buffer: str user: User message: str @classmethod def from_backlog(cls, backlog): return cls(MessageType(backlog.type), backlog.time, backlog.buffer.buffername, User.from_sender(backlog.sender.sender, backlog.senderprefixes), backlog.message) def format_from(backlog_row): message = Message.from_backlog(backlog_row) formatter = FORMATTERS[message.type] return f'{format_timestamp(message.time)}\t{message.buffer}\t{formatter(message)}' def format_timestamp(time): # make sure to convert timestamps to local time return time.astimezone().strftime('%Y-%m-%d %H:%M:%S') def format_part(msg): if msg.message: return f'<-- {msg.user} has left {msg.buffer} ({msg.message})' return f'<-- {msg.user} has left {msg.buffer}' def format_quit(msg): if msg.message: return f'<-- {msg.user} has quit ({msg.message})' return f'<-- {msg.user} has quit' def format_kick(msg): target, *kickmsg = msg.message.split(' ', 1) if kickmsg: return f'<-* {msg.user} has kicked {target} from {msg.buffer} ({kickmsg[0]})' return f'<-* {msg.user} has kicked {target} from {msg.buffer}' def format_kill(msg): # As of 2021-04-24 not even Quassel implements printing this message [1]. # They do have a symbol [2] for it, however, so use that along with the message # [1] https://github.com/quassel/quassel/blob/285215315e6f2420724532323a4b1bccae156cb1/src/uisupport/uistyle.cpp#L950 # [2] https://github.com/quassel/quassel/blob/285215315e6f2420724532323a4b1bccae156cb1/src/uisupport/uistyle.cpp#L1079-L1080 return f'<-x {msg.message}' def format_generic(msg): return f'* {msg.message}' def parse_netsplit(splitmsg): # splitmsg contains user!host separated by #:# ... elements = splitmsg.split('#:#') # ... however, the last element contains the split servers instead servers = elements.pop().split(' ', 1) # This list can be unwieldily large. Mirror quassel's behaviour and cut off # after printing 15 users. If there were any more users than this, have # rest contain a value larger than 0 users = [User.from_sender(e) for e in elements[0:15]] rest = max(0, len(elements) - 15) return users, servers, rest def format_netsplit(has_ended, msg): users, (srv_left, srv_right), rest = parse_netsplit(msg.message) affected = ', '.join(user.nick for user in users) + (f' ({rest} more)' if rest else '') if has_ended: return f'=> Netsplit between {srv_left} and {srv_right} ended. Users joined: {affected}' return f'<= Netsplit between {srv_left} and {srv_right}. Users quit: {affected}' def fmt(string): return partial(lambda string, msg: string.format(**msg._asdict()), string) FORMATTERS = { MessageType.PRIVMSG: fmt('<{user}> {message}'), MessageType.NOTICE: fmt('[{user}] {message}'), MessageType.ACTION: fmt('-*- {user} {message}'), MessageType.NICK: fmt('<-> {user} is now known as {message}'), MessageType.MODE: fmt('*** Mode {message} by {user}'), MessageType.JOIN: fmt('--> {user} ({user.host}) has joined {buffer}'), MessageType.PART: format_part, MessageType.QUIT: format_quit, MessageType.KICK: format_kick, MessageType.KILL: format_kill, MessageType.SERVER: format_generic, # MessageType.INFO: format_generic, MessageType.ERROR: format_generic, # MessageType.DAYCHANGE: format_generic, MessageType.TOPIC: format_generic, MessageType.NETSPLIT_JOIN: partial(format_netsplit, True), MessageType.NETSPLIT_QUIT: partial(format_netsplit, False), MessageType.INVITE: format_generic, }