示例#1
0
def load(irc, source, args):
    """<plugin name>.

    Loads a plugin from the plugin folder."""
    irc.checkAuthenticated(source, allowOper=False)
    try:
        name = args[0]
    except IndexError:
        irc.reply("Error: Not enough arguments. Needs 1: plugin name.")
        return
    if name in world.plugins:
        irc.reply("Error: %r is already loaded." % name)
        return
    log.info('(%s) Loading plugin %r for %s', irc.name, name,
             irc.getHostmask(source))
    try:
        world.plugins[name] = pl = utils.loadPlugin(name)
    except ImportError as e:
        if str(e) == ('No module named %r' % name):
            log.exception(
                'Failed to load plugin %r: The plugin could not be found.',
                name)
        else:
            log.exception('Failed to load plugin %r: ImportError.', name)
        raise
    else:
        if hasattr(pl, 'main'):
            log.debug('Calling main() function of plugin %r', pl)
            pl.main(irc)
    irc.reply("Loaded plugin %r." % name)
示例#2
0
def load(irc, source, args):
    """<plugin name>.

    Loads a plugin from the plugin folder."""
    # Note: reload capability is acceptable here, because all it actually does is call
    # load after unload.
    permissions.checkPermissions(irc, source, ['core.load', 'core.reload'])

    try:
        name = args[0]
    except IndexError:
        irc.reply("Error: Not enough arguments. Needs 1: plugin name.")
        return
    if name in world.plugins:
        irc.reply("Error: %r is already loaded." % name)
        return
    log.info('(%s) Loading plugin %r for %s', irc.name, name, irc.getHostmask(source))
    try:
        world.plugins[name] = pl = utils.loadPlugin(name)
    except ImportError as e:
        if str(e) == ('No module named %r' % name):
            log.exception('Failed to load plugin %r: The plugin could not be found.', name)
        else:
            log.exception('Failed to load plugin %r: ImportError.', name)
        raise
    else:
        if hasattr(pl, 'main'):
            log.debug('Calling main() function of plugin %r', pl)
            pl.main(irc=irc)
    irc.reply("Loaded plugin %r." % name)
示例#3
0
def rehash():
    """Rehashes the PyLink daemon."""
    log.info('Reloading PyLink configuration...')
    old_conf = conf.conf.copy()
    fname = conf.fname
    new_conf = conf.load_conf(fname, errors_fatal=False, logger=log)
    conf.conf = new_conf

    # Reset any file logger options.
    _stop_file_loggers()
    files = new_conf['logging'].get('files')
    if files:
        for filename, config in files.items():
            _make_file_logger(filename, config.get('loglevel'))

    log.debug('rehash: updating console log level')
    world.console_handler.setLevel(_get_console_log_level())
    login._make_cryptcontext()  # refresh password hashing settings

    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.serverdata = new_conf['servers'][network]

            ircobj.autoconnect_active_multiplier = 1

            # Clear the IRC object's channel loggers and replace them with
            # new ones by re-running log_setup().
            while ircobj.loghandlers:
                log.removeHandler(ircobj.loghandlers.pop())

            ircobj.log_setup()

    utils._reset_module_dirs()

    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:
            try:
                proto = utils._get_protocol_module(sdata['protocol'])

                # API note: 2.0.x style of starting network connections
                world.networkobjects[network] = newirc = proto.Class(network)
                newirc.connect()
            except:
                log.exception(
                    'Failed to initialize network %r, skipping it...', network)

    log.info('Finished reloading PyLink configuration.')
示例#4
0
def _process_conns():
    """Main loop which processes connected sockets."""

    while not world.shutting_down.is_set():
        for socketkey, mask in selector.select(timeout=SELECT_TIMEOUT):
            irc = socketkey.data
            try:
                if mask & selectors.EVENT_READ and not irc._aborted.is_set():
                    irc._run_irc()
            except:
                log.exception('Error in select driver loop:')
                continue
