mirror of
https://github.com/jlu5/PyLink.git
synced 2024-11-27 21:19:31 +01:00
Merge branch 'master' into wip/relay-no-duplicate-modes
This commit is contained in:
commit
25da086a6b
@ -18,7 +18,7 @@ Dependencies currently include:
|
||||
#### Supported IRCds
|
||||
|
||||
* InspIRCd 2.0.x - module: `inspircd`
|
||||
* charybdis (3.5.x / git master) - module: `ts6`
|
||||
* charybdis (3.5.x / git master) - module: `ts6` (**experimental**)
|
||||
|
||||
### Installation
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
from collections import defaultdict
|
||||
import threading
|
||||
from random import randint
|
||||
|
||||
@ -9,7 +8,7 @@ import time
|
||||
class IrcUser():
|
||||
def __init__(self, nick, ts, uid, ident='null', host='null',
|
||||
realname='PyLink dummy client', realhost='null',
|
||||
ip='0.0.0.0', modes=set()):
|
||||
ip='0.0.0.0'):
|
||||
self.nick = nick
|
||||
self.ts = ts
|
||||
self.uid = uid
|
||||
@ -18,7 +17,7 @@ class IrcUser():
|
||||
self.realhost = realhost
|
||||
self.ip = ip
|
||||
self.realname = realname
|
||||
self.modes = modes
|
||||
self.modes = set()
|
||||
|
||||
self.identified = False
|
||||
self.channels = set()
|
||||
@ -39,7 +38,6 @@ class IrcServer():
|
||||
self.users = []
|
||||
self.internal = internal
|
||||
self.name = name.lower()
|
||||
self.has_bursted = False
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
|
||||
|
@ -1,3 +1,8 @@
|
||||
# This is a sample configuration file for PyLink. You'll likely want to rename it to config.yml
|
||||
# and begin your configuration there.
|
||||
|
||||
# Note: lines starting with a "#" are comments and will be ignored.
|
||||
|
||||
bot:
|
||||
# Sets nick, user/ident, and real name.
|
||||
nick: pylink
|
||||
|
@ -36,3 +36,61 @@ def handle_commands(irc, source, command, args):
|
||||
utils.msg(irc, source, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e)))
|
||||
return
|
||||
utils.add_hook(handle_commands, 'PRIVMSG')
|
||||
|
||||
# Return WHOIS replies to IRCds that use them.
|
||||
def handle_whois(irc, source, command, args):
|
||||
target = args['target']
|
||||
user = irc.users.get(target)
|
||||
if user is None:
|
||||
log.warning('(%s) Got a WHOIS request for %r from %r, but the target doesn\'t exist in irc.users!', irc.name, target, source)
|
||||
f = irc.proto.numericServer
|
||||
server = utils.clientToServer(irc, target) or irc.sid
|
||||
nick = user.nick
|
||||
sourceisOper = ('o', None) in irc.users[source].modes
|
||||
# https://www.alien.net.au/irc/irc2numerics.html
|
||||
# 311: sends nick!user@host information
|
||||
f(irc, server, 311, source, "%s %s %s * :%s" % (nick, user.ident, user.host, user.realname))
|
||||
# 312: sends the server the target is on, and the name
|
||||
f(irc, server, 312, source, "%s %s :PyLink Server" % (nick, irc.serverdata['hostname']))
|
||||
# 313: sends a string denoting the target's operator privilege;
|
||||
# we'll only send it if the user has umode +o.
|
||||
if ('o', None) in user.modes:
|
||||
f(irc, server, 313, source, "%s :is an IRC Operator" % nick)
|
||||
# 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd.
|
||||
# Only shown to opers!
|
||||
if sourceisOper:
|
||||
f(irc, server, 379, source, '%s :is using modes %s' % (nick, utils.joinModes(user.modes)))
|
||||
# 319: RPL_WHOISCHANNELS, shows channel list
|
||||
public_chans = []
|
||||
for chan in user.channels:
|
||||
# Here, we'll want to hide secret/private channels from non-opers
|
||||
# who are not in them.
|
||||
c = irc.channels[chan]
|
||||
if ((irc.cmodes.get('secret'), None) in c.modes or \
|
||||
(irc.cmodes.get('private'), None) in c.modes) \
|
||||
and not (sourceisOper or source in c.users):
|
||||
continue
|
||||
# TODO: show prefix modes like a regular IRCd does.
|
||||
public_chans.append(chan)
|
||||
if public_chans:
|
||||
f(irc, server, 319, source, '%s :%s' % (nick, ' '.join(public_chans)))
|
||||
# 317: shows idle and signon time. Though we don't track the user's real
|
||||
# idle time; we just return 0.
|
||||
# 317 GL GL 15 1437632859 :seconds idle, signon time
|
||||
f(irc, server, 317, source, "%s 0 %s :seconds idle, signon time" % (nick, user.ts))
|
||||
try:
|
||||
# Iterate over plugin-created WHOIS handlers. They return a tuple
|
||||
# or list with two arguments: the numeric, and the text to send.
|
||||
for func in utils.whois_handlers:
|
||||
res = func(irc, target)
|
||||
if res:
|
||||
num, text = res
|
||||
f(irc, server, num, source, text)
|
||||
except Exception as e:
|
||||
# Again, we wouldn't want this to crash our service, in case
|
||||
# something goes wrong!
|
||||
log.exception('Error caught in WHOIS handler: %s', e)
|
||||
finally:
|
||||
# 318: End of WHOIS.
|
||||
f(irc, server, 318, source, "%s :End of /WHOIS list" % nick)
|
||||
utils.add_hook(handle_whois, 'WHOIS')
|
||||
|
7
main.py
7
main.py
@ -61,7 +61,7 @@ class Irc():
|
||||
self.sid = self.serverdata["sid"]
|
||||
self.botdata = conf['bot']
|
||||
self.proto = proto
|
||||
self.pingfreq = self.serverdata.get('pingfreq') or 10
|
||||
self.pingfreq = self.serverdata.get('pingfreq') or 30
|
||||
self.pingtimeout = self.pingfreq * 2
|
||||
|
||||
self.initVars()
|
||||
@ -84,7 +84,9 @@ class Irc():
|
||||
self.socket.settimeout(self.pingtimeout)
|
||||
self.proto.connect(self)
|
||||
self.spawnMain()
|
||||
log.info('(%s) Starting ping schedulers....', self.name)
|
||||
self.schedulePing()
|
||||
log.info('(%s) Server ready; listening for data.', self.name)
|
||||
self.run()
|
||||
except (socket.error, classes.ProtocolError, ConnectionError) as e:
|
||||
log.warning('(%s) Disconnected from IRC: %s: %s',
|
||||
@ -183,7 +185,8 @@ class Irc():
|
||||
nick = self.botdata.get('nick') or 'PyLink'
|
||||
ident = self.botdata.get('ident') or 'pylink'
|
||||
host = self.serverdata["hostname"]
|
||||
self.pseudoclient = self.proto.spawnClient(self, nick, ident, host, modes={("o", None)})
|
||||
log.info('(%s) Connected! Spawning main client %s.', self.name, nick)
|
||||
self.pseudoclient = self.proto.spawnClient(self, nick, ident, host, modes={("+o", None)})
|
||||
for chan in self.serverdata['channels']:
|
||||
self.proto.joinClient(self, self.pseudoclient.uid, chan)
|
||||
|
||||
|
@ -49,7 +49,7 @@ def spawnclient(irc, source, args):
|
||||
def quit(irc, source, args):
|
||||
"""<target> [<reason>]
|
||||
|
||||
Admin-only. Quits the PyLink client <target>, if it exists."""
|
||||
Admin-only. Quits the PyLink client with nick <target>, if one exists."""
|
||||
checkauthenticated(irc, source)
|
||||
try:
|
||||
nick = args[0]
|
||||
@ -66,7 +66,7 @@ def quit(irc, source, args):
|
||||
def joinclient(irc, source, args):
|
||||
"""<target> <channel1>,[<channel2>], etc.
|
||||
|
||||
Admin-only. Joins <target>, a PyLink client, to a comma-separated list of channels."""
|
||||
Admin-only. Joins <target>, the nick of a PyLink client, to a comma-separated list of channels."""
|
||||
checkauthenticated(irc, source)
|
||||
try:
|
||||
nick = args[0]
|
||||
@ -108,7 +108,7 @@ def nick(irc, source, args):
|
||||
def part(irc, source, args):
|
||||
"""<target> <channel1>,[<channel2>],... [<reason>]
|
||||
|
||||
Admin-only. Parts <target>, a PyLink client, from a comma-separated list of channels."""
|
||||
Admin-only. Parts <target>, the nick of a PyLink client, from a comma-separated list of channels."""
|
||||
checkauthenticated(irc, source)
|
||||
try:
|
||||
nick = args[0]
|
||||
@ -128,7 +128,7 @@ def part(irc, source, args):
|
||||
def kick(irc, source, args):
|
||||
"""<source> <channel> <user> [<reason>]
|
||||
|
||||
Admin-only. Kicks <user> from <channel> via <source>, where <source> is a PyLink client."""
|
||||
Admin-only. Kicks <user> from <channel> via <source>, where <source> is the nick of a PyLink client."""
|
||||
checkauthenticated(irc, source)
|
||||
try:
|
||||
nick = args[0]
|
||||
@ -186,7 +186,7 @@ def showchan(irc, source, args):
|
||||
def mode(irc, source, args):
|
||||
"""<source> <target> <modes>
|
||||
|
||||
Admin-only. Sets modes <modes> on <target>."""
|
||||
Admin-only. Sets modes <modes> on <target> from <source>, where <source> is the nick of a PyLink client."""
|
||||
checkauthenticated(irc, source)
|
||||
try:
|
||||
modesource, target, modes = args[0], args[1], args[2:]
|
||||
|
@ -1,7 +1,6 @@
|
||||
# commands.py: base PyLink commands
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import utils
|
||||
|
118
plugins/relay.py
118
plugins/relay.py
@ -5,7 +5,6 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import pickle
|
||||
import sched
|
||||
import threading
|
||||
import time
|
||||
import string
|
||||
from collections import defaultdict
|
||||
|
||||
@ -15,6 +14,17 @@ from log import log
|
||||
dbname = "pylinkrelay.db"
|
||||
relayusers = defaultdict(dict)
|
||||
|
||||
def relayWhoisHandlers(irc, target):
|
||||
user = irc.users[target]
|
||||
orig = getLocalUser(irc, target)
|
||||
if orig:
|
||||
network, remoteuid = orig
|
||||
remotenick = utils.networkobjects[network].users[remoteuid].nick
|
||||
return [320, "%s :is a remote user connected via PyLink Relay. Home "
|
||||
"network: %s; Home nick: %s" % (user.nick, network,
|
||||
remotenick)]
|
||||
utils.whois_handlers.append(relayWhoisHandlers)
|
||||
|
||||
def normalizeNick(irc, netname, nick, separator=None):
|
||||
# Block until we know the IRC network's nick length (after capabilities
|
||||
# are sent)
|
||||
@ -97,8 +107,7 @@ def save(irc, source, args):
|
||||
def getPrefixModes(irc, remoteirc, channel, user):
|
||||
modes = ''
|
||||
for pmode in ('owner', 'admin', 'op', 'halfop', 'voice'):
|
||||
if pmode not in remoteirc.cmodes: # Mode not supported by IRCd
|
||||
continue
|
||||
if pmode in remoteirc.cmodes: # Mode supported by IRCd
|
||||
mlist = irc.channels[channel].prefixmodes[pmode+'s']
|
||||
log.debug('(%s) getPrefixModes: checking if %r is in %r', irc.name, user, mlist)
|
||||
if user in mlist:
|
||||
@ -133,7 +142,7 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True):
|
||||
modes = getSupportedUmodes(irc, remoteirc, userobj.modes)
|
||||
u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident,
|
||||
host=host, realname=realname,
|
||||
modes=modes).uid
|
||||
modes=modes, ts=userobj.ts).uid
|
||||
remoteirc.users[u].remote = irc.name
|
||||
relayusers[(irc.name, user)][remoteirc.name] = u
|
||||
remoteirc.users[u].remote = irc.name
|
||||
@ -218,6 +227,7 @@ def initializeChannel(irc, channel):
|
||||
all_links = db[relay]['links'].copy()
|
||||
all_links.update((relay,))
|
||||
log.debug('(%s) initializeChannel: all_links: %s', irc.name, all_links)
|
||||
# Iterate over all the remote channels linked in this relay.
|
||||
for link in all_links:
|
||||
modes = []
|
||||
remotenet, remotechan = link
|
||||
@ -229,20 +239,9 @@ def initializeChannel(irc, channel):
|
||||
rc = remoteirc.channels[remotechan]
|
||||
if not (remoteirc.connected and findRemoteChan(remoteirc, irc, remotechan)):
|
||||
continue # They aren't connected, don't bother!
|
||||
for user in remoteirc.channels[remotechan].users:
|
||||
# Don't spawn our pseudoclients again.
|
||||
if not utils.isInternalClient(remoteirc, user):
|
||||
log.debug('(%s) initializeChannel: should be joining %s/%s to %s', irc.name, user, remotenet, channel)
|
||||
localuser = getRemoteUser(remoteirc, irc, user)
|
||||
if localuser is None:
|
||||
log.warning('(%s) got None for local user for %s/%s', irc.name, user, remotenet)
|
||||
continue
|
||||
userpair = (getPrefixModes(remoteirc, irc, remotechan, user), localuser)
|
||||
log.debug('(%s) initializeChannel: adding %s to queued_users for %s', irc.name, userpair, channel)
|
||||
queued_users.append(userpair)
|
||||
if queued_users:
|
||||
irc.proto.sjoinServer(irc, irc.sid, channel, queued_users, ts=rc.ts)
|
||||
relayModes(remoteirc, irc, remoteirc.sid, remotechan)
|
||||
# Join their (remote) users and set their modes.
|
||||
relayJoins(remoteirc, remotechan, rc.users,
|
||||
rc.ts, rc.modes)
|
||||
relayModes(irc, remoteirc, irc.sid, channel)
|
||||
topic = remoteirc.channels[relay[1]].topic
|
||||
# Only update the topic if it's different from what we already have,
|
||||
@ -251,6 +250,7 @@ def initializeChannel(irc, channel):
|
||||
irc.proto.topicServer(irc, irc.sid, channel, topic)
|
||||
|
||||
log.debug('(%s) initializeChannel: joining our users: %s', irc.name, c.users)
|
||||
# After that's done, we'll send our users to them.
|
||||
relayJoins(irc, channel, c.users, c.ts, c.modes)
|
||||
irc.proto.joinClient(irc, irc.pseudoclient.uid, channel)
|
||||
|
||||
@ -262,7 +262,6 @@ def handle_join(irc, numeric, command, args):
|
||||
modes = args['modes']
|
||||
ts = args['ts']
|
||||
users = set(args['users'])
|
||||
# users.update(irc.channels[channel].users)
|
||||
relayJoins(irc, channel, users, ts, modes)
|
||||
utils.add_hook(handle_join, 'JOIN')
|
||||
|
||||
@ -296,6 +295,8 @@ def handle_part(irc, numeric, command, args):
|
||||
for netname, user in relayusers[(irc.name, numeric)].copy().items():
|
||||
remoteirc = utils.networkobjects[netname]
|
||||
remotechan = findRemoteChan(irc, remoteirc, channel)
|
||||
if remotechan is None:
|
||||
continue
|
||||
remoteirc.proto.partClient(remoteirc, user, remotechan, text)
|
||||
if not remoteirc.users[user].channels:
|
||||
remoteirc.proto.quitClient(remoteirc, user, 'Left all shared channels.')
|
||||
@ -310,7 +311,10 @@ def handle_privmsg(irc, numeric, command, args):
|
||||
return
|
||||
sent = 0
|
||||
relay = findRelay((irc.name, target))
|
||||
if utils.isChannel(target) and relay and not db[relay]['links']:
|
||||
# Don't send any "you must be in common channels" if we're not part
|
||||
# of a relay, or we are but there are no links!
|
||||
if utils.isChannel(target) and ((relay and not db[relay]['links']) or \
|
||||
relay is None):
|
||||
return
|
||||
for netname, user in relayusers[(irc.name, numeric)].items():
|
||||
remoteirc = utils.networkobjects[netname]
|
||||
@ -416,6 +420,12 @@ def handle_kick(irc, source, command, args):
|
||||
text = "(<unknown kicker>@%s) %s" % (irc.name, text)
|
||||
remoteirc.proto.kickServer(remoteirc, remoteirc.sid,
|
||||
remotechan, real_target, text)
|
||||
|
||||
if target != irc.pseudoclient.uid and not irc.users[target].channels:
|
||||
irc.proto.quitClient(irc, target, 'Left all shared channels.')
|
||||
remoteuser = getLocalUser(irc, target)
|
||||
del relayusers[remoteuser][irc.name]
|
||||
|
||||
utils.add_hook(handle_kick, 'KICK')
|
||||
|
||||
def handle_chgclient(irc, source, command, args):
|
||||
@ -434,8 +444,10 @@ def handle_chgclient(irc, source, command, args):
|
||||
remoteirc = utils.networkobjects[netname]
|
||||
try:
|
||||
remoteirc.proto.updateClient(remoteirc, user, field, text)
|
||||
except ValueError: # IRCd doesn't support changing the field we want
|
||||
logging.debug('(%s) Error raised changing field %r of %s on %s (for %s/%s)', irc.name, field, user, target, remotenet, irc.name)
|
||||
except NotImplementedError: # IRCd doesn't support changing the field we want
|
||||
log.debug('(%s) Ignoring changing field %r of %s on %s (for %s/%s);'
|
||||
' remote IRCd doesn\'t support it', irc.name, field,
|
||||
user, target, netname, irc.name)
|
||||
continue
|
||||
|
||||
for c in ('CHGHOST', 'CHGNAME', 'CHGIDENT'):
|
||||
@ -446,10 +458,10 @@ whitelisted_cmodes = {'admin', 'allowinvite', 'autoop', 'ban', 'banexception',
|
||||
'limit', 'moderated', 'noctcp', 'noextmsg', 'nokick',
|
||||
'noknock', 'nonick', 'nonotice', 'op', 'operonly',
|
||||
'opmoderated', 'owner', 'private', 'regonly',
|
||||
'regmoderated', 'secret', 'sslonly',
|
||||
'regmoderated', 'secret', 'sslonly', 'adminonly',
|
||||
'stripcolor', 'topiclock', 'voice'}
|
||||
whitelisted_umodes = {'bot', 'hidechans', 'hideoper', 'invisible', 'oper',
|
||||
'regdeaf', 'u_stripcolor', 'servprotect', 'u_noctcp'}
|
||||
'regdeaf', 'u_stripcolor', 'u_noctcp', 'wallops'}
|
||||
def relayModes(irc, remoteirc, sender, channel, modes=None):
|
||||
remotechan = findRemoteChan(irc, remoteirc, channel)
|
||||
log.debug('(%s) Relay mode: remotechan for %s on %s is %s', irc.name, channel, irc.name, remotechan)
|
||||
@ -590,30 +602,40 @@ utils.add_hook(handle_topic, 'TOPIC')
|
||||
def handle_kill(irc, numeric, command, args):
|
||||
target = args['target']
|
||||
userdata = args['userdata']
|
||||
if numeric not in irc.users:
|
||||
# A server's sending kill? Uh oh, this can't be good.
|
||||
return
|
||||
# We don't allow killing over the relay, so we must spawn the client.
|
||||
# all over again and rejoin it to its channels.
|
||||
realuser = getLocalUser(irc, target)
|
||||
log.debug('(%s) relay handle_kill: realuser is %r', irc.name, realuser)
|
||||
# Target user was remote:
|
||||
if realuser and realuser[0] != irc.name:
|
||||
# We don't allow killing over the relay, so we must respawn the affected
|
||||
# client and rejoin it to its channels.
|
||||
del relayusers[realuser][irc.name]
|
||||
remoteirc = utils.networkobjects[realuser[0]]
|
||||
for channel in remoteirc.channels:
|
||||
remotechan = findRemoteChan(remoteirc, irc, channel)
|
||||
if remotechan:
|
||||
modes = getPrefixModes(remoteirc, irc, remotechan, realuser[1])
|
||||
log.debug('(%s) handle_kill: userpair: %s, %s', irc.name, modes, realuser)
|
||||
log.debug('(%s) relay handle_kill: userpair: %s, %s', irc.name, modes, realuser)
|
||||
client = getRemoteUser(remoteirc, irc, realuser[1])
|
||||
irc.proto.sjoinServer(irc, irc.sid, remotechan, [(modes, client)])
|
||||
if userdata and numeric in irc.users:
|
||||
utils.msg(irc, numeric, "Your kill has to %s been blocked "
|
||||
"because PyLink does not allow killing"
|
||||
" users over the relay at this time." % \
|
||||
userdata.nick, notice=True)
|
||||
# Target user was local.
|
||||
else:
|
||||
# IMPORTANT: some IRCds (charybdis) don't send explicit QUIT messages
|
||||
# for locally killed clients, while others (inspircd) do!
|
||||
# If we receive a user object in 'userdata' instead of None, it means
|
||||
# that the KILL hasn't been handled by a preceding QUIT message.
|
||||
if userdata:
|
||||
handle_quit(irc, target, 'KILL', {'text': args['text']})
|
||||
|
||||
utils.add_hook(handle_kill, 'KILL')
|
||||
|
||||
def relayJoins(irc, channel, users, ts, modes):
|
||||
queued_users = []
|
||||
for name, remoteirc in utils.networkobjects.items():
|
||||
queued_users = []
|
||||
if name == irc.name:
|
||||
# Don't relay things to their source network...
|
||||
continue
|
||||
@ -622,18 +644,19 @@ def relayJoins(irc, channel, users, ts, modes):
|
||||
# If there is no link on our network for the user, don't
|
||||
# bother spawning it.
|
||||
continue
|
||||
log.debug('(%s) relayJoins: got %r for users', irc.name, users)
|
||||
for user in users.copy():
|
||||
if utils.isInternalClient(irc, user) or user not in irc.users:
|
||||
# We don't need to clone PyLink pseudoclients... That's
|
||||
# meaningless.
|
||||
continue
|
||||
try:
|
||||
if irc.users[user].remote:
|
||||
# Is the .remote attribute set? If so, don't relay already
|
||||
# relayed clients; that'll trigger an endless loop!
|
||||
continue
|
||||
except (AttributeError, KeyError): # Nope, it isn't.
|
||||
except AttributeError: # Nope, it isn't.
|
||||
pass
|
||||
if utils.isInternalClient(irc, user) or user not in irc.users:
|
||||
# We don't need to clone PyLink pseudoclients... That's
|
||||
# meaningless.
|
||||
continue
|
||||
log.debug('Okay, spawning %s/%s everywhere', user, irc.name)
|
||||
assert user in irc.users, "(%s) How is this possible? %r isn't in our user database." % (irc.name, user)
|
||||
u = getRemoteUser(irc, remoteirc, user)
|
||||
@ -673,9 +696,14 @@ def removeChannel(irc, channel):
|
||||
for user in irc.channels[channel].users.copy():
|
||||
if not utils.isInternalClient(irc, user):
|
||||
relayPart(irc, channel, user)
|
||||
# Don't ever part the main client from any of its autojoin channels.
|
||||
else:
|
||||
if user == irc.pseudoclient.uid and channel in \
|
||||
irc.serverdata['channels']:
|
||||
continue
|
||||
irc.proto.partClient(irc, user, channel, 'Channel delinked.')
|
||||
if not irc.users[user].channels:
|
||||
# Don't ever quit it either...
|
||||
if user != irc.pseudoclient.uid and not irc.users[user].channels:
|
||||
irc.proto.quitClient(irc, user, 'Left all shared channels.')
|
||||
remoteuser = getLocalUser(irc, user)
|
||||
del relayusers[remoteuser][irc.name]
|
||||
@ -853,18 +881,24 @@ def handle_disconnect(irc, numeric, command, args):
|
||||
utils.add_hook(handle_disconnect, "PYLINK_DISCONNECT")
|
||||
|
||||
def handle_save(irc, numeric, command, args):
|
||||
# Nick collision! Try to change our nick to the next available normalized
|
||||
# nick.
|
||||
target = args['target']
|
||||
if utils.isInternalClient(irc, target):
|
||||
realuser = getLocalUser(irc, target)
|
||||
if realuser is None:
|
||||
return
|
||||
log.debug('(%s) relay handle_save: %r got in a nick collision! Real user: %r',
|
||||
irc.name, target, realuser)
|
||||
if utils.isInternalClient(irc, target) and realuser:
|
||||
# Nick collision!
|
||||
# It's one of our relay clients; try to fix our nick to the next
|
||||
# available normalized nick.
|
||||
remotenet, remoteuser = realuser
|
||||
remoteirc = utils.networkobjects[remotenet]
|
||||
nick = remoteirc.users[remoteuser].nick
|
||||
newnick = normalizeNick(irc, remotenet, nick)
|
||||
irc.proto.nickClient(irc, target, newnick)
|
||||
else:
|
||||
# Somebody else on the network (not a PyLink client) had a nick collision;
|
||||
# relay this as a nick change appropriately.
|
||||
handle_nick(irc, target, 'SAVE', {'oldnick': None, 'newnick': target})
|
||||
|
||||
utils.add_hook(handle_save, "SAVE")
|
||||
|
||||
@utils.add_cmd
|
||||
|
@ -10,6 +10,8 @@ from log import log
|
||||
|
||||
from classes import *
|
||||
|
||||
casemapping = 'rfc1459'
|
||||
|
||||
# Raw commands sent from servers vary from protocol to protocol. Here, we map
|
||||
# non-standard names to our hook handlers, so plugins get the information they need.
|
||||
|
||||
@ -23,7 +25,7 @@ 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):
|
||||
server=None, ip='0.0.0.0', realname=None, ts=None):
|
||||
server = server or irc.sid
|
||||
if not utils.isInternalServer(irc, server):
|
||||
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server)
|
||||
@ -32,12 +34,13 @@ def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set()
|
||||
if server not in irc.uidgen:
|
||||
irc.uidgen[server] = utils.TS6UIDGenerator(server)
|
||||
uid = irc.uidgen[server].next_uid()
|
||||
ts = int(time.time())
|
||||
ts = ts or int(time.time())
|
||||
realname = realname or irc.botdata['realname']
|
||||
realhost = realhost or host
|
||||
raw_modes = utils.joinModes(modes)
|
||||
u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
|
||||
realhost=realhost, ip=ip, modes=modes)
|
||||
realhost=realhost, ip=ip)
|
||||
utils.applyModes(irc, uid, modes)
|
||||
irc.servers[server].users.append(uid)
|
||||
_send(irc, server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}"
|
||||
" {ts} {modes} + :{realname}".format(ts=ts, host=host,
|
||||
@ -273,16 +276,16 @@ def updateClient(irc, numeric, field, text):
|
||||
Changes the <field> field of <target> PyLink PseudoClient <client numeric>."""
|
||||
field = field.upper()
|
||||
if field == 'IDENT':
|
||||
handle_fident(irc, numeric, 'PYLINK_UPDATECLIENT_IDENT', [text])
|
||||
irc.users[numeric].ident = text
|
||||
_send(irc, numeric, 'FIDENT %s' % text)
|
||||
elif field == 'HOST':
|
||||
handle_fhost(irc, numeric, 'PYLINK_UPDATECLIENT_HOST', [text])
|
||||
irc.users[numeric].host = text
|
||||
_send(irc, numeric, 'FHOST %s' % text)
|
||||
elif field in ('REALNAME', 'GECOS'):
|
||||
handle_fname(irc, numeric, 'PYLINK_UPDATECLIENT_GECOS', [text])
|
||||
irc.users[numeric].realname = text
|
||||
_send(irc, numeric, 'FNAME :%s' % text)
|
||||
else:
|
||||
raise ValueError("Changing field %r of a client is unsupported by this protocol." % field)
|
||||
raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field)
|
||||
|
||||
def pingServer(irc, source=None, target=None):
|
||||
source = source or irc.sid
|
||||
@ -290,6 +293,12 @@ def pingServer(irc, source=None, target=None):
|
||||
if not (target is None or source is None):
|
||||
_send(irc, source, 'PING %s %s' % (source, target))
|
||||
|
||||
def numericServer(irc, source, numeric, text):
|
||||
raise NotImplementedError("Numeric sending is not yet implemented by this "
|
||||
"protocol module. WHOIS requests are handled "
|
||||
"locally by InspIRCd servers, so there is no "
|
||||
"need for PyLink to send numerics directly yet.")
|
||||
|
||||
def connect(irc):
|
||||
ts = irc.start_ts
|
||||
irc.uidgen = {}
|
||||
@ -318,7 +327,8 @@ def handle_privmsg(irc, source, command, args):
|
||||
|
||||
def handle_kill(irc, source, command, args):
|
||||
killed = args[0]
|
||||
data = irc.users[killed]
|
||||
data = irc.users.get(killed)
|
||||
if data:
|
||||
removeClient(irc, killed)
|
||||
return {'target': killed, 'text': args[1], 'userdata': data}
|
||||
|
||||
@ -467,7 +477,7 @@ def handle_events(irc, data):
|
||||
# Each server message looks something like this:
|
||||
# :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :v,1SRAAESWE
|
||||
# :<sid> <command> <argument1> <argument2> ... :final multi word argument
|
||||
args = data.split()
|
||||
args = data.split(" ")
|
||||
if not args:
|
||||
# No data??
|
||||
return
|
||||
@ -559,7 +569,7 @@ def handle_events(irc, data):
|
||||
if parsed_args is not None:
|
||||
return [numeric, command, parsed_args]
|
||||
|
||||
def spawnServer(irc, name, sid=None, uplink=None, desc='PyLink Server', endburst=True):
|
||||
def spawnServer(irc, name, sid=None, uplink=None, desc='PyLink Server'):
|
||||
# -> :0AL SERVER test.server * 1 0AM :some silly pseudoserver
|
||||
uplink = uplink or irc.sid
|
||||
name = name.lower()
|
||||
@ -578,13 +588,8 @@ def spawnServer(irc, name, sid=None, uplink=None, desc='PyLink Server', endburst
|
||||
raise ValueError('Invalid server name %r' % name)
|
||||
_send(irc, uplink, 'SERVER %s * 1 %s :%s' % (name, sid, desc))
|
||||
irc.servers[sid] = IrcServer(uplink, name, internal=True)
|
||||
if endburst:
|
||||
endburstServer(irc, sid)
|
||||
return sid
|
||||
|
||||
def endburstServer(irc, sid):
|
||||
_send(irc, sid, 'ENDBURST')
|
||||
irc.servers[sid].has_bursted = True
|
||||
return sid
|
||||
|
||||
def handle_ftopic(irc, numeric, command, args):
|
||||
# <- :70M FTOPIC #channel 1434510754 GLo|o|!GLolol@escape.the.dreamland.ca :Some channel topic
|
||||
|
@ -2,7 +2,6 @@ import time
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
from copy import copy
|
||||
|
||||
curdir = os.path.dirname(__file__)
|
||||
sys.path += [curdir, os.path.dirname(curdir)]
|
||||
@ -17,13 +16,14 @@ from inspircd import handle_privmsg, handle_kill, handle_kick, handle_error, \
|
||||
handle_quit, handle_nick, handle_save, handle_squit, handle_mode, handle_topic, \
|
||||
handle_notice
|
||||
|
||||
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):
|
||||
server=None, ip='0.0.0.0', realname=None, ts=None):
|
||||
server = server or irc.sid
|
||||
if not utils.isInternalServer(irc, server):
|
||||
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server)
|
||||
@ -32,20 +32,22 @@ def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set()
|
||||
if server not in irc.uidgen:
|
||||
irc.uidgen[server] = utils.TS6UIDGenerator(server)
|
||||
uid = irc.uidgen[server].next_uid()
|
||||
# UID:
|
||||
# EUID:
|
||||
# parameters: nickname, hopcount, nickTS, umodes, username,
|
||||
# visible hostname, IP address, UID, gecos
|
||||
ts = int(time.time())
|
||||
# visible hostname, IP address, UID, real hostname, account name, gecos
|
||||
ts = ts or int(time.time())
|
||||
realname = realname or irc.botdata['realname']
|
||||
realhost = realhost or host
|
||||
raw_modes = utils.joinModes(modes)
|
||||
u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
|
||||
realhost=realhost, ip=ip, modes=modes)
|
||||
realhost=realhost, ip=ip)
|
||||
utils.applyModes(irc, uid, modes)
|
||||
irc.servers[server].users.append(uid)
|
||||
_send(irc, server, "UID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} "
|
||||
":{realname}".format(ts=ts, host=host,
|
||||
_send(irc, server, "EUID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} "
|
||||
"{realhost} * :{realname}".format(ts=ts, host=host,
|
||||
nick=nick, ident=ident, uid=uid,
|
||||
modes=raw_modes, ip=ip, realname=realname))
|
||||
modes=raw_modes, ip=ip, realname=realname,
|
||||
realhost=realhost))
|
||||
return u
|
||||
|
||||
def joinClient(irc, client, channel):
|
||||
@ -214,7 +216,7 @@ def updateClient(irc, numeric, field, text):
|
||||
Changes the <field> field of <target> PyLink PseudoClient <client numeric>."""
|
||||
field = field.upper()
|
||||
if field == 'HOST':
|
||||
handle_chghost(irc, numeric, 'PYLINK_UPDATECLIENT_HOST', [text])
|
||||
irc.users[numeric].host = text
|
||||
_send(irc, irc.sid, 'CHGHOST %s :%s' % (numeric, text))
|
||||
else:
|
||||
raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field)
|
||||
@ -228,6 +230,9 @@ def pingServer(irc, source=None, target=None):
|
||||
else:
|
||||
_send(irc, source, 'PING %s' % source)
|
||||
|
||||
def numericServer(irc, source, numeric, target, text):
|
||||
_send(irc, source, '%s %s %s' % (numeric, target, text))
|
||||
|
||||
def connect(irc):
|
||||
ts = irc.start_ts
|
||||
irc.uidgen = {}
|
||||
@ -375,8 +380,12 @@ def handle_euid(irc, numeric, command, args):
|
||||
irc.servers[numeric].users.append(uid)
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
|
||||
|
||||
def handle_uid(irc, numeric, command, args):
|
||||
raise ProtocolError("Servers must use EUID to send users! This is a "
|
||||
"requested capability; plain UID (received) is not "
|
||||
"handled by us at all!")
|
||||
|
||||
def handle_server(irc, numeric, command, args):
|
||||
# SERVER is sent by our uplink or any other server to introduce others.
|
||||
# parameters: server name, hopcount, sid, server description
|
||||
servername = args[0].lower()
|
||||
try:
|
||||
@ -390,6 +399,8 @@ def handle_server(irc, numeric, command, args):
|
||||
irc.servers[sid] = IrcServer(numeric, servername)
|
||||
return {'name': servername, 'sid': sid, 'text': sdesc}
|
||||
|
||||
handle_sid = handle_server
|
||||
|
||||
def handle_tmode(irc, numeric, command, args):
|
||||
# <- :42XAAAAAB TMODE 1437450768 #endlessvoid -c+lkC 3 agte4
|
||||
channel = args[1].lower()
|
||||
@ -403,7 +414,7 @@ def handle_events(irc, data):
|
||||
# TS6 messages:
|
||||
# :42X COMMAND arg1 arg2 :final long arg
|
||||
# :42XAAAAAA PRIVMSG #somewhere :hello!
|
||||
args = data.split()
|
||||
args = data.split(" ")
|
||||
if not args:
|
||||
# No data??
|
||||
return
|
||||
@ -428,6 +439,12 @@ def handle_events(irc, data):
|
||||
# According to the TS6 protocol documentation, we should send SVINFO
|
||||
# when we get our uplink's SERVER command.
|
||||
irc.send('SVINFO 6 6 0 :%s' % int(time.time()))
|
||||
elif args[0] == 'SQUIT':
|
||||
# What? Charybdis send this in a different format!
|
||||
# <- SQUIT 00A :Remote host closed the connection
|
||||
split_server = args[1]
|
||||
res = handle_squit(irc, split_server, 'SQUIT', [split_server])
|
||||
irc.callHooks([split_server, 'SQUIT', res])
|
||||
elif args[0] == 'CAPAB':
|
||||
# We only get a list of keywords here. Charybdis obviously assumes that
|
||||
# we know what modes it supports (indeed, this is a standard list).
|
||||
@ -447,6 +464,10 @@ def handle_events(irc, data):
|
||||
|
||||
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L80
|
||||
chary_cmodes = { # TS6 generic modes:
|
||||
# Note: charybdis +p has the effect of being both
|
||||
# noknock AND private. Surprisingly, mapping it twice
|
||||
# works pretty well: setting +p on a charybdis relay
|
||||
# server sets +pK on an InspIRCd network.
|
||||
'op': 'o', 'voice': 'v', 'ban': 'b', 'key': 'k', 'limit':
|
||||
'l', 'moderated': 'm', 'noextmsg': 'n', 'noknock': 'p',
|
||||
'secret': 's', 'topiclock': 't',
|
||||
@ -454,7 +475,9 @@ def handle_events(irc, data):
|
||||
'quiet': 'q', 'redirect': 'f', 'freetarget': 'F',
|
||||
'joinflood': 'j', 'largebanlist': 'L', 'permanent': 'P',
|
||||
'c_noforwards': 'Q', 'stripcolor': 'c', 'allowinvite':
|
||||
'g', 'opmoderated': 'z',
|
||||
'g', 'opmoderated': 'z', 'noctcp': 'C',
|
||||
# charybdis-specific modes provided by EXTENSIONS
|
||||
'operonly': 'O', 'adminonly': 'A', 'sslonly': 'S',
|
||||
# Now, map all the ABCD type modes:
|
||||
'*A': 'beI', '*B': 'k', '*C': 'l', '*D': 'mnprst'}
|
||||
if 'EX' in caps:
|
||||
@ -526,7 +549,7 @@ def handle_events(irc, data):
|
||||
if parsed_args is not None:
|
||||
return [numeric, command, parsed_args]
|
||||
|
||||
def spawnServer(irc, name, sid=None, uplink=None, desc='PyLink Server', endburst=True):
|
||||
def spawnServer(irc, name, sid=None, uplink=None, desc='PyLink Server'):
|
||||
# -> :0AL SERVER test.server 1 0XY :some silly pseudoserver
|
||||
uplink = uplink or irc.sid
|
||||
name = name.lower()
|
||||
@ -584,3 +607,23 @@ def handle_bmask(irc, numeric, command, args):
|
||||
modes.append(('+%s' % mode, ban))
|
||||
utils.applyModes(irc, channel, modes)
|
||||
return {'target': channel, 'modes': modes, 'ts': ts}
|
||||
|
||||
def handle_whois(irc, numeric, command, args):
|
||||
# <- :42XAAAAAB WHOIS 5PYAAAAAA :pylink-devel
|
||||
return {'target': args[0]}
|
||||
|
||||
def handle_472(irc, numeric, command, args):
|
||||
# <- :charybdis.midnight.vpn 472 GL|devel O :is an unknown mode char to me
|
||||
# 472 is sent to us when one of our clients tries to set a mode the server
|
||||
# doesn't support. In this case, we'll raise a warning to alert the user
|
||||
# about it.
|
||||
badmode = args[1]
|
||||
reason = args[-1]
|
||||
setter = args[0]
|
||||
charlist = {'A': 'chm_adminonly', 'O': 'chm_operonly', 'S': 'chm_sslonly'}
|
||||
if badmode in charlist:
|
||||
log.warning('(%s) User %r attempted to set channel mode %r, but the '
|
||||
'extension providing it isn\'t loaded! To prevent possible'
|
||||
' desyncs, try adding the line "loadmodule "extensions/%s.so";" to '
|
||||
'your IRCd configuration.', irc.name, setter, badmode,
|
||||
charlist[badmode])
|
||||
|
@ -2,7 +2,6 @@ import sys
|
||||
import os
|
||||
sys.path += [os.getcwd(), os.path.join(os.getcwd(), 'protocols')]
|
||||
import unittest
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
import inspircd
|
||||
@ -72,7 +71,6 @@ class TestProtoInspIRCd(unittest.TestCase):
|
||||
self.assertNotIn(u, self.irc.channels['#channel'].users)
|
||||
self.assertNotIn(u, self.irc.users)
|
||||
self.assertNotIn(u, self.irc.servers[self.irc.sid].users)
|
||||
pass
|
||||
|
||||
def testKickClient(self):
|
||||
target = self.proto.spawnClient(self.irc, 'soccerball', 'soccerball', 'abcd').uid
|
||||
|
29
utils.py
29
utils.py
@ -12,6 +12,7 @@ command_hooks = defaultdict(list)
|
||||
networkobjects = {}
|
||||
schedulers = {}
|
||||
plugins = []
|
||||
whois_handlers = []
|
||||
started = threading.Event()
|
||||
|
||||
class TS6UIDGenerator():
|
||||
@ -97,8 +98,6 @@ class TS6SIDGenerator():
|
||||
self.iters[pos] = iter(self.allowedchars[pos])
|
||||
next(self.iters[pos])
|
||||
self.increment(pos-1)
|
||||
else:
|
||||
print('NEXT')
|
||||
|
||||
def next_sid(self):
|
||||
sid = ''.join(self.output)
|
||||
@ -122,9 +121,18 @@ def add_hook(func, command):
|
||||
command = command.upper()
|
||||
command_hooks[command].append(func)
|
||||
|
||||
def toLower(irc, text):
|
||||
if irc.proto.casemapping == 'rfc1459':
|
||||
text = text.replace('{', '[')
|
||||
text = text.replace('}', ']')
|
||||
text = text.replace('|', '\\')
|
||||
text = text.replace('~', '^')
|
||||
return text.lower()
|
||||
|
||||
def nickToUid(irc, nick):
|
||||
nick = toLower(irc, nick)
|
||||
for k, v in irc.users.items():
|
||||
if v.nick == nick:
|
||||
if toLower(irc, v.nick) == nick:
|
||||
return k
|
||||
|
||||
def clientToServer(irc, numeric):
|
||||
@ -170,16 +178,18 @@ def parseModes(irc, target, args):
|
||||
if usermodes:
|
||||
log.debug('(%s) Using irc.umodes for this query: %s', irc.name, irc.umodes)
|
||||
supported_modes = irc.umodes
|
||||
oldmodes = irc.users[target].modes
|
||||
else:
|
||||
log.debug('(%s) Using irc.cmodes for this query: %s', irc.name, irc.cmodes)
|
||||
supported_modes = irc.cmodes
|
||||
oldmodes = irc.channels[target].modes
|
||||
res = []
|
||||
for mode in modestring:
|
||||
if mode in '+-':
|
||||
prefix = mode
|
||||
else:
|
||||
if not prefix:
|
||||
raise ValueError('Invalid query %r: mode char given without preceding prefix.' % modestring)
|
||||
prefix = '+'
|
||||
arg = None
|
||||
log.debug('Current mode: %s%s; args left: %s', prefix, mode, args)
|
||||
try:
|
||||
@ -187,6 +197,17 @@ def parseModes(irc, target, args):
|
||||
# Must have parameter.
|
||||
log.debug('Mode %s: This mode must have parameter.', mode)
|
||||
arg = args.pop(0)
|
||||
if prefix == '-' and mode in supported_modes['*B'] and arg == '*':
|
||||
# Charybdis allows unsetting +k without actually
|
||||
# knowing the key by faking the argument when unsetting
|
||||
# as a single "*".
|
||||
# We'd need to know the real argument of +k for us to
|
||||
# be able to unset the mode.
|
||||
oldargs = [m[1] for m in oldmodes if m[0] == mode]
|
||||
if oldargs:
|
||||
# Set the arg to the old one on the channel.
|
||||
arg = oldargs[0]
|
||||
log.debug("Mode %s: coersing argument of '*' to %r.", mode, arg)
|
||||
elif mode in irc.prefixmodes and not usermodes:
|
||||
# We're setting a prefix mode on someone (e.g. +o user1)
|
||||
log.debug('Mode %s: This mode is a prefix mode.', mode)
|
||||
|
Loading…
Reference in New Issue
Block a user