Beispiel #1
0
 def join(self, client, channel):
     """Joins a PyLink client to a channel."""
     # InspIRCd doesn't distinguish between burst joins and regular joins,
     # so what we're actually doing here is sending FJOIN from the server,
     # on behalf of the clients that are joining.
     channel = self.irc.toLower(channel)
     server = self.irc.isInternalClient(client)
     if not server:
         log.error(
             '(%s) Error trying to join %r to %r (no such client exists)',
             self.irc.name, client, channel)
         raise LookupError('No such PyLink client exists.')
     # Strip out list-modes, they shouldn't be ever sent in FJOIN.
     modes = [
         m for m in self.irc.channels[channel].modes
         if m[0] not in self.irc.cmodes['*A']
     ]
     self._send(
         server, "FJOIN {channel} {ts} {modes} :,{uid}".format(
             ts=self.irc.channels[channel].ts,
             uid=client,
             channel=channel,
             modes=self.irc.joinModes(modes)))
     self.irc.channels[channel].users.add(client)
     self.irc.users[client].channels.add(channel)
Beispiel #2
0
    def message(self, source, target, text, notice=False):
        """Sends messages to the target."""
        if target in self.virtual_parent.client.state.users:
            try:
                discord_target = self.bot_plugin._dm_channels[target]
                log.debug('(%s) Found DM channel for %s: %s', self.name, target, discord_target)
            except KeyError:
                u = self.virtual_parent.client.state.users[target]
                discord_target = self.bot_plugin._dm_channels[target] = u.open_dm()
                log.debug('(%s) Creating new DM channel for %s: %s', self.name, target, discord_target)

        elif target in self.channels:
            discord_target = self.channels[target].discord_channel
        else:
            log.error('(%s) Could not find message target for %s', self.name, target)
            return

        if text.startswith('\x01ACTION '):  # Mangle IRC CTCP actions
            # TODO: possibly allow toggling between IRC style actions (* nick abcd) and Discord style (italicize the text)
            text = '\x1d%s' % text[8:-1]
        elif text.startswith('\x01'):
            return  # Drop other CTCPs

        sourceobj = None
        if self.pseudoclient and self.pseudoclient.uid != source:
            sourceobj = self.users.get(source)

        message_data = QueuedMessage(discord_target, target, text, sender=sourceobj, is_notice=notice)
        self.virtual_parent.message_queue.put_nowait(message_data)
Beispiel #3
0
 def on_member_add(self, event: events.GuildMemberAdd, *args, **kwargs):
     log.info('(%s) got GuildMemberAdd event for guild %s/%s: %s', self.protocol.name, event.guild.id, event.guild.name, event.member)
     try:
         pylink_netobj = self.protocol._children[event.guild.id]
     except KeyError:
         log.error("(%s) Could not burst user %s as the parent network object does not exist", self.protocol.name, event.member)
         return
     self._burst_new_client(event.guild, event.member, pylink_netobj)
Beispiel #4
0
 def on_server_update(self, event: events.GuildUpdate, *args, **kwargs):
     log.info('(%s) got GuildUpdate event for guild %s/%s', self.protocol.name, event.guild.id, event.guild.name)
     try:
         pylink_netobj = self.protocol._children[event.guild.id]
     except KeyError:
         log.error("(%s) Could not update guild %s/%s as the corresponding network object does not exist", self.protocol.name, event.guild.id, event.guild.name)
         return
     else:
         pylink_netobj._guild_name = event.guild.name
Beispiel #5
0
    def on_member_chunk(self, event: events.GuildMembersChunk, *args, **kwargs):
        log.debug('(%s) got GuildMembersChunk event for guild %s/%s: %s', self.protocol.name, event.guild.id, event.guild.name, event.members)
        try:
            pylink_netobj = self.protocol._children[event.guild.id]
        except KeyError:
            log.error("(%s) Could not burst users %s as the parent network object does not exist", self.protocol.name, event.members)
            return

        for member in event.members:
            self._burst_new_client(event.guild, member, pylink_netobj)
Beispiel #6
0
 def join(self, client, channel):
     """Joins a PyLink client to a channel."""
     # JOIN:
     # parameters: channelTS, channel, '+' (a plus sign)
     if not self.is_internal_client(client):
         log.error('(%s) Error trying to join %r to %r (no such client exists)', self.name, client, channel)
         raise LookupError('No such PyLink client exists.')
     self._send_with_prefix(client, "JOIN {ts} {channel} +".format(ts=self._channels[channel].ts, channel=channel))
     self._channels[channel].users.add(client)
     self.users[client].channels.add(channel)
Beispiel #7
0
def handle_kill(irc, numeric, command, args):
    """
    Tracks kills against PyLink clients. If too many are received,
    automatically disconnects from the network.
    """
    if killcache.setdefault(irc.name, 1) >= 5:
        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