示例#5
0
    def _get_webhook_fields(self, user):
        """
        Returns a dict of Relay substitution fields for the given User object.
        This attempts to find the original user via Relay if the .remote metadata field is set.

        The output includes all keys provided in User.get_fields(), plus the following:
            netname: The full network name of the network 'user' belongs to
            nettag: The short network tag of the network 'user' belongs to
            avatar: The URL to the user's avatar (str), or None if no avatar is specified
        """
        # Try to lookup the remote user data via relay metadata
        if hasattr(user, 'remote'):
            remotenet, remoteuid = user.remote
            try:
                netobj = world.networkobjects[remotenet]
                user = netobj.users[remoteuid]
            except LookupError:
                netobj = user._irc

        fields = user.get_fields()
        fields['netname'] = netobj.get_full_network_name()
        fields['nettag'] = netobj.name

        default_avatar_url = self.serverdata.get('default_avatar_url')
        avatar = None
        # XXX: we'll have a more rigorous matching system later on
        if user.services_account in self.serverdata.get('avatars', {}):
            avatar_url = self.serverdata['avatars'][user.services_account]
            p = urllib.parse.urlparse(avatar_url)
            log.debug('(%s) Got raw avatar URL %s for user %s', self.name, avatar_url, user)

            if p.scheme == 'gravatar' and libgravatar:  # gravatar:[email protected]
                try:
                    g = libgravatar.Gravatar(p.path)
                    log.debug('(%s) Using Gravatar email %s for user %s', self.name, p.path, user)
                    avatar = g.get_image(use_ssl=True)
                except:
                    log.exception('Failed to obtain Gravatar image for user %s/%s', user, p.path)

            elif p.scheme in ('http', 'https'):  # a direct image link
                avatar = avatar_url

            else:
                log.warning('(%s) Unknown avatar URI %s for user %s', self.name, avatar_url, user)
        elif default_avatar_url:
            log.debug('(%s) Avatar not defined for user %s; using default avatar %s', self.name, user, default_avatar_url)
            avatar = default_avatar_url
        else:
            log.debug('(%s) Avatar not defined for user %s; using default webhook avatar', self.name, user)
        fields['avatar'] = avatar
        return fields
示例#6
0
def _remove_pid():
    pidfile = "%s.pid" % conf.confname
    if world._should_remove_pid:
        # Remove our pid file.
        log.info("Removing PID file %r.", pidfile)
        try:
            os.remove(pidfile)
        except OSError:
            log.exception("Failed to remove PID file %r, ignoring..." %
                          pidfile)
    else:
        log.debug(
            'Not removing PID file %s as world._should_remove_pid is False.' %
            pidfile)
示例#7
0
def _kill_plugins(irc=None):
    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)
示例#8
0
def rehash(irc, source, args):
    """takes no arguments.

    Reloads the configuration file for PyLink, (dis)connecting added/removed networks.

    Note: plugins must be manually reloaded."""
    permissions.checkPermissions(irc, source, ['core.rehash'])
    try:
        control._rehash()
    except Exception as e:  # Something went wrong, abort.
        log.exception("Error REHASHing config: ")
        irc.reply("Error loading configuration file: %s: %s" % (type(e).__name__, e))
        return
    else:
        irc.reply("Done.")
示例#9
0
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])
示例#10
0
    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()
示例#11
0
def _shutdown(irc=None):
    """Shuts down the Pylink daemon."""
    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)

    for ircobj in world.networkobjects.copy().values():
        # Disconnect all our networks.
        remove_network(ircobj)
def _check_connection(irc, args):
    ip = args['ip']
    try:
        result = sshbl.scan(ip)
    except:
        log.exception("SSHBL scan errored:")
        return

    threshold = irc.get_service_option('sshbl', 'threshold', default=0)
    reason = irc.get_service_option('sshbl', 'reason',
        "Your host runs an SSH daemon commonly used by spammer IPs. Consider upgrading your machines "
        "or contacting network staff for an exemption.")

    if result:
        _, port, blacklisted, score = result
        if not blacklisted:
            return
        log.info("sshbl: caught IP %s:%s (%s/%s) with score %s", ip, port, irc.name, args['nick'], score)
        if args['uid'] in irc.users:
            irc.kill(irc.pseudoclient.uid, args['uid'], reason)
示例#13
0
    def _update_user_status(self, guild, uid, presence):
        """Handles a Discord presence update."""
        pylink_netobj = self.protocol._children.get(guild.id)
        if pylink_netobj:
            try:
                u = pylink_netobj.users[uid]
            except KeyError:
                log.exception('(%s) _update_user_status: could not fetch user %s', self.protocol.name, uid)
                return
            # It seems that presence updates are not sent at all for offline users, so they
            # turn into an unset field in disco. I guess this makes sense for saving bandwidth?
            if presence:
                status = presence.status
            else:
                status = DiscordStatus.OFFLINE

            if status != DiscordStatus.ONLINE:
                awaymsg = self.status_mapping.get(status.value, 'Unknown Status')
            else:
                awaymsg = ''

            now_invisible = None
            if not pylink_netobj.join_offline_users:
                if status in (DiscordStatus.OFFLINE, DiscordStatus.INVISIBLE):
                    # If we are hiding offline users, set a special flag for relay to quit the user.
                    log.debug('(%s) Hiding user %s/%s from relay channels as they are offline', pylink_netobj.name,
                              uid, pylink_netobj.get_friendly_name(uid))
                    now_invisible = True
                    u._invisible = True
                elif (u.away in (self.status_mapping['INVISIBLE'], self.status_mapping['OFFLINE'])):
                    # User was previously offline - burst them now.
                    log.debug('(%s) Rejoining user %s/%s from as they are now online', pylink_netobj.name,
                              uid, pylink_netobj.get_friendly_name(uid))
                    now_invisible = False
                    u._invisible = False

            u.away = awaymsg
            pylink_netobj.call_hooks([uid, 'AWAY', {'text': awaymsg, 'now_invisible': now_invisible}])
示例#14
0
def masskill(irc, source, args, use_regex=False):
    """<banmask / exttarget> [<kill/ban reason>] [--akill/ak] [--force-kb/-f] [--include-opers/-o]

    Kills all users matching the given PyLink banmask.

    The --akill option can also be given to convert kills to akills, which expire after 7 days.

    For relay users, attempts to kill are forwarded as a kickban to every channel where the calling user
    meets claim requirements to set a ban (i.e. this is true if you are opped, if your network is in claim list, etc.;
    see "help CLAIM" for more specific rules). This can also be extended to all shared channels
    the user is in using the --force-kb option (we hope this feature is only used for good).

    By default, this command will ignore opers. This behaviour can be suppressed using the --include-opers option.

    To properly kill abusers on another network, combine this command with the 'remote' command in the
    'networks' plugin and adjust your banmasks accordingly."""
    permissions.check_permissions(irc, source, ['opercmds.masskill'])

    args = masskill_parser.parse_args(args)

    if args.force_kb:
        permissions.check_permissions(irc, source, ['opercmds.masskill.force'])

    reason = ' '.join(args.reason)

    results = killed = 0

    userlist_func = irc.match_all_re if use_regex else irc.match_all

    seen_users = set()
    for uid in userlist_func(args.banmask):
        userobj = irc.users[uid]

        if irc.is_oper(uid) and not args.include_opers:
            irc.reply('Skipping killing \x02%s\x02 because they are opered.' %
                      userobj.nick)
            continue
        elif irc.get_service_bot(uid):
            irc.reply(
                'Skipping killing \x02%s\x02 because it is a service client.' %
                userobj.nick)
            continue

        relay = world.plugins.get('relay')
        if relay and hasattr(userobj, 'remote'):
            # For relay users, forward kill attempts as kickban because we don't want networks k-lining each others' users.
            bans = [irc.make_channel_ban(uid)]
            for channel in userobj.channels.copy(
            ):  # Look in which channels the user appears to be in locally

                if (args.force_kb or relay.check_claim(irc, channel, source)):
                    irc.mode(irc.pseudoclient.uid, channel, bans)
                    irc.kick(irc.pseudoclient.uid, channel, uid, reason)

                    # XXX: code duplication with massban.
                    try:
                        irc.call_hooks([
                            irc.pseudoclient.uid, 'OPERCMDS_MASSKILL_BAN', {
                                'target': channel,
                                'modes': bans,
                                'parse_as': 'MODE'
                            }
                        ])
                        irc.call_hooks([
                            irc.pseudoclient.uid, 'OPERCMDS_MASSKILL_KICK', {
                                'channel': channel,
                                'target': uid,
                                'text': reason,
                                'parse_as': 'KICK'
                            }
                        ])
                    except:
                        log.exception(
                            '(%s) Failed to send process massban hook; some kickbans may have not '
                            'been sent to plugins / relay networks!', irc.name)

                    if uid not in seen_users:  # Don't count users multiple times on different channels
                        killed += 1
                else:
                    irc.reply(
                        "Not kicking \x02%s\x02 from \x02%s\x02 because you don't have CLAIM access. If this is "
                        "another network's channel, ask someone to op you or use the --force-kb option."
                        % (userobj.nick, channel))
        else:
            if args.akill:  # TODO: configurable length via strings such as "2w3d5h6m3s" - though month and minute clash this way?
                if not (userobj.realhost or userobj.ip):
                    irc.reply(
                        "Skipping akill on %s because PyLink doesn't know the real host."
                        % irc.get_hostmask(uid))
                    continue
                irc.set_server_ban(irc.pseudoclient.uid,
                                   604800,
                                   host=userobj.realhost or userobj.ip
                                   or userobj.host,
                                   reason=reason)
            else:
                irc.kill(irc.pseudoclient.uid, uid, reason)
                try:
                    irc.call_hooks([
                        irc.pseudoclient.uid, 'OPERCMDS_MASSKILL', {
                            'target': uid,
                            'parse_as': 'KILL',
                            'userdata': userobj,
                            'text': reason
                        }
                    ])
                except:
                    log.exception(
                        '(%s) Failed to send process massban hook; some kickbans may have not '
                        'been sent to plugins / relay networks!', irc.name)
            killed += 1
        results += 1
        seen_users.add(uid)
    else:
        log.info('(%s) Ran masskill%s for %s (%s/%s user(s) removed)',
                 irc.name, 're' if use_regex else '', irc.get_hostmask(source),
                 killed, results)
        irc.reply('Masskilled %s/%s users.' % (killed, results))
示例#15
0
def massban(irc, source, args, use_regex=False):
    """<channel> <banmask / exttarget> [<kick reason>] [--quiet/-q] [--force/-f] [--include-opers/-o]

    Applies (i.e. kicks affected users) the given PyLink banmask on the specified channel.

    The --quiet option can also be given to mass-mute the given user on networks where this is supported
    (currently ts6, unreal, and inspircd). No kicks will be sent in this case.

    By default, this command will ignore opers. This behaviour can be suppressed using the --include-opers option.

    Relay CLAIM checking is used on Relay channels if it is enabled; use the --force option
    to override this if needed."""
    permissions.check_permissions(irc, source, ['opercmds.massban'])

    args = massban_parser.parse_args(args)
    reason = ' '.join(args.reason)

    if args.force:
        permissions.check_permissions(irc, source, ['opercmds.massban.force'])

    if args.channel not in irc.channels:
        irc.error("Unknown channel %r." % args.channel)
        return
    elif 'relay' in world.plugins and (not world.plugins['relay'].check_claim(
            irc, args.channel, source)) and (not args.force):
        irc.error(
            "You do not have access to set bans in %s. Ask someone to op you or use the --force option."
            % args.channel)
        return

    results = 0

    userlist_func = irc.match_all_re if use_regex else irc.match_all
    for uid in userlist_func(args.banmask, channel=args.channel):

        if irc.is_oper(uid) and not args.include_opers:
            irc.reply('Skipping banning \x02%s\x02 because they are opered.' %
                      irc.users[uid].nick)
            continue
        elif irc.get_service_bot(uid):
            irc.reply(
                'Skipping banning \x02%s\x02 because it is a service client.' %
                irc.users[uid].nick)
            continue

        # Remove the target's access before banning them.
        bans = [('-%s' % irc.cmodes[prefix], uid)
                for prefix in irc.channels[args.channel].get_prefix_modes(uid)
                if prefix in irc.cmodes]

        # Then, add the actual ban.
        bans += [
            irc.make_channel_ban(uid,
                                 ban_type='quiet' if args.quiet else 'ban')
        ]
        irc.mode(irc.pseudoclient.uid, args.channel, bans)

        try:
            irc.call_hooks([
                irc.pseudoclient.uid, 'OPERCMDS_MASSBAN', {
                    'target': args.channel,
                    'modes': bans,
                    'parse_as': 'MODE'
                }
            ])
        except:
            log.exception(
                '(%s) Failed to send process massban hook; some bans may have not '
                'been sent to plugins / relay networks!', irc.name)

        if not args.quiet:
            irc.kick(irc.pseudoclient.uid, args.channel, uid, reason)

            # XXX: this better not be blocking...
            try:
                irc.call_hooks([
                    irc.pseudoclient.uid, 'OPERCMDS_MASSKICK', {
                        'channel': args.channel,
                        'target': uid,
                        'text': reason,
                        'parse_as': 'KICK'
                    }
                ])

            except:
                log.exception(
                    '(%s) Failed to send process massban hook; some kicks may have not '
                    'been sent to plugins / relay networks!', irc.name)

        results += 1
    else:
        irc.reply('Banned %s users on %r.' % (results, args.channel))
        log.info('(%s) Ran massban%s for %s on %s (%s user(s) removed)',
                 irc.name, 're' if use_regex else '', irc.get_hostmask(source),
                 args.channel, results)
