3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-11-01 17:29:21 +01:00
PyLink/protocols/clientbot.py
2016-07-21 18:49:01 -07:00

398 lines
15 KiB
Python

import time
import string
from pylinkirc import utils, conf
from pylinkirc.log import log
from pylinkirc.classes import Protocol, IrcUser, IrcServer
class ClientbotWrapperProtocol(Protocol):
def __init__(self, irc):
super().__init__(irc)
# This is just a fallback. Actual casemapping is fetched by handle_005()
self.casemapping = 'ascii'
self.caps = {}
# Initialize counter-based pseudo UID generators
self.uidgen = utils.PUIDGenerator('PUID')
self.sidgen = utils.PUIDGenerator('PSID')
def _expandPUID(self, uid):
"""
Returns the real nick for the given PUID.
"""
if uid in self.irc.users:
nick = self.irc.users[uid].nick
log.debug('(%s) Mangling target PUID %s to nick %s', self.irc.name, uid, nick)
return nick
return uid
def _formatText(self, source, text):
"""
Formats text with the given sender as a prefix.
"""
if self.irc.pseudoclient and source == self.irc.pseudoclient.uid:
return text
else:
# TODO: configurable formatting
return '<%s> %s' % (self.irc.getFriendlyName(source), text)
def connect(self):
"""Initializes a connection to a server."""
ts = self.irc.start_ts
f = self.irc.send
# TODO: fetch channel/user/prefix modes from RPL_ISUPPORT.
#self.irc.prefixmodes = {'q': '~', 'a': '&', 'o': '@', 'h': '%', 'v': '+'}
# HACK: Replace the SID from the config options with our own.
old_sid = self.irc.sid
self.irc.sid = sid = self.sidgen.next_uid()
self.irc.servers[sid] = self.irc.servers[old_sid]
del self.irc.servers[old_sid]
sendpass = self.irc.serverdata.get("sendpass")
if sendpass:
f('PASS %s' % sendpass)
# This is a really gross hack to get the defined NICK/IDENT/HOST/GECOS.
# But this connection stuff is done before any of the spawnClient stuff in
# services_support fires.
f('NICK %s' % (self.irc.serverdata.get('pylink_nick') or conf.conf["bot"].get("nick", "PyLink")))
ident = self.irc.serverdata.get('pylink_ident') or conf.conf["bot"].get("ident", "pylink")
f('USER %s 8 * %s' % (ident, # TODO: per net realnames or hostnames aren't implemented yet.
conf.conf["bot"].get("realname", "PyLink Clientbot")))
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):
"""
STUB: Pretends to spawn a new client with a subset of the given options.
"""
server = server or self.irc.sid
uid = self.uidgen.next_uid()
ts = ts or int(time.time())
realname = realname or ''
log.debug('(%s) spawnClient stub called, saving nick %s as PUID %s', self.irc.name, nick, uid)
u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, manipulatable=manipulatable)
log.debug('(%s) self.irc.users: %s', self.irc.name, self.irc.users)
self.irc.servers[server].users.add(uid)
return u
def spawnServer(self, name, sid=None, uplink=None, desc=None, endburst_delay=0):
"""
STUB: Pretends to spawn a new server with a subset of the given options.
"""
name = name.lower()
sid = self.sidgen.next_sid()
self.irc.servers[sid] = IrcServer(uplink, name)
return sid
def invite(self, client, target, channel):
"""Invites a user to a channel."""
self.irc.send('INVITE %s %s' % (self.irc.getFriendlyName(target), channel))
def join(self, client, channel):
"""STUB: Joins a user to a channel."""
channel = self.irc.toLower(channel)
self.irc.channels[channel].users.add(client)
self.irc.users[client].channels.add(channel)
# Only joins for the main PyLink client are actually forwarded. Others are ignored.
if self.irc.pseudoclient and client == self.irc.pseudoclient.uid:
self.irc.send('JOIN %s' % channel)
else:
log.debug('(%s) join: faking JOIN of client %s/%s to %s', self.irc.name, client,
self.irc.getFriendlyName(client), channel)
def kick(self, source, channel, target, reason=''):
"""Sends channel kicks."""
# TODO: handle kick failures and send rejoin hooks for the target
reason = self._formatText(source, reason)
self.irc.send('KICK %s %s :%s' % (channel, self._expandPUID(target), reason))
# Don't update our state here: wait for the IRCd to send an acknowledgement instead.
def message(self, source, target, text, notice=False):
"""Sends messages to the target."""
command = 'NOTICE' if notice else 'PRIVMSG'
target = self._expandPUID(target)
self.irc.send('%s %s :%s' % (command, target, self._formatText(source, text)))
def nick(self, source, newnick):
"""STUB: Sends NICK changes."""
if source == self.irc.pseudoclient.uid:
self.irc.send('NICK :%s' % (channel, self._expandPUID(target), reason))
self.irc.users[source].nick = newnick
def notice(self, source, target, text):
"""Sends notices to the target."""
# Wrap around message(), which does all the text formatting for us.
self.message(source, target, text, notice=True)
def ping(self, source=None, target=None):
"""
Sends PING to the uplink.
"""
if self.irc.uplink:
self.irc.send('PING %s' % self.irc.getFriendlyName(self.irc.uplink))
def part(self, source, channel, reason=''):
"""STUB: Parts a user from a channel."""
self.irc.channels[channel].removeuser(source)
self.irc.users[source].channels.discard(channel)
# Only parts for the main PyLink client are actually forwarded. Others are ignored.
if self.irc.pseudoclient and source == self.irc.pseudoclient.uid:
self.irc.send('PART %s :%s' % (channel, reason))
def quit(self, source, reason):
"""STUB: Quits a client."""
self.removeClient(source)
def sjoin(self, server, channel, users, ts=None, modes=set()):
"""STUB: bursts joins from a server."""
# This stub only updates the state internally with the users
# given. modes and TS are currently ignored.
puids = {u[-1] for u in users}
for user in puids:
self.irc.users[user].channels.add(channel)
self.irc.channels[channel].users |= puids
def squit(self, source, target, text):
"""STUB: SQUITs a server."""
# What this actually does is just handle the SQUIT internally: i.e.
# Removing pseudoclients and pseudoservers.
self._squit(source, target, text)
def _stub(self, *args):
"""Stub outgoing command function (does nothing)."""
return
kill = away = mode = topic = topicBurst = knock = updateClient = numeric = _stub
def updateClient(self, target, field, text):
"""Updates the known ident, host, or realname of a client."""
if field == 'IDENT':
self.irc.users[target].ident = text
elif field == 'HOST':
self.irc.users[target].host = text
elif field in ('REALNAME', 'GECOS'):
self.irc.users[target].realname = text
else:
raise NotImplementedError
def handle_events(self, data):
"""Event handler for the RFC1459/2812 (clientbot) protocol."""
data = data.split(" ")
try:
args = self.parsePrefixedArgs(data)
sender = args[0]
command = args[1]
args = args[2:]
except IndexError:
# Raw command without an explicit sender; assume it's being sent by our uplink.
args = self.parseArgs(data)
idsource = sender = self.irc.uplink
command = args[0]
args = args[1:]
else:
# PyLink as a services framework expects UIDs and SIDs for everythiung. Since we connect
# as a bot here, there's no explicit user introduction, so we're going to generate
# pseudo-uids and pseudo-sids as we see prefixes.
if '!' not in sender:
# Sender is a server name.
idsource = self._getSid(sender)
if idsource not in self.irc.servers:
idsource = self.spawnServer(sender)
else:
# Sender is a nick!user@host prefix. Split it into its relevant parts.
nick, ident, host = utils.splitHostmask(sender)
idsource = self.irc.nickToUid(nick)
if not idsource:
idsource = self.spawnClient(nick, ident, host, server=self.irc.uplink).uid
try:
func = getattr(self, 'handle_'+command.lower())
except AttributeError: # unhandled command
pass
else:
parsed_args = func(idsource, command, args)
if parsed_args is not None:
return [idsource, command, parsed_args]
def handle_001(self, source, command, args):
"""
Handles 001 / RPL_WELCOME.
"""
# enumerate our uplink
self.irc.uplink = source
def handle_005(self, source, command, args):
"""
Handles 005 / RPL_ISUPPORT.
"""
self.caps.update(self.parseCapabilities(args[1:-1]))
log.debug('(%s) handle_005: self.caps is %s', self.irc.name, self.caps)
if 'CHANMODES' in self.caps:
self.irc.cmodes['*A'], self.irc.cmodes['*B'], self.irc.cmodes['*C'], self.irc.cmodes['*D'] = \
self.caps['CHANMODES'].split(',')
log.debug('(%s) handle_005: cmodes: %s', self.irc.name, self.irc.cmodes)
if 'USERMODES' in self.caps:
self.irc.umodes['*A'], self.irc.umodes['*B'], self.irc.umodes['*C'], self.irc.umodes['*D'] = \
self.caps['USERMODES'].split(',')
log.debug('(%s) handle_005: umodes: %s', self.irc.name, self.irc.umodes)
self.casemapping = self.caps.get('CASEMAPPING', self.casemapping)
log.debug('(%s) handle_005: casemapping set to %s', self.irc.name, self.casemapping)
if 'PREFIX' in self.caps:
self.irc.prefixmodes = self.parsePrefixes(self.caps['PREFIX'])
log.debug('(%s) handle_005: prefix modes set to %s', self.irc.name, self.irc.prefixmodes)
if not self.irc.connected.is_set():
self.irc.connected.set()
# Run autoperform commands.
for line in self.irc.serverdata.get("autoperform", []):
self.irc.send(line)
return {'parse_as': 'ENDBURST'}
def handle_353(self, source, command, args):
"""
Handles 353 / RPL_NAMREPLY.
"""
# <- :charybdis.midnight.vpn 353 ice = #test :ice @GL
# Mark "@"-type channels as secret automatically, per RFC2812.
channel = self.irc.toLower(args[2])
if args[1] == '@':
self.irc.applyModes(channel, [('+s', None)])
names = set()
modes = set()
prefix_to_mode = {v:k for k, v in self.irc.prefixmodes.items()}
prefixes = ''.join(self.irc.prefixmodes.values())
for name in args[-1].split():
nick = name.lstrip(prefixes)
# Get the PUID for the given nick. If one doesn't exist, spawn
# a new virtual user. TODO: wait for WHO responses for each nick before
# spawning in order to get a real ident/host.
idsource = self.irc.nickToUid(nick) or self.spawnClient(nick, server=self.irc.uplink).uid
# Queue these virtual users to be joined if they're not already in the channel.
if idsource not in self.irc.channels[channel].users:
names.add(idsource)
self.irc.users[idsource].channels.add(channel)
# Process prefix modes
for char in name:
if char in self.irc.prefixmodes.values():
modes.add(('+' + prefix_to_mode[char], idsource))
else:
break
# Statekeeping: make sure the channel's user list is updated!
self.irc.channels[channel].users |= names
self.irc.applyModes(channel, modes)
log.debug('(%s) handle_353: adding users %s to %s', self.irc.name, names, channel)
log.debug('(%s) handle_353: adding modes %s to %s', self.irc.name, modes, channel)
return {'channel': channel, 'users': names, 'modes': self.irc.channels[channel].modes,
'parse_as': "JOIN"}
def handle_join(self, source, command, args):
"""
Handles incoming JOINs.
"""
# <- :GL|!~GL@127.0.0.1 JOIN #whatever
channel = self.irc.toLower(args[0])
self.join(source, channel)
return {'channel': channel, 'users': [source], 'modes': self.irc.channels[channel].modes}
def handle_kick(self, source, command, args):
"""
Handles incoming KICKs.
"""
# <- :GL!~gl@127.0.0.1 KICK #whatever GL| :xd
channel = self.irc.toLower(args[0])
target = self.irc.nickToUid(args[1])
try:
reason = args[2]
except IndexError:
reason = ''
self.part(target, channel, reason)
return {'channel': channel, 'target': target, 'text': reason}
def handle_nick(self, source, command, args):
# <- :GL|!~GL@127.0.0.1 NICK :GL_
oldnick = self.irc.users[source].nick
self.nick(source, args[0])
return {'newnick': args[0], 'oldnick': oldnick}
def handle_part(self, source, command, args):
"""
Handles incoming PARTs.
"""
# <- :GL|!~GL@127.0.0.1 PART #whatever
channels = list(map(self.irc.toLower, args[0].split(',')))
try:
reason = args[1]
except IndexError:
reason = ''
for channel in channels:
self.part(source, channel, reason)
return {'channels': channels, 'text': reason}
def handle_ping(self, source, command, args):
"""
Handles incoming PING requests.
"""
self.irc.send('PONG :%s' % args[0])
def handle_pong(self, source, command, args):
"""
Handles incoming PONG.
"""
if source == self.irc.uplink:
self.irc.lastping = time.time()
def handle_privmsg(self, source, command, args):
"""Handles incoming PRIVMSG/NOTICE."""
# <- :sender PRIVMSG #dev :afasfsa
# <- :sender NOTICE somenick :afasfsa
target = args[0]
# We use lowercase channels internally.
if utils.isChannel(target):
target = self.irc.toLower(target)
else:
target = self._getUid(target)
return {'target': target, 'text': args[1]}
def handle_quit(self, source, command, args):
"""Handles incoming QUITs."""
self.quit(source, args[0])
return {'text': args[0]}
handle_notice = handle_privmsg
Class = ClientbotWrapperProtocol