def _sayit(irc, sid, userdict, channel, nick, text, action=False): """Mimic core function.""" # Fix imperfections in the REGEX matching. nick = nick.strip(('\x03\x02\x01')) lowernick = irc.toLower(nick) uid = userdict.get(lowernick) log.debug('(%s) mimic: got UID %s for nick %s', irc.name, uid, nick) if not uid: # Nick doesn't exist; make a new client for it. ircnick = nick ircnick = ircnick.replace('/', '|') if irc.nickToUid(nick): # Nick exists, but is not one of our temp users. Tag it with |mimic ircnick += MIMICSUFFIX if nick.startswith(tuple(string.digits)): ircnick = '_' + ircnick if not utils.isNick(ircnick): log.warning('(%s) mimic: Bad nick %s, ignoring text: <%s> %s', irc.name, ircnick, nick, text) return userdict userdict[lowernick] = uid = irc.proto.spawnClient(ircnick, 'mimic', HOSTNAME, server=sid).uid log.debug('(%s) mimic: spawning client %s for nick %s (really %s)', irc.name, uid, ircnick, nick) irc.proto.join(uid, channel) if action: # Format CTCP action text = '\x01ACTION %s\x01' % text irc.proto.message(uid, channel, text) return userdict
def handle_join(irc, source, command, args): """Monitors channel joins for dynamic service bot joining.""" if irc.has_cap('visible-state-only'): # No-op on bot-only servers. return channel = args['channel'] users = irc.channels[channel].users for servicename, sbot in world.services.items(): if channel in sbot.get_persistent_channels(irc) and \ sbot.uids.get(irc.name) not in users: log.debug('(%s) Dynamically joining service %r to channel %r.', irc.name, servicename, channel) sbot.join(irc, channel)
def _burst_new_client(self, guild, member, pylink_netobj): """Bursts the given member as a new PyLink client.""" uid = member.id if not member.name: log.debug('(%s) Not bursting user %s as their data is not ready yet', self.protocol.name, member) return if uid in pylink_netobj.users: log.debug('(%s) Not reintroducing user %s/%s', self.protocol.name, uid, member.user.username) pylink_user = pylink_netobj.users[uid] elif uid != self.me.id: tag = str(member.user) # get their name#1234 tag username = member.user.username # this is just the name portion realname = '%s @ Discord/%s' % (tag, guild.name) # Prefer the person's guild nick (nick=member.name) if defined pylink_netobj.users[uid] = pylink_user = User(pylink_netobj, nick=member.name, ident=username, realname=realname, host='discord/user/%s' % tag, # XXX make this configurable ts=calendar.timegm(member.joined_at.timetuple()), uid=uid, server=guild.id) pylink_user.modes.add(('i', None)) pylink_user.services_account = str(uid) # Expose their UID as a services account if member.user.bot: pylink_user.modes.add(('B', None)) pylink_user.discord_user = member pylink_netobj.call_hooks([ guild.id, 'UID', { 'uid': uid, 'ts': pylink_user.ts, 'nick': pylink_user.nick, 'realhost': pylink_user.realhost, 'host': pylink_user.host, 'ident': pylink_user.ident, 'ip': pylink_user.ip } ]) else: return # Update user presence self._update_user_status(guild, uid, member.user.presence) # Calculate which channels the user belongs to for channel in guild.channels.values(): if channel.type == ChannelType.GUILD_TEXT: self._update_channel_presence(guild, channel, member) return pylink_user
def handle_euid(self, numeric, command, args): """Handles incoming EUID commands (user introduction).""" # <- :42X EUID GL 1 1437505322 +ailoswz ~gl 127.0.0.1 127.0.0.1 42XAAAAAB * * :realname nick = args[0] self.check_nick_collision(nick) ts, modes, ident, host, ip, uid, realhost, accountname, realname = args[ 2:11] if realhost == '*': realhost = None log.debug( '(%s) handle_euid got args: nick=%s ts=%s uid=%s ident=%s ' 'host=%s realname=%s realhost=%s ip=%s', self.irc.name, nick, ts, uid, ident, host, realname, realhost, ip) assert ts != 0, "Bad TS 0 for user %s" % uid if ip == '0': # IP was invalid; something used for services. ip = '0.0.0.0' self.irc.users[uid] = IrcUser(nick, ts, uid, numeric, ident, host, realname, realhost, ip) parsedmodes = self.irc.parseModes(uid, [modes]) log.debug('Applying modes %s for %s', 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: otype = 'Server Administrator' if ( '+a', None) in parsedmodes else 'IRC Operator' self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': otype}]) # Set the accountname if present if accountname != "*": self.irc.callHooks( [uid, 'CLIENT_SERVICES_LOGIN', { 'text': accountname }]) return { 'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip }
def on_channel_delete(self, event, *args, **kwargs): channel = event.channel try: pylink_netobj = self.protocol._children[event.channel.guild_id] except KeyError: log.debug("(%s) Could not delete channel %s as the parent network object does not exist", self.protocol.name, event.channel) return if channel.id not in pylink_netobj.channels: # wasn't a type of channel we track return # Remove the channel from everyone's channel list for u in pylink_netobj.channels[channel.id].users: pylink_netobj.users[u].channels.discard(channel.id) del pylink_netobj.channels[channel.id]
def handle_kick(self, source, command, args): """Handles incoming KICKs.""" # :70MAAAAAA KICK #test 70MAAAAAA :some reason channel = args[0] kicked = self._get_UID(args[1]) try: reason = args[2] except IndexError: reason = '' log.debug('(%s) Removing kick target %s from %s', self.name, kicked, channel) self.handle_part(kicked, 'KICK', [channel, reason]) return {'channel': channel, 'target': kicked, 'text': reason}
def _kill(): if target not in irc.users: log.debug('(%s) antispam: not killing %s/%s; they already left', irc.name, target, irc.get_friendly_name(target)) return userdata = irc.users[target] irc.kill(my_uid, target, reason) irc.call_hooks([ my_uid, 'ANTISPAM_KILL', { 'target': target, 'text': reason, 'userdata': userdata, 'parse_as': 'KILL' } ])
def handle_kill(irc, numeric, command, args): """ Tracks kills against PyLink clients. If too many are received, automatically disconnects from the network. """ if (args['userdata'] and irc.isInternalServer( args['userdata'].server)) or irc.isInternalClient(args['target']): if killcache.setdefault(irc.name, 1) >= length: log.error('(%s) servprotect: Too many kills received, aborting!', irc.name) irc.disconnect() log.debug('(%s) servprotect: Incrementing killcache by 1', irc.name) killcache[irc.name] += 1
def _state_cleanup_mode(irc, source, command, args): """ Cleans up and removes empty channels when -P (permanent mode) is removed from them. """ target = args['target'] if target in irc.channels and 'permanent' in irc.cmodes: c = irc.channels[target] mode = '-%s' % irc.cmodes['permanent'] if (not c.users) and (mode, None) in args['modes']: log.debug( '(%s) _state_cleanup_mode: deleting empty channel %s as %s was set', irc.name, target, mode) del irc._channels[target] return False # Block further hooks from running
def main(irc=None): """Main function, called during plugin loading at start.""" # Load the automode database. datastore.load() # Register our permissions. permissions.addDefaultPermissions(default_permissions) # Queue joins to all channels where Automode has entries. for entry in db: netname, channel = entry.split('#', 1) channel = '#' + channel log.debug('automode: auto-joining %s on %s', channel, netname) modebot.join(netname, channel)
def handle_mode(self, numeric, command, args): # <- :unreal.midnight.vpn MODE #test +bb test!*@* *!*@bad.net # <- :unreal.midnight.vpn MODE #test +q jlu5 1444361345 # <- :unreal.midnight.vpn MODE #test +ntCo jlu5 1444361345 # <- :unreal.midnight.vpn MODE #test +mntClfo 5 [10t]:5 jlu5 1444361345 # <- :jlu5 MODE #services +v jlu5 # This seems pretty relatively inconsistent - why do some commands have a TS at the end while others don't? # Answer: the first syntax (MODE sent by SERVER) is used for channel bursts - according to Unreal 3.2 docs, # the last argument should be interpreted as a timestamp ONLY if it is a number and the sender is a server. # Ban bursting does not give any TS, nor do normal users setting modes. SAMODE is special though, it will # send 0 as a TS argument (which should be ignored unless breaking the internal channel TS is desired). # Also, we need to get rid of that extra space following the +f argument. :| if self.is_channel(args[0]): channel = args[0] oldobj = self._channels[channel].deepcopy() modes = [arg for arg in args[1:] if arg] # normalize whitespace parsedmodes = self.parse_modes(channel, modes) if parsedmodes: if parsedmodes[0][0] == '+&': # UnrealIRCd uses a & virtual mode to denote mode bounces, meaning that an # attempt to set modes by us was rejected for some reason (usually due to # timestamps). Drop the mode change to prevent mode floods. log.debug( "(%s) Received mode bounce %s in channel %s! Our TS: %s", self.name, modes, channel, self._channels[channel].ts) return self.apply_modes(channel, parsedmodes) if numeric in self.servers and args[-1].isdigit(): # Sender is a server AND last arg is number. Perform TS updates. their_ts = int(args[-1]) if their_ts > 0: self.updateTS(numeric, channel, their_ts) return { 'target': channel, 'modes': parsedmodes, 'channeldata': oldobj } else: # User mode change target = self._get_UID(args[0]) return self._handle_umode(target, self.parse_modes(target, args[1:]))
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] self.check_nick_collision(nick) 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, numeric, 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_kick(self, source, command, args): """ Handles incoming KICKs. """ # <- :[email protected] KICK #whatever GL| :xd channel = self.irc.toLower(args[0]) target = self.irc.nickToUid(args[1]) try: reason = args[2] except IndexError: reason = '' if channel in self.kick_queue: # Remove this client from the kick queue if present there. log.debug('(%s) kick: removing %s from kick queue for channel %s', self.irc.name, target, channel) self.kick_queue[channel][0].discard(target) if not self.kick_queue[channel][0]: log.debug( '(%s) kick: cancelling kick timer for channel %s (all kicks accounted for)', self.irc.name, channel) # There aren't any kicks that failed to be acknowledged. We can remove the timer now self.kick_queue[channel][1].cancel() del self.kick_queue[channel] # Statekeeping: remove the target from the channel they were previously in. self.irc.channels[channel].removeuser(target) try: self.irc.users[target].channels.remove(channel) except KeyError: pass if (not self.irc.isInternalClient(source) ) and not self.irc.isInternalServer(source): # Don't repeat hooks if we're the kicker. self.irc.callHooks([ source, 'KICK', { 'channel': channel, 'target': target, 'text': reason } ]) # Delete channels that we were kicked from, for better state keeping. if self.irc.pseudoclient and target == self.irc.pseudoclient.uid: del self.irc.channels[channel]
def handle_uid(irc, source, command, args): """Checks incoming connections against SSHBL.""" ip = args['ip'] exemptions = irc.get_service_option('sshbl', 'exempt_hosts', default=None) or [] if not irc.connected.is_set(): # Don't scan users until we've finished bursting. return elif not ipaddress.ip_address(ip).is_global: log.debug("sshbl: skipping scanning local address %s", ip) return elif args['uid'] in irc.users and exemptions: for glob in exemptions: if irc.match_host(glob, args['uid']): log.debug("sshbl: skipping scanning exempt address %s (%s/%s)", ip, irc.name, args['nick']) return pool.submit(_check_connection, irc, args)
def join(self, client, channel): """STUB: Joins a user to a channel.""" if self.pseudoclient and client == self.pseudoclient.uid: log.debug("(%s) discord: ignoring explicit channel join to %s", self.name, channel) return elif channel not in self.channels: log.warning("(%s) Ignoring attempt to join unknown channel ID %s", self.name, channel) return self.channels[channel].users.add(client) self.users[client].channels.add(channel) log.debug('(%s) join: faking JOIN of client %s/%s to %s', self.name, client, self.get_friendly_name(client), channel) self.call_hooks([client, 'CLIENTBOT_JOIN', {'channel': channel}])
def _services_dynamic_part(irc, channel): """Dynamically removes service bots from empty channels.""" if irc.has_cap('visible-state-only'): # No-op on bot-only servers. return if irc.serverdata.get('join_empty_channels', conf.conf['pylink'].get('join_empty_channels', False)): return # If all remaining users in the channel are service bots, make them all part. if all(irc.get_service_bot(u) for u in irc.channels[channel].users): for u in irc.channels[channel].users.copy(): sbot = irc.get_service_bot(u) if sbot: log.debug('(%s) Dynamically parting service %r from channel %r.', irc.name, sbot.name, channel) irc.part(u, channel) return True
def sjoin(self, server, channel, users, ts=None, modes=set()): """Sends an SJOIN for a group of users to a channel. The sender should always be a Server ID (SID). TS is optional, and defaults to the one we've stored in the channel state if not given. <users> is a list of (prefix mode, UID) pairs: Example uses: sjoin('100', '#test', [('', 'user0@0'), ('o', user1@1'), ('v', 'someone@2')]) sjoin(self.sid, '#test', [('o', self.pseudoclient.uid)]) """ server = server or self.sid if not server: raise LookupError('No such PyLink client exists.') log.debug('(%s) sjoin: got %r for users', self.name, users) njoin_prefix = ':%s NJOIN %s :' % (self._expandPUID(server), channel) # Format the user list into strings such as @user1, +user2, user3, etc. nicks_to_send = [] for userpair in users: prefixes, uid = userpair if uid not in self.users: log.warning('(%s) Trying to NJOIN missing user %s?', self.name, uid) continue elif uid in self._channels[channel].users: # Don't rejoin users already in the channel, this causes errors with ngIRCd. continue self._channels[channel].users.add(uid) self.users[uid].channels.add(channel) self.apply_modes(channel, (('+%s' % prefix, uid) for prefix in userpair[0])) nicks_to_send.append(''.join(self.prefixmodes[modechar] for modechar in userpair[0]) + \ self._expandPUID(userpair[1])) if nicks_to_send: # Use 13 args max per line: this is equal to the max of 15 minus the command name and target channel. for message in utils.wrap_arguments(njoin_prefix, nicks_to_send, self.S2S_BUFSIZE, separator=',', max_args_per_line=13): self.send(message) if modes: # Burst modes separately if there are any. log.debug("(%s) sjoin: bursting modes %r for channel %r now", self.name, modes, channel) self.mode(server, channel, modes)
def account(irc, host, uid): """ $account exttarget handler. The following forms are supported, with groups separated by a literal colon. Account matching is case insensitive, while network name matching IS case sensitive. $account -> Returns True (a match) if the target is registered. $account:accountname -> Returns True if the target's account name matches the one given, and the target is connected to the local network.. $account:accountname:netname -> Returns True if both the target's account name and origin network name match the ones given. $account:*:netname -> Matches all logged in users on the given network. """ userobj = irc.users[uid] homenet = irc.name if hasattr(userobj, 'remote'): # User is a PyLink Relay pseudoclient. Use their real services account on their # origin network. homenet, realuid = userobj.remote log.debug( '(%s) exttargets.account: Changing UID of relay client %s to %s/%s', irc.name, uid, homenet, realuid) try: userobj = world.networkobjects[homenet].users[realuid] except KeyError: # User lookup failed. Bail and return False. log.exception('(%s) exttargets.account: KeyError finding %s/%s:', irc.name, homenet, realuid) return False slogin = irc.toLower(userobj.services_account) # Split the given exttarget host into parts, so we know how many to look for. groups = list(map(irc.toLower, host.split(':'))) log.debug('(%s) exttargets.account: groups to match: %s', irc.name, groups) if len(groups) == 1: # First scenario. Return True if user is logged in. return bool(slogin) elif len(groups) == 2: # Second scenario. Return True if the user's account matches the one given. return slogin == groups[1] and homenet == irc.name else: # Third or fourth scenario. If there are more than 3 groups, the rest are ignored. # In other words: Return True if the user is logged in, the query matches either '*' or the # user's login, and the user is connected on the network requested. return slogin and (groups[1] in ('*', slogin)) and (homenet == groups[2])
def handle_server(self, numeric, command, args): """Handles the SERVER command, which is used for both authentication and introducing legacy (non-SID) servers.""" # <- SERVER unreal.midnight.vpn 1 :U3999-Fhin6OoEM UnrealIRCd test server sname = args[0] if numeric == self.irc.uplink and not self.irc.connected.is_set( ): # We're doing authentication for cap in self.needed_caps: if cap not in self.caps: raise ProtocolError( "Not all required capabilities were met " "by the remote server. Your version of UnrealIRCd " "is probably too old! (Got: %s, needed: %s)" % (sorted(self.caps), sorted(self.needed_caps))) sdesc = args[-1].split(" ", 1) # Get our protocol version. I really don't know why the version and the server # description aren't two arguments instead of one... -GLolol vline = sdesc[0].split('-', 1) sdesc = " ".join(sdesc[1:]) try: protover = int(vline[0].strip('U')) except ValueError: raise ProtocolError( "Protocol version too old! (needs at least %s " "(Unreal 4.x), got something invalid; " "is VL being sent?)" % self.min_proto_ver) if protover < self.min_proto_ver: raise ProtocolError( "Protocol version too old! (needs at least %s " "(Unreal 4.x), got %s)" % (self.min_proto_ver, protover)) self.irc.servers[numeric] = IrcServer(None, sname, desc=sdesc) # Set irc.connected to True, meaning that protocol negotiation passed. log.debug('(%s) self.irc.connected set!', self.irc.name) self.irc.connected.set() else: # Legacy (non-SID) servers can still be introduced using the SERVER command. # <- :services.int SERVER a.bc 2 :(H) [GL] a servername = args[0].lower() sdesc = args[-1] self.irc.servers[servername] = IrcServer(numeric, servername, desc=sdesc) return {'name': servername, 'sid': None, 'text': sdesc}
def handle_nick(self, numeric, command, args): """Handles NICK changes, and legacy NICK introductions from pre-4.0 servers.""" if self.mixed_link and len(args) > 2: # Handle legacy NICK introduction here. # I don't want to rewrite all the user introduction stuff, so I'll just reorder the arguments # so that handle_uid can handle this instead. # But since legacy nicks don't have any UIDs attached, we'll have to store the users # internally by their nicks. In other words, we need to convert from this: # <- NICK Global 3 1456843578 services novernet.com services.novernet.com 0 +ioS * :Global Noticer # & nick hopcount timestamp username hostname server service-identifier-token :realname # With NICKIP and VHP enabled: # <- NICK GL32 2 1470699865 gl localhost unreal32.midnight.vpn GL +iowx hidden-1C620195 AAAAAAAAAAAAAAAAAAAAAQ== :realname # to this: # <- :001 UID GL 0 1441306929 gl localhost 0018S7901 0 +iowx * hidden-1C620195 fwAAAQ== :realname log.debug('(%s) got legacy NICK args: %s', self.irc.name, ' '.join(args)) new_args = args[:] # Clone the old args list servername = new_args[5].lower( ) # Get the name of the users' server. # Fake a UID and put it where it belongs in the new-style UID command. fake_uid = '%s@%s' % (args[0], self.legacy_nickcount) self.legacy_nickcount += 1 new_args[5] = fake_uid # This adds a dummy cloaked host (equal the real host) to put the displayed host in the # right position. As long as the VHP capability is respected, this will propagate +x cloaked # hosts from UnrealIRCd 3.2 users. Otherwise, +x host cloaking won't work! new_args.insert(-2, args[4]) log.debug('(%s) translating legacy NICK args to: %s', self.irc.name, ' '.join(new_args)) return self.handle_uid(servername, 'UID_LEGACY', new_args) else: # Normal NICK change, just let ts6_common handle it. # :70MAAAAAA NICK GL-devel 1434744242 try: return super().handle_nick(numeric, command, args) except KeyError: log.exception( '(%s) Malformed NICK command received. If you are linking PyLink to a ' 'mixed UnrealIRCd 3.2/4.0 network, enable the mixed_link option in the ' 'server config and restart your PyLink daemon.', self.irc.name) self.irc.disconnect()
def _submit_dnsblim(irc, ip, apikey, nickuserhost=None): reason = irc.get_service_option('badchans', 'dnsbl_reason', DEFAULT_DNSBL_REASON) request = { 'key': apikey, 'addresses': [{ 'ip': ip, 'type': str(DNSBLIM_TYPE), 'reason': reason, }], } headers = {'Content-Type': 'application/json'} log.debug('(%s) badchans: posting to dnsblim: %s', irc.name, request) # Expecting this to block r = requests.post('https://api.dnsbl.im/import', data=json.dumps(request), headers=headers)
def serverdata(self): """ Implements serverdata property for Discord subservers. This merges in the root serverdata config block, plus any guild-specific settings for this guild. NOTE: serverdata in DiscordServer is not modifiable because it is dynamically generated. Changes should instead be made to self.virtual_parent.serverdata """ if getattr(self, 'sid', None): data = self.virtual_parent.serverdata.copy() guild_data = data.get('guilds', {}).get(self.sid, {}) log.debug('serverdata: merging data %s with guild_data %s', data, guild_data) data.update(guild_data) return data else: log.debug('serverdata: sid not set, using parent data only') return self.virtual_parent.serverdata
def _process_hooks(self): """Loop to process incoming hook data.""" while not self._aborted.is_set(): data = self._hooks_queue.get() if data is None: log.debug('(%s) Stopping queue thread due to getting None as item', self.name) break elif self not in world.networkobjects.values(): log.debug('(%s) Stopping stale queue thread; no longer matches world.networkobjects', self.name) break subserver, data = data if subserver not in world.networkobjects: log.error('(%s) Not queuing hook for subserver %r no longer in networks list.', self.name, subserver) elif subserver in self._children: self._children[subserver].call_hooks(data)
def handle_server(self, source, command, args): """ Handles the SERVER command. """ # <- :ngircd.midnight.local SERVER ngircd.midnight.local 1 :ngIRCd dev server servername = args[0].lower() serverdesc = args[-1] # The uplink should be set to None for the uplink; otherwise, set it equal to the sender server. self.servers[servername] = Server(self, source if source != servername else None, servername, desc=serverdesc) if self.uplink is None: self.uplink = servername log.debug('(%s) Got %s as uplink', self.name, servername) else: # Only send the SERVER hook if this isn't the initial connection. return {'name': servername, 'sid': None, 'text': serverdesc}
def handle_ctcp(irc, source, command, args): """ CTCP event handler. """ text = args['text'] if not (text.startswith('\x01') and text.endswith('\x01')): return None # Pass through to other plugins target = args['target'] if not irc.get_service_bot(target): # Ignore this message if the target isn't a service bot return None text = text.strip('\x01') try: ctcp_command, data = text.split(" ", 1) except ValueError: ctcp_command = text data = '' ctcp_command = ctcp_command.upper() log.debug('(%s) ctcp: got CTCP command %r, data %r', irc.name, ctcp_command, data) if ctcp_command in SUPPORTED_COMMANDS: log.info('(%s) Received CTCP %s from %s to %s', irc.name, ctcp_command, irc.get_hostmask(source), irc.get_friendly_name(target)) # Call the helper function and display its result. result = SUPPORTED_COMMANDS[ctcp_command](irc, source, ctcp_command, data) if result and source in irc.users: # Note, do NOT use irc.reply() in hook handlers because nothing except the # command handler system actually updates the last caller. irc.msg(source, '\x01%s %s\x01' % (ctcp_command, result), notice=True, source=target) return False # Block this message from reaching the general command handler else: log.info('(%s) Received unknown CTCP %s from %s to %s', irc.name, ctcp_command, irc.get_hostmask(source), irc.get_friendly_name(target)) return False
def match(irc, channel, uids=None): """ Automode matcher engine. """ dbentry = db.get(irc.name + channel) if dbentry is None: return modebot_uid = modebot.uids.get(irc.name) # Check every mask defined in the channel ACL. outgoing_modes = [] # If UIDs are given, match those. Otherwise, match all users in the given channel. uids = uids or irc.channels[channel].users for mask, modes in dbentry.items(): for uid in uids: if irc.matchHost(mask, uid): # User matched a mask. Filter the mode list given to only those that are valid # prefix mode characters. outgoing_modes += [('+' + mode, uid) for mode in modes if mode in irc.prefixmodes] log.debug( "(%s) automode: Filtered mode list of %s to %s (protocol:%s)", irc.name, modes, outgoing_modes, irc.protoname) if outgoing_modes: # If the Automode bot is missing, send the mode through the PyLink server. if modebot_uid not in irc.users: modebot_uid = irc.sid log.debug("(%s) automode: sending modes from modebot_uid %s", irc.name, modebot_uid) irc.proto.mode(modebot_uid, channel, outgoing_modes) # Create a hook payload to support plugins like relay. irc.callHooks([ modebot_uid, 'AUTOMODE_MODE', { 'target': channel, 'modes': outgoing_modes, 'parse_as': 'MODE' } ])
def _rehash(): """Rehashes the PyLink daemon.""" log.info('Reloading PyLink configuration...') old_conf = conf.conf.copy() fname = conf.fname new_conf = conf.loadConf(fname, errors_fatal=False, logger=log) conf.conf = new_conf # Reset any file logger options. stopFileLoggers() files = new_conf['logging'].get('files') if files: for filename, config in files.items(): makeFileLogger(filename, config.get('loglevel')) log.debug('rehash: updating console log level') world.console_handler.setLevel(getConsoleLogLevel()) # Reset permissions. log.debug('rehash: resetting permissions') permissions.resetPermissions() for network, ircobj in world.networkobjects.copy().items(): # Server was removed from the config file, disconnect them. log.debug('rehash: checking if %r is in new conf still.', network) if network not in new_conf['servers']: log.debug( 'rehash: removing connection to %r (removed from config).', network) remove_network(ircobj) else: # XXX: we should really just add abstraction to Irc to update config settings... ircobj.conf = new_conf ircobj.serverdata = new_conf['servers'][network] ircobj.botdata = new_conf['bot'] ircobj.autoconnect_active_multiplier = 1 # Clear the IRC object's channel loggers and replace them with # new ones by re-running logSetup(). while ircobj.loghandlers: log.removeHandler(ircobj.loghandlers.pop()) ircobj.logSetup() utils.resetModuleDirs() for network, sdata in new_conf['servers'].items(): # Connect any new networks or disconnected networks if they aren't already. if (network not in world.networkobjects) or ( not world.networkobjects[network].connection_thread.is_alive() ): proto = utils.getProtocolModule(sdata['protocol']) world.networkobjects[network] = classes.Irc( network, proto, new_conf) log.info('Finished reloading PyLink configuration.')
def fml(irc, source, args): """[<id>] Displays an entry from fmylife.com. If <id> is not given, fetch a random entry from the API.""" try: query = args[0] except IndexError: # Get a random FML from the API. query = 'random' # TODO: configurable language? url = ('http://api.betacie.com/view/%s/nocomment' '?key=4be9c43fc03fe&language=en' % query) try: data = urllib.request.urlopen(url).read() except urllib.error as e: error(irc, '%s' % e) return tree = ElementTree.fromstring(data.decode('utf-8')) tree = tree.find('items/item') try: category = tree.find('category').text text = tree.find('text').text fmlid = tree.attrib['id'] url = tree.find('short_url').text except AttributeError as e: log.debug("games.FML: Error fetching FML %s from URL %s: %s", query, url, e) error( irc, "That FML does not exist or there was an error " "fetching data from the API.") return if not fmlid: error(irc, "That FML does not exist.") return # TODO: customizable formatting votes = "\x02[Agreed: %s / Deserved: %s]\x02" % \ (tree.find('agree').text, tree.find('deserved').text) s = '\x02#%s [%s]\x02: %s - %s \x02<\x0311%s\x03>\x02' % \ (fmlid, category, text, votes, url) reply(irc, s)
def ircop(irc, host, uid): """ $ircop exttarget handler. The following forms are supported, with groups separated by a literal colon. Oper types are matched case insensitively. $ircop -> Returns True (a match) if the target is opered. $ircop:*admin* -> Returns True if the target's is opered and their opertype matches the glob given. """ groups = host.split(':') log.debug('(%s) exttargets.ircop: groups to match: %s', irc.name, groups) if len(groups) == 1: # 1st scenario. return irc.is_oper(uid) else: # 2nd scenario. Use match_host (ircmatch) to match the opertype glob to the opertype. return irc.match_host(groups[1], irc.users[uid].opertype)
def pylinkacc(irc, host, uid): """ $pylinkacc (PyLink account) exttarget handler. The following forms are supported, with groups separated by a literal colon. Account matching is case insensitive. $pylinkacc -> Returns True if the target is logged in to PyLink. $pylinkacc:accountname -> Returns True if the target's PyLink login matches the one given. """ login = irc.to_lower(irc.users[uid].account) groups = list(map(irc.to_lower, host.split(':'))) log.debug('(%s) exttargets.pylinkacc: groups to match: %s', irc.name, groups) if len(groups) == 1: # First scenario. Return True if user is logged in. return bool(login) elif len(groups) == 2: # Second scenario. Return True if the user's login matches the one given. return login == groups[1]
def _kill_plugins(irc=None): if not world.plugins: # No plugins were loaded or we were in a pre-initialized state, ignore. return log.info("Shutting down plugins.") for name, plugin in world.plugins.items(): # Before closing connections, tell all plugins to shutdown cleanly first. if hasattr(plugin, 'die'): log.debug( 'coremods.control: Running die() on plugin %s due to shutdown.', name) try: plugin.die(irc) except: # But don't allow it to crash the server. log.exception( 'coremods.control: Error occurred in die() of plugin %s, skipping...', name)
def raw(irc, source, args): """<text> Admin-only. Sends raw text to the uplink IRC server. \x02**WARNING: THIS CAN BREAK YOUR NETWORK IF USED IMPROPERLY!**\x02""" permissions.checkPermissions(irc, source, ['exec.raw']) args = ' '.join(args) if not args.strip(): irc.reply('No text entered!') return log.debug('(%s) Sending raw text %r to IRC for %s', irc.name, args, irc.getHostmask(source)) irc.send(args) irc.reply("Done.")
def _should_enforce(irc, source, args): exemptions = irc.get_service_option('operlock', 'exempt_hosts', default=None) or [] if not irc.connected.is_set(): # Don't act users until we've finished bursting. return False elif source not in irc.users: # Target isn't a user return False elif irc.is_internal_client(source): # Don't enforce against internal clients. return # Ignore exempt hosts for glob in exemptions: if irc.match_host(glob, source): log.debug("(%s) operlock: skipping exempt user %s", irc.name, irc.get_friendly_name(source)) return False return True
def on_member_remove(self, event: GuildMemberRemove, *args, **kwargs): log.info('(%s) got GuildMemberRemove event for guild %s: %s', self.protocol.name, event.guild_id, event.user) try: pylink_netobj = self.protocol._children[event.guild_id] except KeyError: log.debug( "(%s) Could not remove user %s as the parent network object does not exist", self.protocol.name, event.user) return if event.user.id in pylink_netobj.users: pylink_netobj._remove_client(event.user.id) # XXX: make the message configurable pylink_netobj.call_hooks( [event.user.id, 'QUIT', { 'text': 'User left the guild' }])
def _submit_dronebl(irc, ip, apikey, nickuserhost=None): reason = irc.get_service_option('badchans', 'dnsbl_reason', DEFAULT_DNSBL_REASON) request = '<add ip="%s" type="%s" comment="%s" />' % (ip, DRONEBL_TYPE, reason) xml_data = '<?xml version="1.0"?><request key="%s">%s</request>' % (apikey, request) headers = {'Content-Type': 'text/xml'} log.debug('(%s) badchans: posting to dronebl: %s', irc.name, xml_data) # Expecting this to block r = requests.post('https://dronebl.org/RPC2', data=xml_data, headers=headers) dronebl_response = r.text log.debug('(%s) badchans: got response from dronebl: %s', irc.name, dronebl_response) if '<success' in dronebl_response: log.info('(%s) badchans: got success for DroneBL on %s (%s)', irc.name, ip, nickuserhost or 'some n!u@h') else: log.warning('(%s) badchans: dronebl submission error: %s', irc.name, dronebl_response)
def handle_join(irc, source, command, args): """ killonjoin JOIN listener. """ # Ignore our own clients and other Ulines if irc.is_privileged_service(source) or irc.is_internal_server(source): return badchans = irc.serverdata.get('badchans') if not badchans: return elif not isinstance(badchans, list): log.error("(%s) badchans: the 'badchans' option must be a list of strings, not a %s", irc.name, type(badchans)) return use_kline = irc.get_service_option('badchans', 'use_kline', False) kline_duration = irc.get_service_option('badchans', 'kline_duration', DEFAULT_BAN_DURATION) try: kline_duration = utils.parse_duration(kline_duration) except ValueError: log.warning('(%s) badchans: invalid kline duration %s', irc.name, kline_duration, exc_info=True) kline_duration = DEFAULT_BAN_DURATION channel = args['channel'] for badchan in badchans: if irc.match_text(badchan, channel): asm_uid = None # Try to kill from the antispam service if available if 'antispam' in world.services: asm_uid = world.services['antispam'].uids.get(irc.name) for user in args['users']: try: ip = irc.users[user].ip ipa = ipaddress.ip_address(ip) except (KeyError, ValueError): log.error("(%s) badchans: could not obtain IP of user %s", irc.name, user) continue nuh = irc.get_hostmask(user) exempt_hosts = set(conf.conf.get('badchans', {}).get('exempt_hosts', [])) | \ set(irc.serverdata.get('badchans_exempt_hosts', [])) if not ipa.is_global: irc.msg(user, "Warning: %s kills unopered users, but non-public addresses are exempt." % channel, notice=True, source=asm_uid or irc.pseudoclient.uid) continue if exempt_hosts: skip = False for glob in exempt_hosts: if irc.match_host(glob, user): log.info("(%s) badchans: ignoring exempt user %s on %s (%s)", irc.name, nuh, channel, ip) irc.msg(user, "Warning: %s kills unopered users, but your host is exempt." % channel, notice=True, source=asm_uid or irc.pseudoclient.uid) skip = True break if skip: continue if irc.is_oper(user): irc.msg(user, "Warning: %s kills unopered users!" % channel, notice=True, source=asm_uid or irc.pseudoclient.uid) else: log.info('(%s) badchans: punishing user %s (server: %s) for joining channel %s', irc.name, nuh, irc.get_friendly_name(irc.get_server(user)), channel) if use_kline: irc.set_server_ban(asm_uid or irc.sid, kline_duration, host=ip, reason=REASON) else: irc.kill(asm_uid or irc.sid, user, REASON) if ip not in seen_ips: dronebl_key = irc.get_service_option('badchans', 'dronebl_key') if dronebl_key: log.info('(%s) badchans: submitting IP %s (%s) to DroneBL', irc.name, ip, nuh) pool.submit(_submit_dronebl, irc, ip, dronebl_key, nuh) dnsblim_key = irc.get_service_option('badchans', 'dnsblim_key') if dnsblim_key: log.info('(%s) badchans: submitting IP %s (%s) to DNSBL.im', irc.name, ip, nuh) pool.submit(_submit_dnsblim, irc, ip, dnsblim_key, nuh) seen_ips[ip] = time.time() else: log.debug('(%s) badchans: ignoring already submitted IP %s', irc.name, ip)