示例#16
0
    def on_message(self, event: events.MessageCreate):
        message = event.message
        subserver = None
        target = None

        # If the bot is the one sending the message, don't do anything
        if message.author.id == self.me.id:
            return
        elif message.webhook_id:  # Ignore messages from other webhooks for now...
            return

        text = message.content
        if not message.guild:
            # This is a DM.
            target = self.me.id
            if message.author.id not in self._dm_channels:
                self._dm_channels[message.author.id] = message.channel

            common_guilds = self._find_common_guilds(message.author.id)

            # If we are on multiple guilds, force the sender to choose a server to send from, since
            # every PyLink event needs to be associated with a subserver.
            if len(common_guilds) > 1:
                log.debug('discord: received DM from %s/%s - forcing guild name disambiguation', message.author.id,
                          message.author)
                fail = False
                guild_id = None
                try:
                    netname, text = text.split(' ', 1)
                except ValueError:
                    fail = True
                else:
                    if netname not in world.networkobjects:
                        fail = True
                    else:
                        guild_id = world.networkobjects[netname].sid
                        # Unrecognized guild or not one in common
                        if guild_id not in self.protocol._children or \
                                guild_id not in common_guilds:
                            fail = True

                if fail:
                    # Build a list of common server *names*
                    common_servers = [nwobj.name for gid, nwobj in self.protocol._children.items() if gid in common_guilds]
                    try:
                        message.channel.send_message(
                            "To DM me, please prefix your messages with a guild name so I know where to "
                            "process your messages: **<guild name> <command> <args>**\n"
                            "Guilds we have in common: **%s**" % ', '.join(common_servers)
                        )
                    except:
                        log.exception("(%s) Could not send message to user %s", self.name, message.author)
                    return
                else:
                    log.debug('discord: using guild %s/%s for DM from %s/%s', world.networkobjects[netname].sid, netname,
                              message.author.id, message.author)
                    subserver = guild_id
            elif common_guilds:
                # We should be on at least one guild, right?
                subserver = common_guilds[0]
                log.debug('discord: using guild %s for DM from %s', subserver, message.author.id)
            else:
                log.debug('discord: ignoring message from user %s/%s since we are not in any common guilds',
                          message.author.id, message.author)
                return
        else:
            subserver = message.guild.id
            target = message.channel.id

            def format_user_mentions(u):
                # Try to find the user's guild nick, falling back to the user if that fails
                if message.guild and u.id in message.guild.members:
                    return '@' + message.guild.members[u.id].name
                else:
                    return '@' + str(u)

            # Translate mention IDs to their names
            text = message.replace_mentions(user_replace=format_user_mentions,
                                            role_replace=lambda r: '@' + str(r),
                                            channel_replace=str)

        if not subserver:
            return

        pylink_netobj = self.protocol._children[subserver]
        author = message.author.id

        def _send(text):
            for line in text.split('\n'):  # Relay multiline messages as such
                pylink_netobj.call_hooks([author, 'PRIVMSG', {'target': target, 'text': line}])

        _send(text)
        # For attachments, just send the link
        for attachment in message.attachments.values():
            _send(attachment.url)