Beispiel #8
0
def handle_save(irc, numeric, command, args):
    """
    Tracks SAVEs (nick collision) against PyLink clients. If too many are received,
    automatically disconnects from the network.
    """
    if savecache.setdefault(irc.name, 0) >= 5:
        log.error('(%s) servprotect: Too many nick collisions, aborting!', irc.name)
        irc.disconnect()

    log.debug('(%s) servprotect: Incrementing savecache by 1', irc.name)
    savecache[irc.name] += 1
Beispiel #9
0
 def part(self, client, channel, reason=None):
     """Sends a part from a PyLink client."""
     if not self.is_internal_client(client):
         log.error(
             '(%s) Error trying to part %r from %r (no such client exists)',
             self.name, client, channel)
         raise LookupError('No such PyLink client exists.')
     msg = "PART %s" % channel
     if reason:
         msg += " :%s" % reason
     self._send_with_prefix(client, msg)
     self.handle_part(client, 'PART', [channel])
Beispiel #10
0
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
Beispiel #11
0
    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)
Beispiel #12
0
def setnextid(irc, source, args):
    """<channel> <ID>
    Sets the next quote ID in <channel> to <ID>.
    
    
    This command should only be used if quotes were deleted after a certain number, as overwriting quotes is not supported."""
    permissions.checkPermissions(irc, source, ["quotes.admin"])
    options = sni_parser.parse_args(args)
    if not options.int:
        log.error("Did not receive a ID to set.")
    else:
        ins = engine.execute(
            dbcd.update().where(dbcd.c.channel == options.channel).values(
                next_id=options.int)).rowcount
        if ins > 0:
            reply(
                irc, "Set {}'s next id to {}".format(options.channel,
                                                     options.int))
        else:
            error(irc, "An error occured.")
Beispiel #13
0
def qadd(irc, source, args):
    """<quote text>
    Adds a quote to the bots database."""
    options = qadd_parser.parse_args(args)
    ourquote = " ".join(options.quote)
    if irc.called_in == source:
        if irc.checkAuthenticated(source, allowAuthed=True):
            reply("Please see 'addquote' for arbitrary adding.",
                  notice=True,
                  private=True)
        else:
            error(
                irc,
                "quotes must be sent in using a channel, not a 1to1 message.")
    s = select([dbcd.c.next_id]).where(dbcd.c.channel == irc.called_in)
    try:
        result = engine.execute(s).fetchone()[0]
    except exc.OperationalError as e:
        log.error("OperationalError Occured:")
        log.error("Exception Details: %s" % e)
        error(irc, "Stale Database Connection, Please try again.")
        error(
            irc,
            "If you've already tried once or twice, please forward this error to an admin, who may, or may not already know."
        )
        return

    next_id = result
    channel = irc.called_in
    ins = dbq.insert().values(id=next_id,
                              channel=irc.called_in,
                              quote=ourquote,
                              added_by=irc.getHostmask(source))
    engine.execute(ins)
    reply(irc, "Done. Quote #%s added." % next_id)
    new_nextid = int(next_id) + 1
    updated = engine.execute(dbcd.update().where(
        dbcd.c.channel == channel).values(next_id=new_nextid))
Beispiel #14
0
    def message(self, source, target, text, notice=False):
        """Sends messages to the target."""
        if target in self.users:
            discord_target = self.users[target].discord_user.user.open_dm()
        elif target in self.channels:
            discord_target = self.channels[target].discord_channel
        else:
            log.error('(%s) Could not find message target for %s', self.name,
                      target)
            return

        message_data = {'target': discord_target, 'sender': source}
        if self.pseudoclient and self.pseudoclient.uid == source:
            message_data['text'] = I2DFormatter().format(text)
            self.virtual_parent.message_queue.put_nowait(message_data)
            return

        self.call_hooks([
            source, 'CLIENTBOT_MESSAGE', {
                'target': target,
                'is_notice': notice,
                'text': text
            }
        ])
Beispiel #15
0
    def on_member_update(self, event: GuildMemberUpdate, *args, **kwargs):
        log.info('(%s) got GuildMemberUpdate event for guild %s/%s: %s',
                 self.protocol.name, event.guild.id, event.guild.name,
                 event.member)
        try:
            pylink_netobj = self.protocol._children[event.guild.id]
        except KeyError:
            log.error(
                "(%s) Could not update user %s as the parent network object does not exist",
                self.protocol.name, event.member)
            return

        uid = event.member.id
        pylink_user = pylink_netobj.users.get(uid)
        if not pylink_user:
            self._burst_new_client(event.guild, event.member, pylink_netobj)
            return

        # Handle NICK changes
        oldnick = pylink_user.nick
        if pylink_user.nick != event.member.name:
            pylink_user.nick = event.member.name
            pylink_netobj.call_hooks([
                uid, 'NICK', {
                    'newnick': event.member.name,
                    'oldnick': oldnick
                }
            ])

        # Relay permission changes as modes
        for channel in event.guild.channels.values():
            if channel.type == ChannelType.GUILD_TEXT:
                self._update_channel_presence(event.guild,
                                              channel,
                                              event.member,
                                              relay_modes=True)
