3
0
mirror of https://github.com/jlu5/PyLink.git synced 2025-02-25 18:00:39 +01:00

Merge branch 'devel'

This commit is contained in:
James Lu 2015-08-25 19:54:05 -07:00
commit da0101e750
7 changed files with 107 additions and 58 deletions

View File

@ -14,6 +14,7 @@ Dependencies currently include:
* Python 3.4+ * Python 3.4+
* PyYAML (`pip install pyyaml` or `apt-get install python3-yaml`) * PyYAML (`pip install pyyaml` or `apt-get install python3-yaml`)
* *For the relay plugin only*: expiringdict (`pip install expiringdict`/`apt-get install python3-expiringdict`)
#### Supported IRCds #### Supported IRCds
@ -23,7 +24,7 @@ Dependencies currently include:
### Installation ### Installation
1) Rename `config.yml.example` to `config.yml` and configure your instance there. Not all options are properly implemented yet, and the configuration schema isn't finalized yet - this means your configuration may break in an update! 1) Rename `config.yml.example` to `config.yml` and configure your instance there. Not all options are properly implemented yet, and the configuration schema isn't finalized yet - this means that your configuration may break in an update!
2) Run `main.py` from the command line. 2) Run `main.py` from the command line.

View File

@ -8,6 +8,10 @@ bot:
nick: pylink nick: pylink
user: pylink user: pylink
realname: PyLink Service Client realname: PyLink Service Client
# Server description (shown in /links, /whois, etc.)
serverdesc: PyLink Server
# Console log verbosity: see https://docs.python.org/3/library/logging.html#logging-levels # Console log verbosity: see https://docs.python.org/3/library/logging.html#logging-levels
loglevel: DEBUG loglevel: DEBUG
@ -31,6 +35,12 @@ servers:
# The first char must be a digit [0-9], and the remaining two chars may be letters [A-Z] or digits. # The first char must be a digit [0-9], and the remaining two chars may be letters [A-Z] or digits.
sid: "0AL" sid: "0AL"
# SID range - the range of SIDs PyLink is allowed to use to generate server IDs. On TS6,
# this should be a combination of digits, letters, and #'s. Each # denotes a range (0-9A-Z)
# of characters that can be used by PyLink. You will want to make sure no other servers
# are using this range. There must be at least one # in the entry.
sidrange: "8##"
# Autojoin channels # Autojoin channels
channels: ["#pylink"] channels: ["#pylink"]

View File