示例#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()
示例#18
0
    def _message_builder(self):
        """
        Discord message queue handler. Also supports virtual users via webhooks.
        """
        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)

        joined_messages = collections.defaultdict(collections.deque)
        while not self._aborted.is_set():
            try:
                # message is an instance of QueuedMessage (defined in this file)
                message = self.message_queue.get(timeout=BATCH_DELAY)
                message.text = utils.strip_irc_formatting(message.text)

                if not self.serverdata.get('allow_mention_everyone', False):
                    message.text = message.text.replace('@here', '@ here')
                    message.text = message.text.replace('@everyone', '@ everyone')

                # First, buffer messages by channel
                joined_messages[message.channel].append(message)

            except queue.Empty:  # Then process them together when we run out of things in the queue
                for channel, messages in joined_messages.items():
                    next_message = []
                    length = 0
                    current_sender = None
                    # We group messages here to avoid being throttled as often. In short, we want to send a message when:
                    # 1) The virtual sender (for webhook purposes) changes
                    # 2) We reach the message limit for one batch (2000 chars)
                    # 3) We run out of messages at the end
                    while messages:
                        message = messages.popleft()
                        next_message.append(message.text)
                        length += len(message.text)

                        if message.sender != current_sender or length >= self.MAX_MESSAGE_SIZE:
                            current_sender = message.sender
                            _send(current_sender, channel, message.pylink_target, next_message)
                            next_message.clear()
                            length = 0

                    # The last batch
                    if next_message:
                        _send(current_sender, channel, message.pylink_target, next_message)

                joined_messages.clear()
            except Exception:
                log.exception("Exception in message queueing thread:")
示例#19
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)
示例#20
0
def unload(irc, source, args):
    """<plugin name>.

    Unloads a currently loaded plugin."""
    permissions.checkPermissions(irc, source, ['core.unload', 'core.reload'])

    try:
        name = args[0]
    except IndexError:
        irc.reply("Error: Not enough arguments. Needs 1: plugin name.")
        return

    # Since we're using absolute imports in 0.9.x+, the module name differs from the actual plugin
    # name.
    modulename = utils.PLUGIN_PREFIX + name

    if name in world.plugins:
        log.info('(%s) Unloading plugin %r for %s', irc.name, name, irc.getHostmask(source))
        pl = world.plugins[name]
        log.debug('sys.getrefcount of plugin %s is %s', pl, sys.getrefcount(pl))

        # Remove any command functions defined by the plugin.
        for cmdname, cmdfuncs in world.services['pylink'].commands.copy().items():
            log.debug('cmdname=%s, cmdfuncs=%s', cmdname, cmdfuncs)

            for cmdfunc in cmdfuncs:
                log.debug('__module__ of cmdfunc %s is %s', cmdfunc, cmdfunc.__module__)
                if cmdfunc.__module__ == modulename:
                    log.debug("Removing %s from world.services['pylink'].commands[%s]", cmdfunc, cmdname)
                    world.services['pylink'].commands[cmdname].remove(cmdfunc)

                    # If the cmdfunc list is empty, remove it.
                    if not cmdfuncs:
                        log.debug("Removing world.services['pylink'].commands[%s] (it's empty now)", cmdname)
                        del world.services['pylink'].commands[cmdname]

        # Remove any command hooks set by the plugin.
        for hookname, hookfuncs in world.hooks.copy().items():
            for hookfunc in hookfuncs:
                if hookfunc.__module__ == modulename:
                    world.hooks[hookname].remove(hookfunc)
                    # If the hookfuncs list is empty, remove it.
                    if not hookfuncs:
                        del world.hooks[hookname]

        # Call the die() function in the plugin, if present.
        if hasattr(pl, 'die'):
            try:
                pl.die(irc=irc)
            except:  # But don't allow it to crash the server.
                log.exception('(%s) Error occurred in die() of plugin %s, skipping...', irc.name, pl)

        # Delete it from memory (hopefully).
        del world.plugins[name]
        for n in (name, modulename):
            if n in sys.modules:
                del sys.modules[n]
            if n in globals():
                del globals()[n]

        # Garbage collect.
        gc.collect()

        irc.reply("Unloaded plugin %r." % name)
        return True  # We succeeded, make it clear (this status is used by reload() below)
    else:
        irc.reply("Unknown plugin %r." % name)
示例#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