Beispiel #16
0
        def _send(sender, channel, pylink_target, message_parts):
            """
            Wrapper to send a joined message.
            """
            text = '\n'.join(message_parts)

            # Handle the case when the sender is not the PyLink client (sender != None)
            # For channels, use either virtual webhook users or CLIENTBOT_MESSAGE forwarding (relay_clientbot).
            if sender:
                user_fields = self._get_webhook_fields(sender)

                if channel.guild:  # This message belongs to a channel
                    netobj = self._children[channel.guild.id]

                    # Note: skip webhook sending for messages that contain only spaces, as that fails with
                    # 50006 "Cannot send an empty message" errors
                    if netobj.serverdata.get('use_webhooks') and text.strip():
                        user_format = netobj.serverdata.get('webhook_user_format', "$nick @ $netname")
                        tmpl = string.Template(user_format)
                        webhook_fake_username = tmpl.safe_substitute(self._get_webhook_fields(sender))

                        try:
                            webhook = self._get_webhook(channel)
                            webhook.execute(content=text[:self.MAX_MESSAGE_SIZE], username=webhook_fake_username, avatar_url=user_fields['avatar'])
                        except APIException as e:
                            if e.code == 10015 and channel.id in self.webhooks:
                                log.info("(%s) Invalidating webhook %s for channel %s due to Unknown Webhook error (10015)",
                                         self.name, self.webhooks[channel.id], channel)
                                del self.webhooks[channel.id]
                            elif e.code == 50013:
                                # Prevent spamming errors: disable webhooks we don't have the right permissions
                                log.warning("(%s) Disabling webhooks on guild %s/%s due to insufficient permissions (50013). Rehash to re-enable.",
                                            self.name, channel.guild.id, channel.guild.name)
                                self.serverdata.update(
                                    {'guilds':
                                        {channel.guild.id:
                                            {'use_webhooks': False}
                                        }
                                    })
                            else:
                                log.error("(%s) Caught API exception when sending webhook message to channel %s: %s/%s", self.name, channel, e.response.status_code, e.code)
                            log.debug("(%s) APIException full traceback:", self.name, exc_info=True)

                        except:
                            log.exception("(%s) Failed to send webhook message to channel %s", self.name, channel)
                        else:
                            return

                    for line in message_parts:
                        netobj.call_hooks([sender.uid, 'CLIENTBOT_MESSAGE', {'target': pylink_target, 'text': line}])
                    return
                else:
                    # This is a forwarded PM - prefix the message with its sender info.
                    pm_format = self.serverdata.get('pm_format', "Message from $nick @ $netname: $text")
                    user_fields['text'] = text
                    text = string.Template(pm_format).safe_substitute(user_fields)

            try:
                channel.send_message(text[:self.MAX_MESSAGE_SIZE])
            except Exception as e:
                log.exception("(%s) Could not send message to channel %s (pylink_target=%s)", self.name, channel, pylink_target)
Beispiel #17
0
def _main():
    conf.load_conf(args.config)

    from pylinkirc.log import log
    from pylinkirc import classes, utils, coremods, selectdriver

    # Write and check for an existing PID file unless specifically told not to.
    if not args.no_pid:
        pidfile = '%s.pid' % conf.confname
        pid_exists = False
        pid = None
        if os.path.exists(pidfile):
            try:
                with open(pidfile) as f:
                    pid = int(f.read())
            except OSError:
                log.exception("Could not read PID file %s:", pidfile)
            else:
                pid_exists = True

            if psutil is not None and os.name == 'posix':
                # FIXME: Haven't tested this on other platforms, so not turning it on by default.
                try:
                    proc = psutil.Process(pid)
                except psutil.NoSuchProcess:  # Process doesn't exist!
                    pid_exists = False
                    log.info(
                        "Ignoring stale PID %s from PID file %r: no such process exists.",
                        pid, pidfile)
                else:
                    # This PID got reused for something that isn't us?
                    if not any('pylink' in arg.lower()
                               for arg in proc.cmdline()):
                        log.info(
                            "Ignoring stale PID %s from PID file %r: process command line %r is not us",
                            pid, pidfile, proc.cmdline())
                        pid_exists = False

        if pid and pid_exists:
            if args.rehash:
                os.kill(pid, signal.SIGUSR1)
                log.info('OK, rehashed PyLink instance %s (config %r)', pid,
                         args.config)
                sys.exit()
            elif args.stop or args.restart:  # Handle --stop and --restart options
                os.kill(pid, signal.SIGTERM)

                log.info(
                    "Waiting for PyLink instance %s (config %r) to stop...",
                    pid, args.config)
                while os.path.exists(pidfile):
                    # XXX: this is ugly, but os.waitpid() only works on non-child processes on Windows
                    time.sleep(0.2)
                log.info("Successfully killed PID %s for config %r.", pid,
                         args.config)

                if args.stop:
                    sys.exit()
            else:
                log.error("PID file %r exists; aborting!", pidfile)

                if psutil is None:
                    log.error(
                        "If PyLink didn't shut down cleanly last time it ran, or you're upgrading "
                        "from PyLink < 1.1-dev, delete %r and start the server again.",
                        pidfile)
                    if os.name == 'posix':
                        log.error(
                            "Alternatively, you can install psutil for Python 3 (pip3 install psutil), "
                            "which will allow this launcher to detect stale PID files and ignore them."
                        )
                sys.exit(1)

        elif args.stop or args.restart or args.rehash:  # XXX: also repetitive
            # --stop and --restart should take care of stale PIDs.
            if pid:
                world._should_remove_pid = True
                log.error(
                    'Cannot stop/rehash PyLink: no process with PID %s exists.',
                    pid)
            else:
                log.error(
                    'Cannot stop/rehash PyLink: PID file %r does not exist or cannot be read.',
                    pidfile)
            sys.exit(1)

        world._should_remove_pid = True

    log.info('PyLink %s starting...', __version__)

    world.daemon = args.daemonize
    if args.daemonize:
        if args.no_pid:
            print(
                'ERROR: Combining --no-pid and --daemonize is not supported.')
            sys.exit(1)
        elif os.name != 'posix':
            print(
                'ERROR: Daemonization is not supported outside POSIX systems.')
            sys.exit(1)
        else:
            log.info('Forking into the background.')
            log.removeHandler(world.console_handler)

            # Adapted from https://stackoverflow.com/questions/5975124/
            if os.fork():
                # Fork and exit the parent process.
                os._exit(0)

            os.setsid()  # Decouple from our lovely terminal
            if os.fork():
                # Fork again to prevent starting zombie apocalypses.
                os._exit(0)
    else:
        # For foreground sessions, set the terminal window title.
        # See https://bbs.archlinux.org/viewtopic.php?id=85567 &
        #     https://stackoverflow.com/questions/7387276/
        if os.name == 'nt':
            import ctypes
            ctypes.windll.kernel32.SetConsoleTitleW("PyLink %s" % __version__)
        elif os.name == 'posix':
            sys.stdout.write("\x1b]2;PyLink %s\x07" % __version__)

    if not args.no_pid:
        # Write the PID file only after forking.
        with open(pidfile, 'w') as f:
            f.write(str(os.getpid()))

    # Load configured plugins
    to_load = conf.conf['plugins']
    utils._reset_module_dirs()

    for plugin in to_load:
        try:
            world.plugins[plugin] = pl = utils._load_plugin(plugin)
        except Exception as e:
            log.exception('Failed to load plugin %r: %s: %s', plugin,
                          type(e).__name__, str(e))
        else:
            if hasattr(pl, 'main'):
                log.debug('Calling main() function of plugin %r', pl)
                pl.main()

    # Initialize all the networks one by one
    for network, sdata in conf.conf['servers'].items():
        try:
            protoname = sdata['protocol']
        except (KeyError, TypeError):
            log.error(
                "(%s) Configuration error: No protocol module specified, aborting.",
                network)
        else:
            # Fetch the correct protocol module.
            try:
                proto = utils._get_protocol_module(protoname)

                # Create and connect the network.
                world.networkobjects[network] = irc = proto.Class(network)
                log.debug('Connecting to network %r', network)
                irc.connect()
            except:
                log.exception(
                    '(%s) Failed to connect to network %r, skipping it...',
                    network, network)
                continue

    world.started.set()
    log.info("Loaded plugins: %s", ', '.join(sorted(world.plugins.keys())))
    selectdriver.start()
Beispiel #18
0
def _punish(irc, target, channel, punishment, reason):
    """Punishes the target user. This function returns True if the user was successfully punished."""
    if target not in irc.users:
        log.warning("(%s) antispam: got target %r that isn't a user?", irc.name, target)
        return False
    elif irc.is_oper(target):
        log.debug("(%s) antispam: refusing to punish oper %s/%s", irc.name, target, irc.get_friendly_name(target))
        return False

    target_nick = irc.get_friendly_name(target)

    if channel:
        c = irc.channels[channel]
        exempt_level = irc.get_service_option('antispam', 'exempt_level', DEFAULT_EXEMPT_OPTION).lower()

        if exempt_level not in EXEMPT_OPTIONS:
            log.error('(%s) Antispam exempt %r is not a valid setting, '
                      'falling back to defaults; accepted settings include: %s',
                      irc.name, exempt_level, ', '.join(EXEMPT_OPTIONS))
            exempt_level = DEFAULT_EXEMPT_OPTION

        if exempt_level == 'voice' and c.is_voice_plus(target):
            log.debug("(%s) antispam: refusing to punish voiced and above %s/%s", irc.name, target, target_nick)
            return False
        elif exempt_level == 'halfop' and c.is_halfop_plus(target):
            log.debug("(%s) antispam: refusing to punish halfop and above %s/%s", irc.name, target, target_nick)
            return False
        elif exempt_level == 'op' and c.is_op_plus(target):
            log.debug("(%s) antispam: refusing to punish op and above %s/%s", irc.name, target, target_nick)
            return False

    my_uid = sbot.uids.get(irc.name)
    # XXX workaround for single-bot protocols like Clientbot
    if irc.pseudoclient and not irc.has_cap('can-spawn-clients'):
        my_uid = irc.pseudoclient.uid

    bans = set()
    log.debug('(%s) antispam: got %r as punishment for %s/%s', irc.name, punishment,
              target, irc.get_friendly_name(target))

    def _ban():
        bans.add(irc.make_channel_ban(target))
    def _quiet():
        bans.add(irc.make_channel_ban(target, ban_type='quiet'))
    def _kick():
        irc.kick(my_uid, channel, target, reason)
        irc.call_hooks([my_uid, 'ANTISPAM_KICK', {'channel': channel, 'text': reason, 'target': target,
                                                  'parse_as': 'KICK'}])
    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'}])

    kill = False
    successful_punishments = 0
    for action in set(punishment.split('+')):
        if action not in PUNISH_OPTIONS:
            log.error('(%s) Antispam punishment %r is not a valid setting; '
                      'accepted settings include: %s OR any combination of '
                      'these joined together with a "+".',
                      irc.name, punishment, ', '.join(PUNISH_OPTIONS))
            return
        elif action == 'block':
            # We only need to increment this for this function to return True
            successful_punishments += 1
        elif action == 'kill':
            kill = True  # Delay kills so that the user data doesn't disappear.
        # XXX factorize these blocks
        elif action == 'kick' and channel:
            try:
                _kick()
            except NotImplementedError:
                log.warning("(%s) antispam: Kicks are not supported on this network, skipping; "
                            "target was %s/%s", irc.name, target_nick, channel)
            else:
                successful_punishments += 1
        elif action == 'ban' and channel:
            try:
                _ban()
            except (ValueError, NotImplementedError):
                log.warning("(%s) antispam: Bans are not supported on this network, skipping; "
                            "target was %s/%s", irc.name, target_nick, channel)
            else:
                successful_punishments += 1
        elif action == 'quiet' and channel:
            try:
                _quiet()
            except (ValueError, NotImplementedError):
                log.warning("(%s) antispam: Quiet is not supported on this network, skipping; "
                            "target was %s/%s", irc.name, target_nick, channel)
            else:
                successful_punishments += 1

    if bans:  # Set all bans at once to prevent spam
        irc.mode(my_uid, channel, bans)
        irc.call_hooks([my_uid, 'ANTISPAM_BAN',
                        {'target': channel, 'modes': bans, 'parse_as': 'MODE'}])
    if kill:
        try:
            _kill()
        except NotImplementedError:
            log.warning("(%s) antispam: Kills are not supported on this network, skipping; "
                        "target was %s/%s", irc.name, target_nick, channel)
        else:
            successful_punishments += 1

    if not successful_punishments:
        log.warning('(%s) antispam: Failed to punish %s with %r, target was %s', irc.name,
                    target_nick, punishment, channel or 'a PM')

    return bool(successful_punishments)
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)
Beispiel #20
0
    def _update_channel_presence(self,
                                 guild,
                                 channel,
                                 member=None,
                                 *,
                                 relay_modes=False):
        """
        Updates channel presence & IRC modes for the given member, or all guild members if not given.
        """
        if channel.type == ChannelType.GUILD_CATEGORY:
            # XXX: there doesn't seem to be an easier way to get this. Fortunately, there usually
            # aren't too many channels in one guild...
            for subchannel in guild.channels.values():
                if subchannel.parent_id == channel.id:
                    log.debug(
                        '(%s) _update_channel_presence: checking channel %s/%s in category %s/%s',
                        self.protocol.name, subchannel.id, subchannel,
                        channel.id, channel)
                    self._update_channel_presence(guild,
                                                  subchannel,
                                                  member=member,
                                                  relay_modes=relay_modes)
            return
        elif channel.type != ChannelType.GUILD_TEXT:
            log.debug(
                '(%s) _update_channel_presence: ignoring non-text channel %s/%s',
                self.protocol.name, channel.id, channel)
            return

        modes = []
        users_joined = []

        try:
            pylink_netobj = self.protocol._children[guild.id]
        except KeyError:
            log.error(
                "(%s) Could not update channel %s(%s)/%s as the parent network object does not exist",
                self.protocol.name, guild.id, guild.name, str(channel))
            return

        # Create a new channel if not present
        try:
            pylink_channel = pylink_netobj.channels[channel.id]
            pylink_channel.name = str(channel)
        except KeyError:
            pylink_channel = pylink_netobj.channels[channel.id] = Channel(
                self, name=str(channel))

        pylink_channel.discord_id = channel.id
        pylink_channel.discord_channel = channel

        if member is None:
            members = guild.members.values()
        else:
            members = [member]

        for member in members:
            uid = member.id
            try:
                pylink_user = pylink_netobj.users[uid]
            except KeyError:
                log.error(
                    "(%s) Could not update user %s(%s)/%s as the user object does not exist",
                    self.protocol.name, guild.id, guild.name, uid)
                continue

            channel_permissions = channel.get_permissions(member)
            has_perm = channel_permissions.can(Permissions.read_messages)
            log.debug(
                'discord: checking if member %s/%s has permission read_messages on %s/%s: %s',
                member.id, member, channel.id, channel, has_perm)
            #log.debug('discord: channel permissions are %s', str(channel_permissions.to_dict()))
            if has_perm:
                if uid not in pylink_channel.users:
                    log.debug('discord: joining member %s to %s/%s', member,
                              channel.id, channel)
                    pylink_user.channels.add(channel.id)
                    pylink_channel.users.add(uid)
                    users_joined.append(uid)

                for irc_mode, discord_permission in self.irc_discord_perm_mapping.items(
                ):
                    prefixlist = pylink_channel.prefixmodes[
                        irc_mode]  # channel.prefixmodes['op'] etc.
                    # If the user now has the permission but not the associated mode, add it to the mode list
                    has_op_perm = channel_permissions.can(discord_permission)
                    if has_op_perm:
                        modes.append(
                            ('+%s' % pylink_netobj.cmodes[irc_mode], uid))
                        if irc_mode == 'op':
                            # Stop adding lesser modes once we find an op; this reflects IRC services
                            # which tend to set +ao, +o, ... instead of +ohv, +aohv
                            break
                    elif (not has_op_perm) and uid in prefixlist:
                        modes.append(
                            ('-%s' % pylink_netobj.cmodes[irc_mode], uid))

            elif uid in pylink_channel.users and not has_perm:
                log.debug('discord: parting member %s from %s/%s', member,
                          channel.id, channel)
                pylink_user.channels.discard(channel.id)
                pylink_channel.remove_user(uid)

                # We send KICK from a server to prevent triggering antiflood mechanisms...
                pylink_netobj.call_hooks([
                    guild.id, 'KICK', {
                        'channel': channel.id,
                        'target': uid,
                        'text': "User removed from channel"
                    }
                ])

            # Optionally, burst the server owner as IRC owner
            if self.protocol.serverdata.get('show_owner_status',
                                            True) and uid == guild.owner_id:
                modes.append(('+q', uid))

        if modes:
            pylink_netobj.apply_modes(channel.id, modes)
            log.debug('(%s) Relaying permission changes on %s/%s as modes: %s',
                      self.protocol.name, member.name, channel,
                      pylink_netobj.join_modes(modes))
            if relay_modes:
                pylink_netobj.call_hooks(
                    [guild.id, 'MODE', {
                        'target': channel.id,
                        'modes': modes
                    }])

        if users_joined:
            pylink_netobj.call_hooks([
                guild.id, 'JOIN', {
                    'channel': channel.id,
                    'users': users_joined,
                    'modes': []
                }
            ])
Beispiel #21
0
def main():
    import argparse

    parser = argparse.ArgumentParser(description='Starts an instance of PyLink IRC Services.')
    parser.add_argument('config', help='specifies the path to the config file (defaults to pylink.yml)', nargs='?', default='pylink.yml')
    parser.add_argument("-v", "--version", help="displays the program version and exits", action='store_true')
    parser.add_argument("-c", "--check-pid", help="no-op; kept for compatibility with PyLink <= 1.2.x", action='store_true')
    parser.add_argument("-n", "--no-pid", help="skips generating and checking PID files", action='store_true')
    parser.add_argument("-r", "--restart", help="restarts the PyLink instance with the given config file", action='store_true')
    parser.add_argument("-s", "--stop", help="stops the PyLink instance with the given config file", action='store_true')
    parser.add_argument("-R", "--rehash", help="rehashes the PyLink instance with the given config file", action='store_true')
    parser.add_argument("-d", "--daemonize", help="[experimental] daemonizes the PyLink instance on POSIX systems", action='store_true')
    args = parser.parse_args()

    if args.version:  # Display version and exit
        print('PyLink %s (in VCS: %s)' % (__version__, real_version))
        sys.exit()

    # XXX: repetitive
    elif args.no_pid and (args.restart or args.stop or args.rehash):
        print('ERROR: --no-pid cannot be combined with --restart or --stop')
        sys.exit(1)
    elif args.rehash and os.name != 'posix':
        print('ERROR: Rehashing via the command line is not supported outside Unix.')
        sys.exit(1)

    # FIXME: we can't pass logging on to conf until we set up the config...
    conf.loadConf(args.config)

    from pylinkirc.log import log
    from pylinkirc import classes, utils, coremods

    # Write and check for an existing PID file unless specifically told not to.
    if not args.no_pid:
        pidfile = '%s.pid' % conf.confname
        has_pid = False
        pid = None
        if os.path.exists(pidfile):

            has_pid = True
            if psutil is not None and os.name == 'posix':
                # FIXME: Haven't tested this on other platforms, so not turning it on by default.
                with open(pidfile) as f:
                    try:
                        pid = int(f.read())
                        proc = psutil.Process(pid)
                    except psutil.NoSuchProcess:  # Process doesn't exist!
                        has_pid = False
                        log.info("Ignoring stale PID %s from PID file %r: no such process exists.", pid, pidfile)
                    else:
                        # This PID got reused for something that isn't us?
                        if not any('pylink' in arg.lower() for arg in proc.cmdline()):
                            log.info("Ignoring stale PID %s from PID file %r: process command line %r is not us", pid, pidfile, proc.cmdline())
                            has_pid = False

        if has_pid:
            if args.rehash:
                os.kill(pid, signal.SIGUSR1)
                log.info('OK, rehashed PyLink instance %s (config %r)', pid, args.config)
                sys.exit()
            elif args.stop or args.restart:  # Handle --stop and --restart options
                os.kill(pid, signal.SIGTERM)

                log.info("Waiting for PyLink instance %s (config %r) to stop...", pid, args.config)
                while os.path.exists(pidfile):
                    # XXX: this is ugly, but os.waitpid() only works on non-child processes on Windows
                    time.sleep(0.2)
                log.info("Successfully killed PID %s for config %r.", pid, args.config)

                if args.stop:
                    sys.exit()
            else:
                log.error("PID file %r exists; aborting!", pidfile)

                if psutil is None:
                    log.error("If PyLink didn't shut down cleanly last time it ran, or you're upgrading "
                              "from PyLink < 1.1-dev, delete %r and start the server again.", pidfile)
                    if os.name == 'posix':
                        log.error("Alternatively, you can install psutil for Python 3 (pip3 install psutil), "
                                  "which will allow this launcher to detect stale PID files and ignore them.")
                sys.exit(1)

        elif args.stop or args.restart or args.rehash:  # XXX: also repetitive
            # --stop and --restart should take care of stale PIDs.
            if pid:
                world._should_remove_pid = True
                log.error('Cannot stop/rehash PyLink: no process with PID %s exists.', pid)
            else:
                log.error('Cannot stop/rehash PyLink: PID file %r does not exist.', pidfile)
            sys.exit(1)

        world._should_remove_pid = True

    log.info('PyLink %s starting...', __version__)

    world.daemon = args.daemonize
    if args.daemonize:
        if args.no_pid:
            print('ERROR: Combining --no-pid and --daemonize is not supported.')
            sys.exit(1)
        elif os.name != 'posix':
            print('ERROR: Daemonization is not supported outside POSIX systems.')
            sys.exit(1)
        else:
            log.info('Forking into the background.')
            log.removeHandler(world.console_handler)

            # Adapted from https://stackoverflow.com/questions/5975124/
            if os.fork():
                # Fork and exit the parent process.
                os._exit(0)

            os.setsid()  # Decouple from our lovely terminal
            if os.fork():
                # Fork again to prevent starting zombie apocalypses.
                os._exit(0)
    else:
        # For foreground sessions, set the terminal window title.
        # See https://bbs.archlinux.org/viewtopic.php?id=85567 &
        #     https://stackoverflow.com/questions/7387276/
        if os.name == 'nt':
            import ctypes
            ctypes.windll.kernel32.SetConsoleTitleW("PyLink %s" % __version__)
        elif os.name == 'posix':
            sys.stdout.write("\x1b]2;PyLink %s\x07" % __version__)

    # Write the PID file only after forking.
    with open(pidfile, 'w') as f:
        f.write(str(os.getpid()))

    # Load configured plugins
    to_load = conf.conf['plugins']
    utils.resetModuleDirs()

    for plugin in to_load:
        try:
            world.plugins[plugin] = pl = utils.loadPlugin(plugin)
        except Exception as e:
            log.exception('Failed to load plugin %r: %s: %s', plugin, type(e).__name__, str(e))
        else:
            if hasattr(pl, 'main'):
                log.debug('Calling main() function of plugin %r', pl)
                pl.main()

    # Initialize all the networks one by one
    for network, sdata in conf.conf['servers'].items():
        try:
            protoname = sdata['protocol']
        except (KeyError, TypeError):
            log.error("(%s) Configuration error: No protocol module specified, aborting.", network)
        else:
            # Fetch the correct protocol module.
            try:
                proto = utils.getProtocolModule(protoname)

                # Create and connect the network.
                log.debug('Connecting to network %r', network)
                world.networkobjects[network] = classes.Irc(network, proto, conf.conf)
            except:
                log.exception('(%s) Failed to connect to network %r, skipping it...',
                              network, network)
                continue

    world.started.set()
    log.info("Loaded plugins: %s", ', '.join(sorted(world.plugins.keys())))

    from pylinkirc import coremods
    coremods.permissions.resetPermissions()  # Future note: this is moved to run on import in 2.0