@ -38,7 +38,7 @@ def handle_commands(irc, source, command, args):
return return
utils.add_hook(handle_commands, 'PRIVMSG') utils.add_hook(handle_commands, 'PRIVMSG')
# Return WHOIS replies to IRCds that use them. # Handle WHOIS queries, for IRCds that send them across servers (charybdis, UnrealIRCd; NOT InspIRCd).
def handle_whois(irc, source, command, args): def handle_whois(irc, source, command, args):
target = args['target'] target = args['target']
user = irc.users.get(target) user = irc.users.get(target)
@ -51,16 +51,6 @@ def handle_whois(irc, source, command, args):
# https://www.alien.net.au/irc/irc2numerics.html # https://www.alien.net.au/irc/irc2numerics.html
# 311: sends nick!user@host information # 311: sends nick!user@host information
f(irc, server, 311, source, "%s %s %s * :%s" % (nick, user.ident, user.host, user.realname)) 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 # 319: RPL_WHOISCHANNELS, shows channel list
public_chans = [] public_chans = []
for chan in user.channels: for chan in user.channels:
@ -71,18 +61,33 @@ def handle_whois(irc, source, command, args):
(irc.cmodes.get('private'), None) in c.modes) \ (irc.cmodes.get('private'), None) in c.modes) \
and not (sourceisOper or source in c.users): and not (sourceisOper or source in c.users):
continue continue
# TODO: show prefix modes like a regular IRCd does. # Show prefix modes like a regular IRCd does.
for prefixmode, prefixchar in irc.prefixmodes.items():
modename = [mname for mname, char in irc.cmodes.items() if char == prefixmode]
if modename and target in c.prefixmodes[modename[0]+'s']:
chan = prefixchar + chan
public_chans.append(chan) public_chans.append(chan)
if public_chans: if public_chans:
f(irc, server, 319, source, '%s :%s' % (nick, ' '.join(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 # 312: sends the server the target is on, and its server description.
# idle time; we just return 0. f(irc, server, 312, source, "%s %s :%s" % (nick, irc.serverdata['hostname'],
# 317 GL GL 15 1437632859 :seconds idle, signon time irc.serverdata.get('serverdesc') or irc.botdata['serverdesc']))
# 313: sends a string denoting the target's operator privilege,
# only if they have 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 show this to opers!
if sourceisOper:
f(irc, server, 379, source, '%s :is using modes %s' % (nick, utils.joinModes(user.modes)))
# 317: shows idle and signon time. However, we don't track the user's real
# idle time, so we simply 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)) 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: for func in utils.whois_handlers:
# Iterate over custom plugin WHOIS handlers. They return a tuple
# or list with two arguments: the numeric, and the text to send.
try:
res = func(irc, target) res = func(irc, target)
if res: if res:
num, text = res num, text = res
@ -90,8 +95,7 @@ def handle_whois(irc, source, command, args):
except Exception as e: except Exception as e:
# Again, we wouldn't want this to crash our service, in case # Again, we wouldn't want this to crash our service, in case
# something goes wrong! # something goes wrong!
log.exception('Error caught in WHOIS handler: %s', e) log.exception('(%s) Error caught in WHOIS handler: %s', irc.name, e)
finally:
# 318: End of WHOIS. # 318: End of WHOIS.
f(irc, server, 318, source, "%s :End of /WHOIS list" % nick) f(irc, server, 318, source, "%s :End of /WHOIS list" % nick)
utils.add_hook(handle_whois, 'WHOIS') utils.add_hook(handle_whois, 'WHOIS')

12
main.py
View File

@ -278,10 +278,10 @@ if __name__ == '__main__':
pl = imp.load_source(plugin, moduleinfo[1]) pl = imp.load_source(plugin, moduleinfo[1])
utils.plugins.append(pl) utils.plugins.append(pl)
except ImportError as e: except ImportError as e:
if str(e).startswith('No module named'): if str(e) == ('No module named %r' % plugin):
log.error('Failed to load plugin %r: the plugin could not be found.', plugin) log.error('Failed to load plugin %r: The plugin could not be found.', plugin)
else: else:
log.error('Failed to load plugin %r: import error %s', plugin, str(e)) log.error('Failed to load plugin %r: ImportError: %s', plugin, str(e))
else: else:
if hasattr(pl, 'main'): if hasattr(pl, 'main'):
log.debug('Calling main() function of plugin %r', pl) log.debug('Calling main() function of plugin %r', pl)
@ -293,10 +293,10 @@ if __name__ == '__main__':
moduleinfo = imp.find_module(protoname, protocols_folder) moduleinfo = imp.find_module(protoname, protocols_folder)
proto = imp.load_source(protoname, moduleinfo[1]) proto = imp.load_source(protoname, moduleinfo[1])
except ImportError as e: except ImportError as e:
if str(e).startswith('No module named'): if str(e) == ('No module named %r' % protoname):
log.critical('Failed to load protocol module %r: the file could not be found.', protoname) log.critical('Failed to load protocol module %r: The file could not be found.', protoname)
else: else:
log.critical('Failed to load protocol module: import error %s', protoname, str(e)) log.critical('Failed to load protocol module: ImportError: %s', protoname, str(e))
sys.exit(2) sys.exit(2)
else: else:
utils.networkobjects[network] = Irc(network, proto, conf.conf) utils.networkobjects[network] = Irc(network, proto, conf.conf)

View File

@ -8,6 +8,8 @@ import threading
import string import string
from collections import defaultdict from collections import defaultdict
from expiringdict import ExpiringDict
import utils import utils
from log import log from log import log
from conf import confname from conf import confname
@ -19,6 +21,7 @@ dbname += '.db'
relayusers = defaultdict(dict) relayusers = defaultdict(dict)
spawnlocks = defaultdict(threading.Lock) spawnlocks = defaultdict(threading.Lock)
savecache = ExpiringDict(max_len=5, max_age_seconds=10)
def relayWhoisHandlers(irc, target): def relayWhoisHandlers(irc, target):
user = irc.users[target] user = irc.users[target]
@ -31,41 +34,46 @@ def relayWhoisHandlers(irc, target):
remotenick)] remotenick)]
utils.whois_handlers.append(relayWhoisHandlers) utils.whois_handlers.append(relayWhoisHandlers)
def normalizeNick(irc, netname, nick, separator=None, oldnick=''): def normalizeNick(irc, netname, nick, separator=None, uid=''):
separator = separator or irc.serverdata.get('separator') or "/" separator = separator or irc.serverdata.get('separator') or "/"
log.debug('(%s) normalizeNick: using %r as separator.', irc.name, separator) log.debug('(%s) normalizeNick: using %r as separator.', irc.name, separator)
orig_nick = nick orig_nick = nick
protoname = irc.proto.__name__ protoname = irc.proto.__name__
maxnicklen = irc.maxnicklen maxnicklen = irc.maxnicklen
if not protoname.startswith(('insp', 'unreal')): if not protoname.startswith(('insp', 'unreal')):
# Charybdis doesn't allow / in usernames, and will quit with # Charybdis doesn't allow / in usernames, and will SQUIT with
# a protocol violation if there is one. # a protocol violation if it sees one.
separator = separator.replace('/', '|') separator = separator.replace('/', '|')
nick = nick.replace('/', '|') nick = nick.replace('/', '|')
if nick.startswith(tuple(string.digits)): if nick.startswith(tuple(string.digits)):
# On TS6 IRCds, nicks that start with 0-9 are only allowed if # On TS6 IRCds, nicks that start with 0-9 are only allowed if
# they match the UID of the originating server. Otherwise, you'll # they match the UID of the originating server. Otherwise, you'll
# get nasty protocol violations! # get nasty protocol violation SQUITs!
nick = '_' + nick nick = '_' + nick
tagnicks = True tagnicks = True
suffix = separator + netname suffix = separator + netname
nick = nick[:maxnicklen] nick = nick[:maxnicklen]
# Maximum allowed length of a nickname. # Maximum allowed length of a nickname, minus the obligatory /network tag.
allowedlength = maxnicklen - len(suffix) allowedlength = maxnicklen - len(suffix)
# If a nick is too long, the real nick portion must be cut off, but the
# /network suffix must remain the same.
# If a nick is too long, the real nick portion will be cut off, but the
# /network suffix MUST remain the same.
nick = nick[:allowedlength] nick = nick[:allowedlength]
nick += suffix nick += suffix
# FIXME: factorize
while utils.nickToUid(irc, nick) or utils.nickToUid(irc, oldnick) and not \ # The nick we want exists? Darn, create another one then.
isRelayClient(irc, utils.nickToUid(irc, nick)):
# The nick we want exists? Darn, create another one then, but only if
# the target isn't an internal client!
# Increase the separator length by 1 if the user was already tagged, # Increase the separator length by 1 if the user was already tagged,
# but couldn't be created due to a nick conflict. # but couldn't be created due to a nick conflict.
# This can happen when someone steals a relay user's nick. # This can happen when someone steals a relay user's nick.
# However, if the user is changing from, say, a long, cut-off nick to another long,
# cut-off nick, we don't need to check for duplicates and tag the nick twice.
# somecutoffnick/net would otherwise be erroneous NICK'ed to somecutoffnic//net,
# even though there would be no collision because the old and new nicks are from
# the same client.
while utils.nickToUid(irc, nick) and utils.nickToUid(irc, nick) != uid:
new_sep = separator + separator[-1] new_sep = separator + separator[-1]
log.debug('(%s) normalizeNick: nick %r is in use; using %r as new_sep.', irc.name, nick, new_sep) log.debug('(%s) normalizeNick: nick %r is in use; using %r as new_sep.', irc.name, nick, new_sep)
nick = normalizeNick(irc, netname, orig_nick, separator=new_sep) nick = normalizeNick(irc, netname, orig_nick, separator=new_sep)
@ -271,7 +279,7 @@ utils.add_hook(handle_squit, 'SQUIT')
def handle_nick(irc, numeric, command, args): def handle_nick(irc, numeric, command, args):
for netname, user in relayusers[(irc.name, numeric)].items(): for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = utils.networkobjects[netname] remoteirc = utils.networkobjects[netname]
newnick = normalizeNick(remoteirc, irc.name, args['newnick']) newnick = normalizeNick(remoteirc, irc.name, args['newnick'], uid=user)
if remoteirc.users[user].nick != newnick: if remoteirc.users[user].nick != newnick:
remoteirc.proto.nickClient(remoteirc, user, newnick) remoteirc.proto.nickClient(remoteirc, user, newnick)
utils.add_hook(handle_nick, 'NICK') utils.add_hook(handle_nick, 'NICK')
@ -396,9 +404,16 @@ def handle_kick(irc, source, command, args):
# Join the kicked client back with its respective modes. # Join the kicked client back with its respective modes.
irc.proto.sjoinServer(irc, irc.sid, channel, [(modes, target)]) irc.proto.sjoinServer(irc, irc.sid, channel, [(modes, target)])
if kicker in irc.users: if kicker in irc.users:
log.info('(%s) Blocked KICK (reason %r) from %s to relay client %s/%s on %s.',
irc.name, args['text'], irc.users[source].nick,
remoteirc.users[real_target].nick, remoteirc.name, channel)
utils.msg(irc, kicker, "This channel is claimed; your kick to " utils.msg(irc, kicker, "This channel is claimed; your kick to "
"%s has been blocked because you are not " "%s has been blocked because you are not "
"(half)opped." % channel, notice=True) "(half)opped." % channel, notice=True)
else:
log.info('(%s) Blocked KICK (reason %r) from server %s to relay client %s/%s on %s.',
irc.name, args['text'], irc.servers[source].name,
remoteirc.users[real_target].nick, remoteirc.name, channel)
return return
if not real_target: if not real_target:
@ -626,10 +641,17 @@ def handle_kill(irc, numeric, command, args):
client = getRemoteUser(remoteirc, irc, realuser[1]) client = getRemoteUser(remoteirc, irc, realuser[1])
irc.proto.sjoinServer(irc, irc.sid, localchan, [(modes, client)]) irc.proto.sjoinServer(irc, irc.sid, localchan, [(modes, client)])
if userdata and numeric in irc.users: if userdata and numeric in irc.users:
log.info('(%s) Blocked KILL (reason %r) from %s to relay client %s/%s.',
irc.name, args['text'], irc.users[numeric].nick,
remoteirc.users[realuser[1]].nick, realuser[0])
utils.msg(irc, numeric, "Your kill to %s has been blocked " utils.msg(irc, numeric, "Your kill to %s has been blocked "
"because PyLink does not allow killing" "because PyLink does not allow killing"
" users over the relay at this time." % \ " users over the relay at this time." % \
userdata.nick, notice=True) userdata.nick, notice=True)
else:
log.info('(%s) Blocked KILL (reason %r) from server %s to relay client %s/%s.',
irc.name, args['text'], irc.servers[numeric].name,
remoteirc.users[realuser[1]].nick, realuser[0])
# Target user was local. # Target user was local.
else: else:
# IMPORTANT: some IRCds (charybdis) don't send explicit QUIT messages # IMPORTANT: some IRCds (charybdis) don't send explicit QUIT messages
@ -912,8 +934,18 @@ def handle_save(irc, numeric, command, args):
remotenet, remoteuser = realuser remotenet, remoteuser = realuser
remoteirc = utils.networkobjects[remotenet] remoteirc = utils.networkobjects[remotenet]
nick = remoteirc.users[remoteuser].nick nick = remoteirc.users[remoteuser].nick
newnick = normalizeNick(irc, remotenet, nick, oldnick=args['oldnick']) # Limit how many times we can attempt to fix our nick, to prevent
# floods and such.
if savecache.setdefault(target, 0) <= 5:
newnick = normalizeNick(irc, remotenet, nick)
log.info('(%s) SAVE received for relay client %r (%s), fixing nick to %s',
irc.name, target, nick, newnick)
irc.proto.nickClient(irc, target, newnick) irc.proto.nickClient(irc, target, newnick)
else:
log.warning('(%s) SAVE received for relay client %r (%s), not '
'fixing nick again due to 5 failed attempts in '
'the last 10 seconds!', irc.name, target, nick)
savecache[target] += 1
else: else:
# Somebody else on the network (not a PyLink client) had a nick collision; # Somebody else on the network (not a PyLink client) had a nick collision;
# relay this as a nick change appropriately. # relay this as a nick change appropriately.

