diff --git a/plugins/admin.py b/plugins/admin.py index 892d4f3..8362a10 100644 --- a/plugins/admin.py +++ b/plugins/admin.py @@ -186,7 +186,7 @@ def showchan(irc, source, args): def mode(irc, source, args): """ - Admin-only. Sets modes on from , where is the nick of a PyLink client.""" + Admin-only. Sets modes on from , where is either the nick of a PyLink client, or the SID of a PyLink server.""" checkauthenticated(irc, source) try: modesource, target, modes = args[0], args[1], args[2:] @@ -208,3 +208,30 @@ def mode(irc, source, args): else: sourceuid = utils.nickToUid(irc, modesource) irc.proto.modeClient(irc, sourceuid, target, parsedmodes) + +@utils.add_cmd +def msg(irc, source, args): + """ + + Admin-only. Sends message from , where is the nick of a PyLink client.""" + checkauthenticated(irc, source) + try: + msgsource, target, text = args[0], args[1], ' '.join(args[2:]) + except IndexError: + utils.msg(irc, source, 'Error: not enough arguments. Needs 3: source nick, target, text.') + return + sourceuid = utils.nickToUid(irc, msgsource) + if not sourceuid: + utils.msg(irc, source, 'Error: unknown user %r' % msgsource) + return + if not utils.isChannel(target): + real_target = utils.nickToUid(irc, target) + if real_target is None: + utils.msg(irc, source, 'Error: unknown user %r' % target) + return + else: + real_target = target + if not text: + utils.msg(irc, source, 'Error: no text given.') + return + irc.proto.messageClient(irc, sourceuid, real_target, text) diff --git a/plugins/relay.py b/plugins/relay.py index b8b2c5d..07bba7d 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -16,7 +16,9 @@ dbname = "pylinkrelay" if confname != 'pylink': dbname += '-%s' % confname dbname += '.db' + relayusers = defaultdict(dict) +spawnlocks = defaultdict(threading.Lock) def relayWhoisHandlers(irc, target): user = irc.users[target] @@ -124,30 +126,31 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True): return remoteirc.pseudoclient.uid except AttributeError: # Network hasn't been initialized yet? pass - try: - u = relayusers[(irc.name, user)][remoteirc.name] - except KeyError: - userobj = irc.users.get(user) - if userobj is None or (not spawnIfMissing) or (not remoteirc.connected.is_set()): - # The query wasn't actually a valid user, or the network hasn't - # been connected yet... Oh well! - return - nick = normalizeNick(remoteirc, irc.name, userobj.nick) - # Truncate idents at 10 characters, because TS6 won't like them otherwise! - ident = userobj.ident[:10] - # Ditto hostname at 64 chars. - host = userobj.host[:64] - realname = userobj.realname - modes = getSupportedUmodes(irc, remoteirc, userobj.modes) - u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident, - host=host, realname=realname, - modes=modes, ts=userobj.ts).uid - remoteirc.users[u].remote = irc.name - away = userobj.away - if away: - remoteirc.proto.awayClient(remoteirc, u, away) - relayusers[(irc.name, user)][remoteirc.name] = u - return u + with spawnlocks[irc.name]: + try: + u = relayusers[(irc.name, user)][remoteirc.name] + except KeyError: + userobj = irc.users.get(user) + if userobj is None or (not spawnIfMissing) or (not remoteirc.connected.is_set()): + # The query wasn't actually a valid user, or the network hasn't + # been connected yet... Oh well! + return + nick = normalizeNick(remoteirc, irc.name, userobj.nick) + # Truncate idents at 10 characters, because TS6 won't like them otherwise! + ident = userobj.ident[:10] + # Ditto hostname at 64 chars. + host = userobj.host[:64] + realname = userobj.realname + modes = getSupportedUmodes(irc, remoteirc, userobj.modes) + u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident, + host=host, realname=realname, + modes=modes, ts=userobj.ts).uid + remoteirc.users[u].remote = (irc.name, user) + away = userobj.away + if away: + remoteirc.proto.awayClient(remoteirc, u, away) + relayusers[(irc.name, user)][remoteirc.name] = u + return u def getLocalUser(irc, user, targetirc=None): """ [] @@ -160,21 +163,10 @@ def getLocalUser(irc, user, targetirc=None): representing the original user on the target network, similar to what getRemoteUser() does.""" # First, iterate over everyone! - remoteuser = None - for k, v in relayusers.items(): - if k[0] == irc.name: - # We don't need to do anything if the target users is on - # the same network as us. - continue - if v.get(irc.name) == user: - # If the stored pseudoclient UID for the kicked user on - # this network matches the target we have, set that user - # as the one we're kicking! It's a handful, but remember - # we're mapping (home network, UID) pairs to their - # respective relay pseudoclients on other networks. - remoteuser = k - log.debug('(%s) getLocalUser: found %s to correspond to %s.', irc.name, v, k) - break + try: + remoteuser = irc.users[user].remote + except (AttributeError, KeyError): + remoteuser = None log.debug('(%s) getLocalUser: remoteuser set to %r (looking up %s/%s).', irc.name, remoteuser, user, irc.name) if remoteuser: # If targetirc is given, we'll return simply the UID of the user on the @@ -223,6 +215,11 @@ def initializeChannel(irc, channel): log.debug('(%s) initializeChannel: relay pair found to be %s', irc.name, relay) queued_users = [] if relay: + # Send our users and channel modes to the other nets + log.debug('(%s) initializeChannel: joining our users: %s', irc.name, c.users) + relayJoins(irc, channel, c.users, c.ts) + irc.proto.joinClient(irc, irc.pseudoclient.uid, channel) + all_links = db[relay]['links'].copy() all_links.update((relay,)) log.debug('(%s) initializeChannel: all_links: %s', irc.name, all_links) @@ -239,29 +236,23 @@ def initializeChannel(irc, channel): if not (remoteirc.connected.is_set() and findRemoteChan(remoteirc, irc, remotechan)): continue # They aren't connected, don't bother! # 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 + relayJoins(remoteirc, remotechan, rc.users, rc.ts) + relayModes(remoteirc, irc, remoteirc.sid, remotechan, rc.modes) + relayModes(irc, remoteirc, irc.sid, channel, modes) + topic = remoteirc.channels[remotechan].topic # Only update the topic if it's different from what we already have, # and topic bursting is complete. - if remoteirc.channels[channel].topicset and topic != irc.channels[channel].topic: + if remoteirc.channels[remotechan].topicset and topic != irc.channels[channel].topic: 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) - def handle_join(irc, numeric, command, args): channel = args['channel'] if not findRelay((irc.name, channel)): # No relay here, return. return - modes = args['modes'] ts = args['ts'] users = set(args['users']) - relayJoins(irc, channel, users, ts, modes) + relayJoins(irc, channel, users, ts) utils.add_hook(handle_join, 'JOIN') def handle_quit(irc, numeric, command, args): @@ -308,57 +299,55 @@ def handle_privmsg(irc, numeric, command, args): text = args['text'] if target == irc.pseudoclient.uid: return - sent = 0 relay = findRelay((irc.name, target)) - # 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! - remoteusers = relayusers[(irc.name, numeric)].items() - ''' - if utils.isChannel(target) and ((relay and not db[relay]['links']) or \ - relay is None): + remoteusers = relayusers[(irc.name, numeric)] + # HACK: Don't break on sending to @#channel or similar. + try: + prefix, target = target.split('#', 1) + except ValueError: + prefix = '' + else: + target = '#' + target + log.debug('(%s) relay privmsg: prefix is %r, target is %r', irc.name, prefix, target) + if utils.isChannel(target) and relay and numeric not in irc.channels[target].users: + # The sender must be in the target channel to send messages over the relay; + # it's the only way we can make sure they have a spawned client on ALL + # of the linked networks. This affects -n channels too; see + # https://github.com/GLolol/PyLink/issues/91 for an explanation of why. + utils.msg(irc, numeric, 'Error: You must be in %r in order to send ' + 'messages over the relay.' % target, notice=True) return - ''' - if not remoteusers: - return - for netname, user in relayusers[(irc.name, numeric)].items(): - remoteirc = utils.networkobjects[netname] - # HACK: Don't break on sending to @#channel or similar. - try: - prefix, target = target.split('#', 1) - except ValueError: - prefix = '' - else: - target = '#' + target - if utils.isChannel(target): - log.debug('(%s) relay privmsg: prefix is %r, target is %r', irc.name, prefix, target) + if utils.isChannel(target): + for netname, user in relayusers[(irc.name, numeric)].items(): + remoteirc = utils.networkobjects[netname] real_target = findRemoteChan(irc, remoteirc, target) if not real_target: continue real_target = prefix + real_target - else: - remoteuser = getLocalUser(irc, target) - if remoteuser is None: - continue - real_target = remoteuser[1] + if notice: + remoteirc.proto.noticeClient(remoteirc, user, real_target, text) + else: + remoteirc.proto.messageClient(remoteirc, user, real_target, text) + else: + remoteuser = getLocalUser(irc, target) + if remoteuser is None: + return + homenet, real_target = remoteuser + # For PMs, we must be on a common channel with the target. + # Otherwise, the sender doesn't have a client representing them + # on the remote network, and we won't have anything to send our + # messages from. + if homenet not in remoteusers.keys(): + utils.msg(irc, numeric, 'Error: you must be in a common channel ' + 'with %r in order to send messages.' % \ + irc.users[target].nick, notice=True) + return + remoteirc = utils.networkobjects[homenet] + user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False) if notice: remoteirc.proto.noticeClient(remoteirc, user, real_target, text) else: remoteirc.proto.messageClient(remoteirc, user, real_target, text) - sent += 1 - ''' - if not sent: - # We must be on a common channel with the target. Otherwise, the sender - # doesn't have a client representing them on the remote network, - # and we won't have anywhere to send our messages from. - # In this case, we've iterated over all networks where the sender - # has pseudoclients, and found no suitable targets to send to. - if target in irc.users: - target_s = 'a common channel with %r' % irc.users[target].nick - else: - target_s = repr(target) - utils.msg(irc, numeric, 'Error: You must be in %s in order to send messages.' % \ - target_s, notice=True) - ''' utils.add_hook(handle_privmsg, 'PRIVMSG') utils.add_hook(handle_privmsg, 'NOTICE') @@ -386,7 +375,7 @@ def handle_kick(irc, source, command, args): # they originate from the same network. We won't have # to filter this; the uplink IRCd will handle it appropriately, # and we'll just follow. - real_target = getRemoteUser(irc, remoteirc, target) + real_target = getRemoteUser(irc, remoteirc, target, spawnIfMissing=False) log.debug('(%s) Relay kick: real target for %s is %s', irc.name, target, real_target) else: log.debug('(%s) Relay kick: target %s is an internal client, going to look up the real user', irc.name, target) @@ -428,10 +417,10 @@ def handle_kick(irc, source, command, args): 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] + if target != irc.pseudoclient.uid and not irc.users[target].channels: + remoteuser = getLocalUser(irc, target) + del relayusers[remoteuser][irc.name] + irc.proto.quitClient(irc, target, 'Left all shared channels.') utils.add_hook(handle_kick, 'KICK') @@ -643,7 +632,7 @@ def handle_kill(irc, numeric, command, args): utils.add_hook(handle_kill, 'KILL') -def relayJoins(irc, channel, users, ts, modes): +def relayJoins(irc, channel, users, ts): for name, remoteirc in utils.networkobjects.items(): queued_users = [] if name == irc.name or not remoteirc.connected.is_set(): @@ -672,7 +661,7 @@ def relayJoins(irc, channel, users, ts, modes): u = getRemoteUser(irc, remoteirc, user) # Only join users if they aren't already joined. This prevents op floods # on charybdis from all the SJOINing. - if u not in remoteirc.channels[channel].users: + if u not in remoteirc.channels[remotechan].users: ts = irc.channels[channel].ts prefixes = getPrefixModes(irc, remoteirc, channel, user) userpair = (prefixes, u) @@ -683,7 +672,6 @@ def relayJoins(irc, channel, users, ts, modes): u, remoteirc.name, remotechan) if queued_users: remoteirc.proto.sjoinServer(remoteirc, remoteirc.sid, remotechan, queued_users, ts=ts) - relayModes(irc, remoteirc, irc.sid, channel, modes) def relayPart(irc, channel, user): for name, remoteirc in utils.networkobjects.items(): diff --git a/protocols/inspircd.py b/protocols/inspircd.py index 7ac1f3c..2144caf 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -56,10 +56,11 @@ def joinClient(irc, client, channel): if not server: log.error('(%s) Error trying to join client %r to %r (no such pseudoclient exists)', irc.name, client, channel) raise LookupError('No such PyLink PseudoClient exists.') - # One channel per line here! + # Strip out list-modes, they shouldn't be ever sent in FJOIN. + modes = [m for m in irc.channels[channel].modes if m[0] not in irc.cmodes['*A']] _send(irc, server, "FJOIN {channel} {ts} {modes} :,{uid}".format( ts=irc.channels[channel].ts, uid=client, channel=channel, - modes=utils.joinModes(irc.channels[channel].modes))) + modes=utils.joinModes(modes))) irc.channels[channel].users.add(client) irc.users[client].channels.add(channel) @@ -70,16 +71,17 @@ def sjoinServer(irc, server, channel, users, ts=None): log.debug('(%s) sjoinServer: got %r for users', irc.name, users) if not server: raise LookupError('No such PyLink PseudoClient exists.') - if ts is None: - ts = irc.channels[channel].ts - log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, irc.name, ts, + orig_ts = irc.channels[channel].ts + ts = ts or orig_ts + if ts < orig_ts: + log.debug('(%s) sjoinServer: resetting TS of %r from %s to %s (clearing modes)', + irc.name, channel, orig_ts, ts) + irc.channels[channel].ts = ts + irc.channels[channel].modes.clear() + for p in irc.channels[channel].prefixmodes.values(): + p.clear() + log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, irc.name, ts, time.strftime("%c", time.localtime(ts))) - ''' TODO: handle this properly! - if modes is None: - modes = irc.channels[channel].modes - else: - utils.applyModes(irc, channel, modes) - ''' # Strip out list-modes, they shouldn't be ever sent in FJOIN. modes = [m for m in irc.channels[channel].modes if m[0] not in irc.cmodes['*A']] uids = [] @@ -120,8 +122,12 @@ def removeClient(irc, numeric): Removes a client from our internal databases, regardless of whether it's one of our pseudoclients or not.""" - for v in irc.channels.values(): + 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] @@ -245,6 +251,8 @@ 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): @@ -252,6 +260,8 @@ def topicServer(irc, numeric, target, text): ts = int(time.time()) servername = irc.servers[numeric].name _send(irc, numeric, 'FTOPIC %s %s %s :%s' % (target, ts, servername, text)) + irc.channels[target].topic = text + irc.channels[target].topicset = True def inviteClient(irc, numeric, target, channel): """ @@ -360,6 +370,9 @@ def handle_part(irc, source, command, args): 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): @@ -378,6 +391,9 @@ def handle_fjoin(irc, servernumeric, command, args): log.debug('(%s) Setting channel TS of %s to %s from %s', irc.name, channel, their_ts, our_ts) irc.channels[channel].ts = their_ts + irc.channels[channel].modes.clear() + for p in irc.channels[channel].prefixmodes.values(): + p.clear() modestring = args[2:-1] or args[2] parsedmodes = utils.parseModes(irc, channel, modestring) utils.applyModes(irc, channel, parsedmodes) diff --git a/protocols/ts6.py b/protocols/ts6.py index 7f555e5..745f47b 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -77,8 +77,15 @@ def sjoinServer(irc, server, channel, users, ts=None): log.debug('(%s) sjoinServer: got %r for users', irc.name, users) if not server: raise LookupError('No such PyLink PseudoClient exists.') - if ts is None: - ts = irc.channels[channel].ts + orig_ts = irc.channels[channel].ts + ts = ts or orig_ts + if ts < orig_ts: + log.debug('(%s) sjoinServer: resetting TS of %r from %s to %s (clearing modes)', + irc.name, channel, orig_ts, ts) + irc.channels[channel].ts = ts + irc.channels[channel].modes.clear() + for p in irc.channels[channel].prefixmodes.values(): + p.clear() log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, irc.name, ts, time.strftime("%c", time.localtime(ts))) modes = [m for m in irc.channels[channel].modes if m[0] not in irc.cmodes['*A']] @@ -95,6 +102,7 @@ def sjoinServer(irc, server, channel, users, ts=None): pr = irc.prefixmodes.get(prefix) if pr: prefixchars += pr + changedmodes.append(('+%s' % prefix, user)) namelist.append(prefixchars+user) uids.append(user) try: @@ -186,6 +194,8 @@ def topicServer(irc, numeric, target, text): ts = irc.channels[target].ts servername = irc.servers[numeric].name _send(irc, numeric, 'TB %s %s %s :%s' % (target, ts, servername, text)) + irc.channels[target].topic = text + irc.channels[target].topicset = True def inviteClient(irc, numeric, target, channel): """ @@ -368,9 +378,10 @@ def handle_part(irc, source, command, args): 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 = args[1].lower() @@ -382,6 +393,9 @@ def handle_sjoin(irc, servernumeric, command, args): log.debug('(%s) Setting channel TS of %s to %s from %s', irc.name, channel, their_ts, our_ts) irc.channels[channel].ts = their_ts + irc.channels[channel].modes.clear() + for p in irc.channels[channel].prefixmodes.values(): + p.clear() modestring = args[2:-1] or args[2] parsedmodes = utils.parseModes(irc, channel, modestring) utils.applyModes(irc, channel, parsedmodes) diff --git a/utils.py b/utils.py index 4fc6890..db02ff4 100644 --- a/utils.py +++ b/utils.py @@ -241,7 +241,10 @@ def applyModes(irc, target, changedmodes): log.debug('(%s) Applying modes %r on %s (initial modelist: %s)', irc.name, changedmodes, target, modelist) for mode in changedmodes: # Chop off the +/- part that parseModes gives; it's meaningless for a mode list. - real_mode = (mode[0][1], mode[1]) + try: + real_mode = (mode[0][1], mode[1]) + except IndexError: + real_mode = mode if not usermodes: pmode = '' for m in ('owner', 'admin', 'op', 'halfop', 'voice'):