Beispiel #22
0
    def _update_channel_presence(self, guild, channel, member=None, *, relay_modes=False):
        """
        Updates channel presence & IRC modes for the given member, or all guild members if not given.
        """
        if channel.type == ChannelType.GUILD_CATEGORY:
            # XXX: there doesn't seem to be an easier way to get this. Fortunately, there usually
            # aren't too many channels in one guild...
            for subchannel in guild.channels.values():
                if subchannel.parent_id == channel.id:
                    log.debug('(%s) _update_channel_presence: checking channel %s/%s in category %s/%s', self.protocol.name, subchannel.id, subchannel, channel.id, channel)
                    self._update_channel_presence(guild, subchannel, member=member, relay_modes=relay_modes)
            return
        elif channel.type != ChannelType.GUILD_TEXT:
            log.debug('(%s) _update_channel_presence: ignoring non-text channel %s/%s', self.protocol.name, channel.id, channel)
            return

        modes = []
        users_joined = []

        try:
            pylink_netobj = self.protocol._children[guild.id]
        except KeyError:
            log.error("(%s) Could not update channel %s(%s)/%s as the parent network object does not exist", self.protocol.name, guild.id, guild.name, str(channel))
            return

        # Create a new channel if not present
        try:
            pylink_channel = pylink_netobj.channels[channel.id]
            pylink_channel.name = str(channel)
            log.debug("(%s) Retrieved channel %s for channel ID %s", self.name, pylink_channel, channel.id)
        except KeyError:
            pylink_channel = pylink_netobj.channels[channel.id] = Channel(self, name=str(channel))
            log.debug("(%s) Created new channel %s for channel ID %s", self.name, pylink_channel, channel.id)

        pylink_channel.discord_id = channel.id
        pylink_channel.discord_channel = channel

        if member is None:
            members = guild.members.values()
        else:
            members = [member]

        for member in members:
            uid = member.id
            try:
                pylink_user = pylink_netobj.users[uid]
            except KeyError:
                log.error("(%s) Could not update user %s(%s)/%s as the user object does not exist", self.protocol.name, guild.id, guild.name, uid)
                return

            channel_permissions = channel.get_permissions(member)
            has_perm = channel_permissions.can(Permissions.read_messages)
            log.debug('discord: checking if member %s/%s has permission read_messages on %s/%s: %s',
                      member.id, member, channel.id, channel, has_perm)
            #log.debug('discord: channel permissions are %s', str(channel_permissions.to_dict()))
            if has_perm:
                if uid not in pylink_channel.users:
                    log.debug('discord: adding member %s to %s/%s', member, channel.id, channel)
                    pylink_user.channels.add(channel.id)
                    pylink_channel.users.add(uid)

                    # Hide offline users if join_offline_users is enabled
                    if pylink_netobj.join_offline_users or (member.user.presence and \
                            member.user.presence.status not in (DiscordStatus.OFFLINE, DiscordStatus.INVISIBLE)):
                        users_joined.append(uid)

                # Map Discord role IDs to IRC modes
                # e.g. 1234567890: 'op'
                #      2345678901: 'voice'
                role_map = pylink_netobj.serverdata.get('role_mode_map') or {}
                # Track all modes the user is allowed to have, since multiple roles may map to one mode.
                entitled_modes = {irc_mode for role_id, irc_mode in role_map.items() if role_id in member.roles}

                # Optionally burst guild owner as IRC owner (+q)
                if uid == guild.owner_id and pylink_netobj.serverdata.get('show_owner_status', True):
                    entitled_modes.add('owner')
                # Grant +qo and +ao instead of only +q and +a
                if 'owner' in entitled_modes or 'admin' in entitled_modes:
                    entitled_modes.add('op')

                for mode, prefixlist in pylink_channel.prefixmodes.items():
                    modechar = pylink_netobj.cmodes.get(mode)
                    if not modechar:
                        continue

                    # New role added
                    if mode in entitled_modes and uid not in prefixlist:
                        modes.append(('+%s' % modechar, uid))
                    # Matching role removed
                    if mode not in entitled_modes and uid in prefixlist:
                        modes.append(('-%s' % modechar, uid))

            elif uid in pylink_channel.users and not has_perm:
                log.debug('discord: parting member %s from %s/%s', member, channel.id, channel)
                pylink_user.channels.discard(channel.id)
                pylink_channel.remove_user(uid)

                # We send KICK from a server to prevent triggering antiflood mechanisms...
                pylink_netobj.call_hooks([
                    guild.id,
                    'KICK',
                    {
                        'channel': channel.id,
                        'target': uid,
                        'text': "User removed from channel"
                    }
                ])

        # Note: once we've gotten here, it is possible that the channel was removed because the bot
        # no longer has access to it
        if channel.id in pylink_netobj.channels:
            if users_joined:
                pylink_netobj.call_hooks([
                    guild.id,
                    'JOIN',
                    {
                        'channel': channel.id,
                        'users': users_joined,
                        'modes': []
                    }
                ])

            if modes:
                pylink_netobj.apply_modes(channel.id, modes)
                log.debug('(%s) Relaying permission changes on %s/%s as modes: %s', self.protocol.name, member.name,
                          channel, pylink_netobj.join_modes(modes))
                if relay_modes:
                    pylink_netobj.call_hooks([guild.id, 'MODE', {'target': channel.id, 'modes': modes}])