View File

@ -101,8 +101,8 @@ def sjoinServer(irc, server, channel, users, ts=None):
irc.users[user].channels.add(channel) irc.users[user].channels.add(channel)
except KeyError: # Not initialized yet? except KeyError: # Not initialized yet?
log.debug("(%s) sjoinServer: KeyError trying to add %r to %r's channel list?", irc.name, channel, user) log.debug("(%s) sjoinServer: KeyError trying to add %r to %r's channel list?", irc.name, channel, user)
if ts < orig_ts: if ts <= orig_ts:
# Only save our prefix modes in the channel state if our TS is lower than theirs. # Only save our prefix modes in the channel state if our TS is lower than or equal to theirs.
utils.applyModes(irc, channel, changedmodes) utils.applyModes(irc, channel, changedmodes)
namelist = ' '.join(namelist) namelist = ' '.join(namelist)
_send(irc, server, "FJOIN {channel} {ts} {modes} :{users}".format( _send(irc, server, "FJOIN {channel} {ts} {modes} :{users}".format(
@ -329,8 +329,9 @@ def connect(irc):
f('CAPAB START 1202') f('CAPAB START 1202')
f('CAPAB CAPABILITIES :PROTOCOL=1202') f('CAPAB CAPABILITIES :PROTOCOL=1202')
f('CAPAB END') f('CAPAB END')
f('SERVER {host} {Pass} 0 {sid} :PyLink Service'.format(host=irc.serverdata["hostname"], f('SERVER {host} {Pass} 0 {sid} :{sdesc}'.format(host=irc.serverdata["hostname"],
Pass=irc.serverdata["sendpass"], sid=irc.sid)) Pass=irc.serverdata["sendpass"], sid=irc.sid,
sdesc=irc.serverdata.get('serverdesc') or irc.botdata['serverdesc']))
f(':%s BURST %s' % (irc.sid, ts)) f(':%s BURST %s' % (irc.sid, ts))
f(':%s ENDBURST' % (irc.sid)) f(':%s ENDBURST' % (irc.sid))

View File

@ -117,8 +117,8 @@ def sjoinServer(irc, server, channel, users, ts=None):
ts=ts, users=namelist, channel=channel, ts=ts, users=namelist, channel=channel,
modes=utils.joinModes(modes))) modes=utils.joinModes(modes)))
irc.channels[channel].users.update(uids) irc.channels[channel].users.update(uids)
if ts < orig_ts: if ts <= orig_ts:
# Only save our prefix modes in the channel state if our TS is lower than theirs. # Only save our prefix modes in the channel state if our TS is lower than or equal to theirs.
utils.applyModes(irc, channel, changedmodes) utils.applyModes(irc, channel, changedmodes)
def _sendModes(irc, numeric, target, modes, ts=None): def _sendModes(irc, numeric, target, modes, ts=None):
@ -345,7 +345,8 @@ def connect(irc):
# and allows sending CHGHOST without ENCAP. # and allows sending CHGHOST without ENCAP.
f('CAPAB :QS ENCAP EX CHW IE KNOCK SAVE SERVICES TB EUID') f('CAPAB :QS ENCAP EX CHW IE KNOCK SAVE SERVICES TB EUID')
f('SERVER %s 0 :PyLink Service' % irc.serverdata["hostname"]) f('SERVER %s 0 :%s' % (irc.serverdata["hostname"],
irc.serverdata.get('serverdesc') or irc.botdata['serverdesc']))
def handle_ping(irc, source, command, args): def handle_ping(irc, source, command, args):
# PING: # PING: