mirror of
https://github.com/jlu5/PyLink.git
synced 2024-12-25 20:22:45 +01:00
2864c41d7a
Since this is based off TS6, both initial PING and EOB are handled as endburst. However, ENDBURST should only be called once per session, or things are respawned automatically and nick collisions happen.
271 lines
11 KiB
Python
271 lines
11 KiB
Python
import time
|
|
import sys
|
|
import os
|
|
import re
|
|
|
|
curdir = os.path.dirname(__file__)
|
|
sys.path += [curdir, os.path.dirname(curdir)]
|
|
import utils
|
|
from log import log
|
|
|
|
from classes import *
|
|
from ts6 import *
|
|
|
|
class HybridProtocol(TS6Protocol):
|
|
def __init__(self, irc):
|
|
# This protocol module inherits from the TS6 protocol.
|
|
super().__init__(irc)
|
|
|
|
self.casemapping = 'ascii'
|
|
self.caps = {}
|
|
self.hook_map = {'EOB': 'ENDBURST', 'TBURST': 'TOPIC', 'SJOIN': 'JOIN'}
|
|
self.has_eob = False
|
|
|
|
def connect(self):
|
|
"""Initializes a connection to a server."""
|
|
ts = self.irc.start_ts
|
|
self.has_eob = False
|
|
f = self.irc.send
|
|
|
|
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L80
|
|
cmodes = {
|
|
# TS6 generic modes:
|
|
'op': 'o', 'halfop': 'h', 'voice': 'v', 'ban': 'b', 'key': 'k',
|
|
'limit': 'l', 'moderated': 'm', 'noextmsg': 'n',
|
|
'secret': 's', 'topiclock': 't',
|
|
# hybrid-specific modes:
|
|
'blockcolor': 'c', 'inviteonly': 'i', 'noctcp': 'C',
|
|
'regmoderated': 'M', 'operonly': 'O', 'regonly': 'R',
|
|
'sslonly': 'S', 'banexception': 'e', 'noknock': 'p',
|
|
'registered': 'r', 'invex': 'I',
|
|
# Now, map all the ABCD type modes:
|
|
'*A': 'beI', '*B': 'k', '*C': 'l', '*D': 'cimnprstCMORS'
|
|
}
|
|
|
|
self.irc.cmodes = cmodes
|
|
|
|
umodes = {
|
|
'oper': 'o', 'invisible': 'i', 'wallops': 'w', 'locops': 'l',
|
|
'cloak': 'x', 'hidechans': 'p', 'regdeaf': 'R', 'deaf': 'D',
|
|
'callerid': 'g', 'admin': 'a', 'deaf_commonchan': 'G', 'hideoper': 'H',
|
|
'webirc': 'W', 'sno_clientconnections': 'c', 'sno_badclientconnections': 'u',
|
|
'sno_rejectedclients': 'j', 'sno_skill': 'k', 'sno_fullauthblock': 'f',
|
|
'sno_remoteclientconnections': 'F', 'sno_admin_requests': 'y', 'debug': 'd',
|
|
'sno_nickchange': 'n', 'hideidle': 'q', 'registered': 'r',
|
|
'snomask': 's', 'ssl': 'S', 'sno_server_connects': 'e', 'sno_botfloods': 'b',
|
|
# Now, map all the ABCD type modes:
|
|
'*A': '', '*B': '', '*C': '', '*D': 'DFGHRSWabcdefgijklnopqrsuwxy'
|
|
}
|
|
|
|
self.irc.umodes = umodes
|
|
|
|
# halfops is mandatory on Hybrid
|
|
self.irc.prefixmodes = {'o': '@', 'h': '%', 'v': '+'}
|
|
|
|
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L55
|
|
f('PASS %s TS 6 %s' % (self.irc.serverdata["sendpass"], self.irc.sid))
|
|
|
|
# We request the following capabilities (for hybrid):
|
|
|
|
# ENCAP: message encapsulation for certain commands
|
|
# EX: Support for ban exemptions (+e)
|
|
# IE: Support for invite exemptions (+e)
|
|
# CHW: Allow sending messages to @#channel and the like.
|
|
# KNOCK: Support for /knock
|
|
# SVS: Deal with extended NICK/UID messages that contain service IDs/stamps
|
|
# TBURST: Topic Burst command; we send this in topicBurst
|
|
# DLN: DLINE command
|
|
# UNDLN: UNDLINE command
|
|
# KLN: KLINE command
|
|
# UNKLN: UNKLINE command
|
|
# HOPS: Supports HALFOPS
|
|
# CHW: Can do channel wall (@#)
|
|
# CLUSTER: Supports server clustering
|
|
# EOB: Supports EOB (end of burst) command
|
|
f('CAPAB :TBURST DLN KNOCK UNDLN UNKLN KLN ENCAP IE EX HOPS CHW SVS CLUSTER EOB QS')
|
|
|
|
f('SERVER %s 0 :%s' % (self.irc.serverdata["hostname"],
|
|
self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc']))
|
|
|
|
# send endburst now
|
|
self.irc.send(':%s EOB' % (self.irc.sid,))
|
|
|
|
def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(),
|
|
server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None,
|
|
manipulatable=False):
|
|
"""
|
|
Spawns a new client with the given options.
|
|
|
|
Note: No nick collision / valid nickname checks are done here; it is
|
|
up to plugins to make sure they don't introduce anything invalid.
|
|
"""
|
|
|
|
server = server or self.irc.sid
|
|
if not self.irc.isInternalServer(server):
|
|
raise ValueError('Server %r is not a PyLink server!' % server)
|
|
|
|
uid = self.uidgen[server].next_uid()
|
|
|
|
ts = ts or int(time.time())
|
|
realname = realname or self.irc.botdata['realname']
|
|
realhost = realhost or host
|
|
raw_modes = self.irc.joinModes(modes)
|
|
u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
|
|
realhost=realhost, ip=ip, manipulatable=manipulatable)
|
|
self.irc.applyModes(uid, modes)
|
|
self.irc.servers[server].users.add(uid)
|
|
self._send(server, "UID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} "
|
|
"* :{realname}".format(ts=ts, host=host,
|
|
nick=nick, ident=ident, uid=uid,
|
|
modes=raw_modes, ip=ip, realname=realname))
|
|
return u
|
|
|
|
def updateClient(self, target, field, text):
|
|
"""Updates the ident, host, or realname of a PyLink client."""
|
|
# https://github.com/ircd-hybrid/ircd-hybrid/blob/58323b8/modules/m_svsmode.c#L40-L103
|
|
# parv[0] = command
|
|
# parv[1] = nickname <-- UID works too -GLolol
|
|
# parv[2] = TS <-- Of the user, not the current time. -GLolol
|
|
# parv[3] = mode
|
|
# parv[4] = optional argument (services account, vhost)
|
|
field = field.upper()
|
|
|
|
ts = self.irc.users[target].ts
|
|
|
|
if field == 'HOST':
|
|
self.irc.users[target].host = text
|
|
# On Hybrid, it appears that host changing is actually just forcing umode
|
|
# "+x <hostname>" on the target. -GLolol
|
|
self._send(self.irc.sid, 'SVSMODE %s %s +x %s' % (target, ts, text))
|
|
else:
|
|
raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field)
|
|
|
|
def topicBurst(self, numeric, target, text):
|
|
"""Sends a topic change from a PyLink server. This is usually used on burst."""
|
|
# <- :0UY TBURST 1459308205 #testchan 1459309379 dan!~d@localhost :sdf
|
|
if not self.irc.isInternalServer(numeric):
|
|
raise LookupError('No such PyLink server exists.')
|
|
|
|
ts = self.irc.channels[target].ts
|
|
servername = self.irc.servers[numeric].name
|
|
|
|
self._send(numeric, 'TBURST %s %s %s %s :%s' % (ts, target, int(time.time()), servername, text))
|
|
self.irc.channels[target].topic = text
|
|
self.irc.channels[target].topicset = True
|
|
|
|
# command handlers
|
|
|
|
def handle_capab(self, numeric, command, args):
|
|
# We only get a list of keywords here. Hybrid obviously assumes that
|
|
# we know what modes it supports (indeed, this is a standard list).
|
|
# <- CAPAB :UNDLN UNKLN KLN TBURST KNOCK ENCAP DLN IE EX HOPS CHW SVS CLUSTER EOB QS
|
|
self.irc.caps = caps = args[0].split()
|
|
for required_cap in ('EX', 'IE', 'SVS', 'EOB', 'HOPS', 'QS', 'TBURST', 'SVS'):
|
|
if required_cap not in caps:
|
|
raise ProtocolError('%s not found in TS6 capabilities list; this is required! (got %r)' % (required_cap, caps))
|
|
|
|
log.debug('(%s) self.irc.connected set!', self.irc.name)
|
|
self.irc.connected.set()
|
|
|
|
def handle_uid(self, numeric, command, args):
|
|
"""
|
|
Handles Hybrid-style UID commands (user introduction). This is INCOMPATIBLE
|
|
with standard TS6 implementations, as the arguments are slightly different.
|
|
"""
|
|
# <- :0UY UID dan 1 1451041551 +Facdeiklosuw ~ident localhost 127.0.0.1 0UYAAAAAB * :realname
|
|
nick = args[0]
|
|
ts, modes, ident, host, ip, uid, account, realname = args[2:10]
|
|
if account == '*':
|
|
account = None
|
|
log.debug('(%s) handle_uid: got args nick=%s ts=%s uid=%s ident=%s '
|
|
'host=%s realname=%s ip=%s', self.irc.name, nick, ts, uid,
|
|
ident, host, realname, ip)
|
|
|
|
self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, host, ip)
|
|
|
|
parsedmodes = self.irc.parseModes(uid, [modes])
|
|
log.debug('(%s) handle_uid: Applying modes %s for %s', self.irc.name, parsedmodes, uid)
|
|
self.irc.applyModes(uid, parsedmodes)
|
|
self.irc.servers[numeric].users.add(uid)
|
|
|
|
# Call the OPERED UP hook if +o is being added to the mode list.
|
|
if ('+o', None) in parsedmodes:
|
|
self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': 'IRC_Operator'}])
|
|
|
|
# Set the account name if present
|
|
if account:
|
|
self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}])
|
|
|
|
return {'uid': uid, 'ts': ts, 'nick': nick, 'realname': realname, 'host': host, 'ident': ident, 'ip': ip}
|
|
|
|
def handle_tburst(self, numeric, command, args):
|
|
"""Handles incoming topic burst (TBURST) commands."""
|
|
# <- :0UY TBURST 1459308205 #testchan 1459309379 dan!~d@localhost :sdf
|
|
channel = args[1].lower()
|
|
ts = args[2]
|
|
setter = args[3]
|
|
topic = args[-1]
|
|
self.irc.channels[channel].topic = topic
|
|
self.irc.channels[channel].topicset = True
|
|
return {'channel': channel, 'setter': setter, 'ts': ts, 'text': topic}
|
|
|
|
def handle_eob(self, numeric, command, args):
|
|
log.debug('(%s) end of burst received', self.irc.name)
|
|
if not self.has_eob: # Only call ENDBURST hooks if we haven't already.
|
|
return {}
|
|
|
|
self.has_eob = True
|
|
|
|
def handle_svsmode(self, numeric, command, args):
|
|
"""
|
|
Handles SVSMODE, which is used for sending services metadata
|
|
(vhosts, account logins), and other forced usermode changes.
|
|
"""
|
|
|
|
target = args[0]
|
|
ts = args[1]
|
|
modes = args[2:]
|
|
parsedmodes = self.irc.parseModes(target, modes)
|
|
|
|
for modepair in parsedmodes:
|
|
if modepair[0] == '+d':
|
|
# Login sequence (tested with Anope 2.0.4-git):
|
|
# A mode change +d accountname is used to propagate logins,
|
|
# before setting umode +r on the target.
|
|
# <- :5ANAAAAAG SVSMODE 5HYAAAAAA 1460175209 +d GL
|
|
# <- :5ANAAAAAG SVSMODE 5HYAAAAAA 1460175209 +r
|
|
|
|
# Logout sequence:
|
|
# <- :5ANAAAAAG SVSMODE 5HYAAAAAA 1460175209 +d *
|
|
# <- :5ANAAAAAG SVSMODE 5HYAAAAAA 1460175209 -r
|
|
|
|
account = args[-1]
|
|
if account == '*':
|
|
account = '' # Logout
|
|
|
|
# Send the login hook, and remove this mode from the mode
|
|
# list, as it shouldn't be parsed literally.
|
|
self.irc.callHooks([target, 'CLIENT_SERVICES_LOGIN', {'text': account}])
|
|
parsedmodes.remove(modepair)
|
|
|
|
elif modepair[0] == '+x':
|
|
# SVSMODE is also used to set cloaks on Hybrid.
|
|
# "SVSMODE 001TARGET +x some.host" would change 001TARGET's host
|
|
# to some.host, for example.
|
|
host = args[-1]
|
|
|
|
self.irc.users[target].host = host
|
|
|
|
# Propagate the hostmask change as a hook.
|
|
self.irc.callHooks([numeric, 'CHGHOST',
|
|
{'target': target, 'newhost': host}])
|
|
|
|
parsedmodes.remove(modepair)
|
|
|
|
if parsedmodes:
|
|
self.irc.applyModes(target, parsedmodes)
|
|
|
|
return {'target': target, 'modes': parsedmodes}
|
|
|
|
Class = HybridProtocol
|