diff --git a/protocols/inspircd.py b/protocols/inspircd.py index c15f5ce..0bb6490 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -8,10 +8,14 @@ curdir = os.path.dirname(__file__) sys.path += [curdir, os.path.dirname(curdir)] import utils from log import log -import proto_common - from classes import * +from ts6_common import nickClient, kickServer, kickClient, _sendKick, quitClient, \ + removeClient, partClient, messageClient, noticeClient, topicClient, parseTS6Args +from ts6_common import handle_privmsg, handle_kill, handle_kick, handle_error, \ + handle_quit, handle_nick, handle_save, handle_squit, handle_mode, handle_topic, \ + handle_notice, _send, handle_part + casemapping = 'rfc1459' # Raw commands sent from servers vary from protocol to protocol. Here, we map @@ -114,78 +118,6 @@ def sjoinServer(irc, server, channel, users, ts=None): modes=utils.joinModes(modes))) irc.channels[channel].users.update(uids) -def partClient(irc, client, channel, reason=None): - channel = utils.toLower(irc, channel) - if not utils.isInternalClient(irc, client): - log.error('(%s) Error trying to part client %r to %r (no such pseudoclient exists)', irc.name, client, channel) - raise LookupError('No such PyLink PseudoClient exists.') - msg = "PART %s" % channel - if reason: - msg += " :%s" % reason - _send(irc, client, msg) - handle_part(irc, client, 'PART', [channel]) - -def removeClient(irc, numeric): - """ - - Removes a client from our internal databases, regardless - of whether it's one of our pseudoclients or not.""" - for c, v in irc.channels.copy().items(): - v.removeuser(numeric) - # Clear empty non-permanent channels. - if not (irc.channels[c].users or ((irc.cmodes.get('permanent'), None) in irc.channels[c].modes)): - del irc.channels[c] - - sid = numeric[:3] - log.debug('Removing client %s from irc.users', numeric) - del irc.users[numeric] - log.debug('Removing client %s from irc.servers[%s]', numeric, sid) - irc.servers[sid].users.discard(numeric) - -def quitClient(irc, numeric, reason): - """ - - Quits a PyLink PseudoClient.""" - if utils.isInternalClient(irc, numeric): - _send(irc, numeric, "QUIT :%s" % reason) - removeClient(irc, numeric) - else: - raise LookupError("No such PyLink PseudoClient exists. If you're trying to remove " - "a user that's not a PyLink PseudoClient from " - "the internal state, use removeClient() instead.") - -def _sendKick(irc, numeric, channel, target, reason=None): - """ - - Sends a kick from a PyLink PseudoClient.""" - channel = utils.toLower(irc, channel) - if not reason: - reason = 'No reason given' - _send(irc, numeric, 'KICK %s %s :%s' % (channel, target, reason)) - # We can pretend the target left by its own will; all we really care about - # is that the target gets removed from the channel userlist, and calling - # handle_part() does that just fine. - handle_part(irc, target, 'KICK', [channel]) - -def kickClient(irc, numeric, channel, target, reason=None): - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _sendKick(irc, numeric, channel, target, reason=reason) - -def kickServer(irc, numeric, channel, target, reason=None): - if not utils.isInternalServer(irc, numeric): - raise LookupError('No such PyLink PseudoServer exists.') - _sendKick(irc, numeric, channel, target, reason=reason) - -def nickClient(irc, numeric, newnick): - """ - - Changes the nick of a PyLink PseudoClient.""" - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _send(irc, numeric, 'NICK %s %s' % (newnick, int(time.time()))) - irc.users[numeric].nick = newnick - def _operUp(irc, target, opertype=None): userobj = irc.users[target] try: @@ -259,29 +191,6 @@ def killClient(irc, numeric, target, reason): # We don't need to call removeClient here, since the remote server # will send a QUIT from the target if the command succeeds. -def messageClient(irc, numeric, target, text): - """ - - Sends PRIVMSG from PyLink client .""" - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _send(irc, numeric, 'PRIVMSG %s :%s' % (target, text)) - -def noticeClient(irc, numeric, target, text): - """ - - Sends NOTICE from PyLink client .""" - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _send(irc, numeric, 'NOTICE %s :%s' % (target, text)) - -def topicClient(irc, numeric, target, text): - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _send(irc, numeric, 'TOPIC %s :%s' % (target, text)) - irc.channels[target].topic = text - irc.channels[target].topicset = True - def topicServer(irc, numeric, target, text): if not utils.isInternalServer(irc, numeric): raise LookupError('No such PyLink PseudoServer exists.') @@ -369,53 +278,6 @@ def handle_pong(irc, source, command, args): if source == irc.uplink and args[1] == irc.sid: irc.lastping = time.time() -def handle_privmsg(irc, source, command, args): - # <- :70MAAAAAA PRIVMSG #dev :afasfsa - # <- :70MAAAAAA NOTICE 0ALAAAAAA :afasfsa - target = args[0] - # We use lowercase channels internally, but uppercase UIDs. - if utils.isChannel(target): - target = utils.toLower(irc, target) - return {'target': target, 'text': args[1]} - -handle_notice = handle_privmsg - -def handle_kill(irc, source, command, args): - killed = args[0] - data = irc.users.get(killed) - if data: - removeClient(irc, killed) - return {'target': killed, 'text': args[1], 'userdata': data} - -def handle_kick(irc, source, command, args): - # :70MAAAAAA KICK #endlessvoid 70MAAAAAA :some reason - channel = utils.toLower(irc, args[0]) - kicked = args[1] - handle_part(irc, kicked, 'KICK', [channel, args[2]]) - return {'channel': channel, 'target': kicked, 'text': args[2]} - -def handle_part(irc, source, command, args): - channels = utils.toLower(irc, args[0]).split(',') - for channel in channels: - # We should only get PART commands for channels that exist, right?? - irc.channels[channel].removeuser(source) - try: - irc.users[source].channels.discard(channel) - except KeyError: - log.debug("(%s) handle_part: KeyError trying to remove %r from %r's channel list?", irc.name, channel, source) - try: - reason = args[1] - except IndexError: - reason = '' - # Clear empty non-permanent channels. - if not (irc.channels[channel].users or ((irc.cmodes.get('permanent'), None) in irc.channels[channel].modes)): - del irc.channels[channel] - return {'channels': channels, 'text': reason} - -def handle_error(irc, numeric, command, args): - irc.connected.clear() - raise ProtocolError('Received an ERROR, killing!') - def handle_fjoin(irc, servernumeric, command, args): # :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :o,1SRAABIT4 v,1IOAAF53R <...> channel = utils.toLower(irc, args[0]) @@ -455,11 +317,6 @@ def handle_uid(irc, numeric, command, args): irc.servers[numeric].users.add(uid) return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip} -def handle_quit(irc, numeric, command, args): - # <- :1SRAAGB4T QUIT :Quit: quit message goes here - removeClient(irc, numeric) - return {'text': args[0]} - def handle_server(irc, numeric, command, args): # SERVER is sent by our uplink or any other server to introduce others. # <- :00A SERVER test.server * 1 00C :testing raw message syntax @@ -470,25 +327,6 @@ def handle_server(irc, numeric, command, args): irc.servers[sid] = IrcServer(numeric, servername) return {'name': servername, 'sid': args[3], 'text': sdesc} -def handle_nick(irc, numeric, command, args): - # <- :70MAAAAAA NICK GL-devel 1434744242 - oldnick = irc.users[numeric].nick - newnick = irc.users[numeric].nick = args[0] - return {'newnick': newnick, 'oldnick': oldnick, 'ts': int(args[1])} - -def handle_save(irc, numeric, command, args): - # This is used to handle nick collisions. Here, the client Derp_ already exists, - # so trying to change nick to it will cause a nick collision. On InspIRCd, - # this will simply set the collided user's nick to its UID. - - # <- :70MAAAAAA PRIVMSG 0AL000001 :nickclient PyLink Derp_ - # -> :0AL000001 NICK Derp_ 1433728673 - # <- :70M SAVE 0AL000001 1433728673 - user = args[0] - oldnick = irc.users[user].nick - irc.users[user].nick = user - return {'target': user, 'ts': int(args[1]), 'oldnick': oldnick} - def handle_fmode(irc, numeric, command, args): # <- :70MAAAAAA FMODE #chat 1433653462 +hhT 70MAAAAAA 70MAAAAAD channel = utils.toLower(irc, args[0]) @@ -498,35 +336,6 @@ def handle_fmode(irc, numeric, command, args): ts = int(args[1]) return {'target': channel, 'modes': changedmodes, 'ts': ts} -def handle_mode(irc, numeric, command, args): - # In InspIRCd, MODE is used for setting user modes and - # FMODE is used for channel modes: - # <- :70MAAAAAA MODE 70MAAAAAA -i+xc - target = args[0] - modestrings = args[1:] - changedmodes = utils.parseModes(irc, numeric, modestrings) - utils.applyModes(irc, target, changedmodes) - return {'target': target, 'modes': changedmodes} - -def handle_squit(irc, numeric, command, args): - # :70M SQUIT 1ML :Server quit by GL!gl@0::1 - split_server = args[0] - affected_users = [] - log.info('(%s) Netsplit on server %s', irc.name, split_server) - # Prevent RuntimeError: dictionary changed size during iteration - old_servers = copy(irc.servers) - for sid, data in old_servers.items(): - if data.uplink == split_server: - log.debug('Server %s also hosts server %s, removing those users too...', split_server, sid) - args = handle_squit(irc, sid, 'SQUIT', [sid, "PyLink: Automatically splitting leaf servers of %s" % sid]) - affected_users += args['users'] - for user in copy(irc.servers[split_server].users): - affected_users.append(user) - log.debug('Removing client %s (%s)', user, irc.users[user].nick) - removeClient(irc, user) - del irc.servers[split_server] - log.debug('(%s) Netsplit affected users: %s', irc.name, affected_users) - return {'target': split_server, 'users': affected_users} def handle_idle(irc, numeric, command, args): """Handle the IDLE command, sent between servers in remote WHOIS queries.""" @@ -599,7 +408,7 @@ def handle_events(irc, data): # Sanity check: set this AFTER we fetch the capabilities for the network! irc.connected.set() try: - args = proto_common.parseTS6Args(args) + args = parseTS6Args(args) numeric = args[0] command = args[1] args = args[2:] @@ -648,15 +457,6 @@ def handle_ftopic(irc, numeric, command, args): irc.channels[channel].topicset = True return {'channel': channel, 'setter': setter, 'ts': ts, 'topic': topic} -def handle_topic(irc, numeric, command, args): - # <- :70MAAAAAA TOPIC #test :test - channel = utils.toLower(irc, args[0]) - topic = args[1] - ts = int(time.time()) - irc.channels[channel].topic = topic - irc.channels[channel].topicset = True - return {'channel': channel, 'setter': numeric, 'ts': ts, 'topic': topic} - def handle_invite(irc, numeric, command, args): # <- :70MAAAAAC INVITE 0ALAAAAAA #blah 0 target = args[0] diff --git a/protocols/proto_common.py b/protocols/proto_common.py deleted file mode 100644 index 11ed38b..0000000 --- a/protocols/proto_common.py +++ /dev/null @@ -1,34 +0,0 @@ -def parseArgs(args): - """ - Parses a string of RFC1459-style arguments split into a list, where ":" may - be used for multi-word arguments that last until the end of a line. - """ - real_args = [] - for idx, arg in enumerate(args): - real_args.append(arg) - # If the argument starts with ':' and ISN'T the first argument. - # The first argument is used for denoting the source UID/SID. - if arg.startswith(':') and idx != 0: - # : is used for multi-word arguments that last until the end - # of the message. We can use list splicing here to turn them all - # into one argument. - # Set the last arg to a joined version of the remaining args - arg = args[idx:] - arg = ' '.join(arg)[1:] - # Cut the original argument list right before the multi-word arg, - # and then append the multi-word arg. - real_args = args[:idx] - real_args.append(arg) - break - return real_args - - -def parseTS6Args(args): - """ - - Similar to parseArgs(), but stripping leading colons from the first argument - of a line (usually the sender field).""" - args = parseArgs(args) - args[0] = args[0].split(':', 1)[1] - return args - diff --git a/protocols/ts6.py b/protocols/ts6.py index a28ab8d..995cd69 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -10,19 +10,15 @@ from log import log from classes import * # Shared with inspircd module because the output is the same. -from inspircd import nickClient, kickServer, kickClient, _sendKick, quitClient, \ - removeClient, partClient, messageClient, noticeClient, topicClient -from inspircd import handle_privmsg, handle_kill, handle_kick, handle_error, \ +from ts6_common import nickClient, kickServer, kickClient, _sendKick, quitClient, \ + removeClient, partClient, messageClient, noticeClient, topicClient, parseTS6Args +from ts6_common import handle_privmsg, handle_kill, handle_kick, handle_error, \ handle_quit, handle_nick, handle_save, handle_squit, handle_mode, handle_topic, \ - handle_notice -import proto_common + handle_notice, _send, handle_part casemapping = 'rfc1459' hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE'} -def _send(irc, sid, msg): - irc.send(':%s %s' % (sid, msg)) - def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None): server = server or irc.sid @@ -371,23 +367,6 @@ def handle_pong(irc, source, command, args): if source == irc.uplink: irc.lastping = time.time() -def handle_part(irc, source, command, args): - channels = utils.toLower(irc, args[0]).split(',') - # We should only get PART commands for channels that exist, right?? - for channel in channels: - irc.channels[channel].removeuser(source) - try: - irc.users[source].channels.discard(channel) - except KeyError: - log.debug("(%s) handle_part: KeyError trying to remove %r from %r's channel list?", irc.name, channel, source) - try: - reason = args[1] - except IndexError: - reason = '' - if not (irc.channels[channel].users or ((irc.cmodes.get('permanent'), None) in irc.channels[channel].modes)): - del irc.channels[channel] - return {'channels': channels, 'text': reason} - def handle_sjoin(irc, servernumeric, command, args): # parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist channel = utils.toLower(irc, args[1]) @@ -578,7 +557,7 @@ def handle_events(irc, data): log.debug('(%s) Starting delay to send ENDBURST', irc.name) endburst_timer.start() try: - args = proto_common.parseTS6Args(args) + args = parseTS6Args(args) numeric = args[0] command = args[1] diff --git a/protocols/ts6_common.py b/protocols/ts6_common.py new file mode 100644 index 0000000..4af45f1 --- /dev/null +++ b/protocols/ts6_common.py @@ -0,0 +1,251 @@ +import sys +import os + +curdir = os.path.dirname(__file__) +sys.path += [curdir, os.path.dirname(curdir)] +import utils +from log import log +from classes import * + +def _send(irc, sid, msg): + irc.send(':%s %s' % (sid, msg)) + +def parseArgs(args): + """ + Parses a string of RFC1459-style arguments split into a list, where ":" may + be used for multi-word arguments that last until the end of a line. + """ + real_args = [] + for idx, arg in enumerate(args): + real_args.append(arg) + # If the argument starts with ':' and ISN'T the first argument. + # The first argument is used for denoting the source UID/SID. + if arg.startswith(':') and idx != 0: + # : is used for multi-word arguments that last until the end + # of the message. We can use list splicing here to turn them all + # into one argument. + # Set the last arg to a joined version of the remaining args + arg = args[idx:] + arg = ' '.join(arg)[1:] + # Cut the original argument list right before the multi-word arg, + # and then append the multi-word arg. + real_args = args[:idx] + real_args.append(arg) + break + return real_args + +def parseTS6Args(args): + """ + + Similar to parseArgs(), but stripping leading colons from the first argument + of a line (usually the sender field).""" + args = parseArgs(args) + args[0] = args[0].split(':', 1)[1] + return args + +### OUTGOING COMMANDS + +def _sendKick(irc, numeric, channel, target, reason=None): + """ + + Sends a kick from a PyLink PseudoClient.""" + channel = utils.toLower(irc, channel) + if not reason: + reason = 'No reason given' + _send(irc, numeric, 'KICK %s %s :%s' % (channel, target, reason)) + # We can pretend the target left by its own will; all we really care about + # is that the target gets removed from the channel userlist, and calling + # handle_part() does that just fine. + handle_part(irc, target, 'KICK', [channel]) + +def kickClient(irc, numeric, channel, target, reason=None): + if not utils.isInternalClient(irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + _sendKick(irc, numeric, channel, target, reason=reason) + +def kickServer(irc, numeric, channel, target, reason=None): + if not utils.isInternalServer(irc, numeric): + raise LookupError('No such PyLink PseudoServer exists.') + _sendKick(irc, numeric, channel, target, reason=reason) + +def nickClient(irc, numeric, newnick): + """ + + Changes the nick of a PyLink PseudoClient.""" + if not utils.isInternalClient(irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + _send(irc, numeric, 'NICK %s %s' % (newnick, int(time.time()))) + irc.users[numeric].nick = newnick + +def removeClient(irc, numeric): + """ + + Removes a client from our internal databases, regardless + of whether it's one of our pseudoclients or not.""" + for c, v in irc.channels.copy().items(): + v.removeuser(numeric) + # Clear empty non-permanent channels. + if not (irc.channels[c].users or ((irc.cmodes.get('permanent'), None) in irc.channels[c].modes)): + del irc.channels[c] + + sid = numeric[:3] + log.debug('Removing client %s from irc.users', numeric) + del irc.users[numeric] + log.debug('Removing client %s from irc.servers[%s]', numeric, sid) + irc.servers[sid].users.discard(numeric) + +def partClient(irc, client, channel, reason=None): + channel = utils.toLower(irc, channel) + if not utils.isInternalClient(irc, client): + log.error('(%s) Error trying to part client %r to %r (no such pseudoclient exists)', irc.name, client, channel) + raise LookupError('No such PyLink PseudoClient exists.') + msg = "PART %s" % channel + if reason: + msg += " :%s" % reason + _send(irc, client, msg) + handle_part(irc, client, 'PART', [channel]) + +def quitClient(irc, numeric, reason): + """ + + Quits a PyLink PseudoClient.""" + if utils.isInternalClient(irc, numeric): + _send(irc, numeric, "QUIT :%s" % reason) + removeClient(irc, numeric) + else: + raise LookupError("No such PyLink PseudoClient exists.") + +def messageClient(irc, numeric, target, text): + """ + + Sends PRIVMSG from PyLink client .""" + if not utils.isInternalClient(irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + _send(irc, numeric, 'PRIVMSG %s :%s' % (target, text)) + +def noticeClient(irc, numeric, target, text): + """ + + Sends NOTICE from PyLink client .""" + if not utils.isInternalClient(irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + _send(irc, numeric, 'NOTICE %s :%s' % (target, text)) + +def topicClient(irc, numeric, target, text): + if not utils.isInternalClient(irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + _send(irc, numeric, 'TOPIC %s :%s' % (target, text)) + irc.channels[target].topic = text + irc.channels[target].topicset = True + +### HANDLERS + +def handle_privmsg(irc, source, command, args): + # <- :70MAAAAAA PRIVMSG #dev :afasfsa + # <- :70MAAAAAA NOTICE 0ALAAAAAA :afasfsa + target = args[0] + # We use lowercase channels internally, but uppercase UIDs. + if utils.isChannel(target): + target = utils.toLower(irc, target) + return {'target': target, 'text': args[1]} + +handle_notice = handle_privmsg + +def handle_kill(irc, source, command, args): + killed = args[0] + data = irc.users.get(killed) + if data: + removeClient(irc, killed) + return {'target': killed, 'text': args[1], 'userdata': data} + +def handle_kick(irc, source, command, args): + # :70MAAAAAA KICK #endlessvoid 70MAAAAAA :some reason + channel = utils.toLower(irc, args[0]) + kicked = args[1] + handle_part(irc, kicked, 'KICK', [channel, args[2]]) + return {'channel': channel, 'target': kicked, 'text': args[2]} + +def handle_error(irc, numeric, command, args): + irc.connected.clear() + raise ProtocolError('Received an ERROR, killing!') + +def handle_nick(irc, numeric, command, args): + # <- :70MAAAAAA NICK GL-devel 1434744242 + oldnick = irc.users[numeric].nick + newnick = irc.users[numeric].nick = args[0] + return {'newnick': newnick, 'oldnick': oldnick, 'ts': int(args[1])} + +def handle_quit(irc, numeric, command, args): + # <- :1SRAAGB4T QUIT :Quit: quit message goes here + removeClient(irc, numeric) + return {'text': args[0]} + +def handle_save(irc, numeric, command, args): + # This is used to handle nick collisions. Here, the client Derp_ already exists, + # so trying to change nick to it will cause a nick collision. On InspIRCd, + # this will simply set the collided user's nick to its UID. + + # <- :70MAAAAAA PRIVMSG 0AL000001 :nickclient PyLink Derp_ + # -> :0AL000001 NICK Derp_ 1433728673 + # <- :70M SAVE 0AL000001 1433728673 + user = args[0] + oldnick = irc.users[user].nick + irc.users[user].nick = user + return {'target': user, 'ts': int(args[1]), 'oldnick': oldnick} + +def handle_squit(irc, numeric, command, args): + # :70M SQUIT 1ML :Server quit by GL!gl@0::1 + split_server = args[0] + affected_users = [] + log.info('(%s) Netsplit on server %s', irc.name, split_server) + # Prevent RuntimeError: dictionary changed size during iteration + old_servers = irc.servers.copy() + for sid, data in old_servers.items(): + if data.uplink == split_server: + log.debug('Server %s also hosts server %s, removing those users too...', split_server, sid) + args = handle_squit(irc, sid, 'SQUIT', [sid, "PyLink: Automatically splitting leaf servers of %s" % sid]) + affected_users += args['users'] + for user in irc.servers[split_server].users.copy(): + affected_users.append(user) + log.debug('Removing client %s (%s)', user, irc.users[user].nick) + removeClient(irc, user) + del irc.servers[split_server] + log.debug('(%s) Netsplit affected users: %s', irc.name, affected_users) + return {'target': split_server, 'users': affected_users} + +def handle_mode(irc, numeric, command, args): + # In InspIRCd, MODE is used for setting user modes and + # FMODE is used for channel modes: + # <- :70MAAAAAA MODE 70MAAAAAA -i+xc + target = args[0] + modestrings = args[1:] + changedmodes = utils.parseModes(irc, numeric, modestrings) + utils.applyModes(irc, target, changedmodes) + return {'target': target, 'modes': changedmodes} + +def handle_topic(irc, numeric, command, args): + # <- :70MAAAAAA TOPIC #test :test + channel = utils.toLower(irc, args[0]) + topic = args[1] + ts = int(time.time()) + irc.channels[channel].topic = topic + irc.channels[channel].topicset = True + return {'channel': channel, 'setter': numeric, 'ts': ts, 'topic': topic} + +def handle_part(irc, source, command, args): + channels = utils.toLower(irc, args[0]).split(',') + for channel in channels: + # We should only get PART commands for channels that exist, right?? + irc.channels[channel].removeuser(source) + try: + irc.users[source].channels.discard(channel) + except KeyError: + log.debug("(%s) handle_part: KeyError trying to remove %r from %r's channel list?", irc.name, channel, source) + try: + reason = args[1] + except IndexError: + reason = '' + # Clear empty non-permanent channels. + if not (irc.channels[channel].users or ((irc.cmodes.get('permanent'), None) in irc.channels[channel].modes)): + del irc.channels[channel] + return {'channels': channels, 'text': reason}