コード例 #1
0
class _Schema:

    _openable = {
        "path": str,
        immp.Optional("enabled", True): bool,
        immp.Optional("config", dict): dict
    }

    _plugs = {str: _openable}

    _hooks = {
        str:
        immp.Schema({immp.Optional("priority"): immp.Nullable(int)}, _openable)
    }

    _channels = {str: {"plug": str, "source": str}}

    _logging = {immp.Optional("disable_existing_loggers", False): bool}

    config = immp.Schema({
        immp.Optional("path", list): [str],
        immp.Optional("plugs", dict): _plugs,
        immp.Optional("channels", dict): _channels,
        immp.Optional("groups", dict): {
            str: dict
        },
        immp.Optional("hooks", dict): _hooks,
        immp.Optional("logging"): immp.Nullable(_logging)
    })
コード例 #2
0
class ShellHook(immp.ResourceHook):
    """
    Hook to start a Python shell when a message is received.
    """

    schema = immp.Schema({
        immp.Optional("all", False): bool,
        immp.Optional("console"): immp.Nullable("ptpython")
    })

    def __init__(self, name, config, host):
        super().__init__(name, config, host)
        if self.config["console"] == "ptpython":
            if ptpython:
                log.debug("Using ptpython console")
                self.console = self._ptpython
            else:
                raise immp.PlugError("'ptpython' module not installed")
        else:
            log.debug("Using native console")
            self.console = self._code

    @staticmethod
    def _ptpython(local):
        ptpython.repl.embed(globals(), local)

    @staticmethod
    def _code(local):
        code.interact(local=dict(globals(), **local))

    async def on_receive(self, sent, source, primary):
        await super().on_receive(sent, source, primary)
        if sent.channel in self.host.channels or self.config["all"]:
            log.debug("Entering console: %r", sent)
            self.console(locals())
コード例 #3
0
ファイル: discord.py プロジェクト: raveinid/IMMP
class _Schema:

    config = immp.Schema({"token": str,
                          immp.Optional("bot", True): bool,
                          immp.Optional("webhooks", dict): {str: str},
                          immp.Optional("playing"): immp.Nullable(str)})

    webhook = immp.Schema(immp.Any({"code": int, "message": str}, {"id": str}))
コード例 #4
0
ファイル: sync.py プロジェクト: raveinid/IMMP
class ForwardHook(_SyncHookBase):
    """
    Hook to propagate messages from a source channel to one or more destination channels.
    """

    schema = immp.Schema({immp.Optional("users"): immp.Nullable([str])}, _SyncHookBase.schema)

    @property
    def _channels(self):
        try:
            return {self.host.channels[key]: tuple(self.host.channels[label] for label in value)
                    for key, value in self.config["channels"].items()}
        except KeyError as e:
            raise immp.ConfigError("No such channel '{}'".format(repr(e.args[0])))

    async def send(self, channel, msg):
        """
        Send a message to all channels in this sync.

        Args:
            channel (.Channel):
                Source channel that defines the underlying forwarding channels to send to.
            msg (.Message):
                External message to push.
        """
        queue = []
        clone = msg.clone()
        await self._alter_recurse(clone, self._alter_name)
        for synced in self._channels[channel]:
            local = clone.clone()
            await self._alter_recurse(local, self._alter_identities, synced)
            queue.append(self._send(synced, local))
        # Send all the messages in parallel.
        await gather(*queue)

    def _accept(self, msg):
        if not super()._accept(msg):
            return False
        if self.config["users"] is not None:
            if not msg.user or msg.user.id not in self.config["users"]:
                log.debug("Not syncing message from non-whitelisted user: %r", msg.user.id)
                return False
        return True

    async def on_receive(self, sent, source, primary):
        await super().on_receive(sent, source, primary)
        if primary and sent.channel in self._channels and self._accept(source):
            await self.send(sent.channel, source)
コード例 #5
0
class _Schema:

    config = immp.Schema({
        "route": str,
        immp.Optional("secret"): immp.Nullable(str)
    })

    event = immp.Schema({
        "repository": {
            "full_name": str
        },
        "sender": {
            "id": int,
            "login": str,
            "avatar_url": str,
            "html_url": str
        }
    })
コード例 #6
0
class IRCPlug(immp.Plug):
    """
    Plug for an IRC server.
    """

    schema = immp.Schema({
        "server": {
            "host": str,
            "port": int,
            immp.Optional("ssl", False): bool,
            immp.Optional("password"): immp.Nullable(str)
        },
        "user": {
            "nick": str,
            "real-name": str
        },
        immp.Optional("quit", "Disconnecting"): str,
        immp.Optional("accept-invites", False): bool,
        immp.Optional("puppet", False): bool,
        immp.Optional("puppet-prefix", ""): str
    })

    def __init__(self, name, config, host):
        super().__init__(name, config, host)
        self._client = None
        # Don't yield messages for initial self-joins.
        self._joins = set()
        # Maintain puppet clients by nick for cleaner sending.
        self._puppets = {}

    @property
    def network_name(self):
        if self._client and self._client.network:
            network = self._client.network
        else:
            network = self.config["server"]["host"]
        return "{} IRC".format(network)

    @property
    def network_id(self):
        return "irc:{}".format(self.config["server"]["host"])

    async def start(self):
        self._client = IRCClient(self, self.config["server"]["host"],
                                 self.config["server"]["port"],
                                 self.config["server"]["ssl"],
                                 self.config["user"]["nick"],
                                 self.config["server"]["password"], "immp",
                                 self.config["user"]["real-name"], True)
        await self._client.connect()
        for channel in self.host.channels.values():
            if channel.plug == self and channel.source.startswith("#"):
                self._joins.add(channel.source)
                await self._client.join(channel.source)

    async def stop(self):
        if self._client:
            await self._client.disconnect(self.config["quit"])
            self._client = None
        for client in self._puppets.values():
            await client.disconnect(self.config["quit"])
        self._puppets.clear()

    async def user_from_id(self, id_):
        nick = id_.split("!", 1)[0]
        user = await self.user_from_username(nick)
        if user:
            return user
        else:
            return IRCUser.from_id(self, id_)

    async def user_from_username(self, username):
        try:
            return self._client.users[username]
        except KeyError:
            pass
        for user in await self._client.who(username):
            if user.username == username:
                return user
        return None

    async def user_is_system(self, user):
        if user.id == self._client.nickmask:
            return True
        for client in self._puppets.values():
            if user.id == client.nickmask:
                return True
        else:
            return False

    async def public_channels(self):
        try:
            raw = await self._client.list()
        except IRCTryAgain:
            return None
        channels = (line.args[1] for line in raw)
        return [immp.Channel(self, channel) for channel in channels]

    async def private_channels(self):
        try:
            raw = await self._client.names()
        except IRCTryAgain:
            return None
        names = set()
        for line in raw:
            names.update(
                name.lstrip(self._client.prefixes)
                for name in line.args[3].split())
        return [
            immp.Channel(self, name) for name in names
            if name != self.config["user"]["nick"]
        ]

    async def channel_for_user(self, user):
        return immp.Channel(self, user.username)

    async def channel_is_private(self, channel):
        return not channel.source.startswith(tuple(self._client.types))

    async def channel_title(self, channel):
        return channel.source

    async def channel_members(self, channel):
        try:
            nicks = self._client.members[channel.source]
        except KeyError:
            members = list(await self._client.who(channel.source))
        else:
            members = [await self.user_from_username(nick) for nick in nicks]
        if await channel.is_private(
        ) and members[0].id != self._client.nickmask:
            members.append(await self.user_from_id(self._client.nickmask))
        return members

    async def channel_invite(self, channel, user):
        self._client.invite(channel.source, user.username)

    async def channel_remove(self, channel, user):
        self._client.kick(channel.source, user.username)

    async def _handle(self, line):
        if line.command in ("JOIN", "PART", "KICK", "PRIVMSG"):
            sent = await IRCMessage.from_line(self, line)
            if sent.joined and sent.joined[0].id == self._client.nickmask:
                if sent.channel.source in self._joins:
                    self._joins.remove(sent.channel.source)
                    return
            puppets = [client.nickmask for client in self._puppets.values()]
            # Don't yield messages sent by puppets, or for puppet kicks.
            if sent.user.id in puppets:
                pass
            elif sent.joined and all(user.id in puppets
                                     for user in sent.joined):
                pass
            elif sent.left and all(user.id in puppets for user in sent.left):
                pass
            else:
                self.queue(sent)
        elif line.command == "INVITE" and self.config["accept-invites"]:
            await self._client.join(line.args[1])

    @classmethod
    def _lines(cls, rich, user, action, edited):
        if not rich:
            return []
        elif not isinstance(rich, immp.RichText):
            rich = immp.RichText([immp.Segment(rich)])
        lines = []
        for text in IRCRichText.to_formatted(rich).split("\n"):
            if user:
                template = "* {} {}" if action else "<{}> {}"
                text = template.format(user.username or user.real_name, text)
            if edited:
                text = "[edit] {}".format(text)
            if not user and action:
                text = "\x01ACTION {}\x01".format(text)
            lines.append(text)
        return lines

    async def _puppet(self, user):
        username = user.username or user.real_name
        nick = self.config["puppet-prefix"] + "-".join(username.split())
        try:
            puppet = self._puppets[user]
        except KeyError:
            pass
        else:
            log.debug("Reusing puppet %r for user %r", puppet.nickmask, user)
            if puppet.nick.rstrip("_") != nick:
                puppet.nick = nick
            return puppet
        if user.plug and user.plug.network_id == self.network_id:
            for puppet in self._puppets.values():
                if user.id == puppet.nickmask:
                    log.debug("Matched nickmask with puppet %r", user.id)
                    return puppet
        log.debug("Adding puppet %r for user %r", nick, user)
        real_name = user.real_name or user.username
        if user.plug:
            real_name = "{} ({})".format(real_name, user.plug.network_name)
        puppet = IRCClient(self, self.config["server"]["host"],
                           self.config["server"]["port"],
                           self.config["server"]["ssl"], nick,
                           self.config["server"]["password"], "immp",
                           real_name)
        self._puppets[user] = puppet
        await puppet.connect()
        return puppet

    async def put(self, channel, msg):
        user = None if self.config["puppet"] else msg.user
        lines = []
        if msg.text:
            lines += self._lines(msg.text, user, msg.action, msg.edited)
        for attach in msg.attachments:
            if isinstance(attach, immp.File):
                text = "uploaded a file{}".format(
                    ": {}".format(attach) if str(attach) else "")
                lines += self._lines(text, user, True, msg.edited)
            elif isinstance(attach, immp.Location):
                text = "shared a location: {}".format(attach)
                lines += self._lines(text, user, True, msg.edited)
            elif isinstance(attach, immp.Message) and attach.text:
                lines += self._lines(attach.text, attach.user, attach.action,
                                     attach.edited)
        ids = []
        if self.config["puppet"] and msg.user:
            client = await self._puppet(msg.user)
            if not await channel.is_private():
                await client.join(channel.source)
        else:
            client = self._client
        for text in lines:
            line = client.send(channel.source, text)
            sent = await IRCMessage.from_line(self, line)
            self.queue(sent)
            ids.append(sent.id)
        return ids
コード例 #7
0
ファイル: sync.py プロジェクト: Terrance/IMMP
class ForwardHook(_SyncHookBase):
    """
    Hook to propagate messages from a source channel to one or more destination channels.
    """

    schema = immp.Schema(
        {
            immp.Optional("users"): immp.Nullable([str]),
            immp.Optional("groups", dict): {
                str: [str]
            }
        }, _SyncHookBase.schema)

    _channels = immp.ConfigProperty({immp.Channel: [immp.Channel]})
    _groups = immp.ConfigProperty({immp.Group: [immp.Channel]})

    async def _targets(self, channel):
        targets = set()
        if channel in self._channels:
            targets.update(self._channels[channel])
        for group, channels in self._groups.items():
            if await group.has_channel(channel):
                targets.update(channels)
        return targets

    async def send(self, msg, channels):
        """
        Send a message to all channels in this forwarding group.

        Args:
            msg (.Message):
                External message to push.
            channels (.Channel list):
                Set of target channels to forward the message to.
        """
        queue = []
        clone = msg.clone()
        await self._alter_recurse(clone, self._alter_name)
        for synced in channels:
            local = clone.clone()
            await self._alter_recurse(local, self._alter_identities, synced)
            queue.append(self._send(synced, local))
        # Send all the messages in parallel.
        await gather(*queue)

    def _accept(self, msg, id_):
        if not super()._accept(msg, id_):
            return False
        if self.config["users"] is not None:
            if not msg.user or msg.user.id not in self.config["users"]:
                log.debug("Not syncing message from non-whitelisted user: %r",
                          msg.user.id)
                return False
        return True

    async def on_receive(self, sent, source, primary):
        await super().on_receive(sent, source, primary)
        if not primary or not self._accept(source, sent.id):
            return
        targets = await self._targets(sent.channel)
        if targets:
            await self.send(source, targets)
コード例 #8
0
class LocalIdentityHook(immp.Hook, AccessPredicate, IdentityProvider):
    """
    Hook for managing physical users with multiple logical links across different plugs.  This
    effectively provides self-service identities, as opposed to being provided externally.
    """

    schema = immp.Schema({
        immp.Optional("instance"): immp.Nullable(int),
        "plugs": [str],
        immp.Optional("multiple", True): bool
    })

    _plugs = immp.ConfigProperty([immp.Plug])

    def __init__(self, name, config, host):
        super().__init__(name, config, host)
        self.db = None

    async def start(self):
        if not self.config["instance"]:
            # Find a non-conflicting number and assign it.
            codes = {
                hook.config["instance"]
                for hook in self.host.hooks.values()
                if isinstance(hook, self.__class__)
            }
            code = 1
            while code in codes:
                code += 1
            log.debug("Assigning instance code %d to hook %r", code, self.name)
            self.config["instance"] = code
        self.db = self.host.resources[DatabaseHook].db
        self.db.create_tables([IdentityGroup, IdentityLink, IdentityRole],
                              safe=True)

    def get(self, name):
        """
        Retrieve the identity group using the given name.

        Args:
            name (str):
                Existing name to query.

        Returns:
            .IdentityGroup:
                Linked identity, or ``None`` if not linked.
        """
        try:
            return IdentityGroup.select_links().where(
                IdentityGroup.name == name).get()
        except IdentityGroup.DoesNotExist:
            return None

    def find(self, user):
        """
        Retrieve the identity that contains the given user, if one exists.

        Args:
            user (.User):
                Existing user to query.

        Returns:
            .IdentityGroup:
                Linked identity, or ``None`` if not linked.
        """
        if not user or user.plug not in self._plugs:
            return None
        try:
            return (IdentityGroup.select_links().where(
                IdentityGroup.instance == self.config["instance"],
                IdentityLink.network == user.plug.network_id,
                IdentityLink.user == user.id).get())
        except IdentityGroup.DoesNotExist:
            return None

    async def channel_access(self, channel, user):
        return bool(
            IdentityLink.get(network=user.plug.network_id, user=user.id))

    async def identity_from_name(self, name):
        group = self.get(name)
        return await group.to_identity(self.host, self) if group else None

    async def identity_from_user(self, user):
        group = self.find(user)
        return await group.to_identity(self.host, self) if group else None

    def _test(self, channel, user):
        return channel.plug in self._plugs

    @command("id-add", scope=CommandScope.private, test=_test)
    async def add(self, msg, name, pwd):
        """
        Create a new identity, or link to an existing one from a second user.
        """
        if not msg.user or msg.user.plug not in self._plugs:
            return
        if self.find(msg.user):
            text = "{} Already identified".format(CROSS)
        else:
            pwd = IdentityGroup.hash(pwd)
            exists = False
            try:
                group = IdentityGroup.get(instance=self.config["instance"],
                                          name=name)
                exists = True
            except IdentityGroup.DoesNotExist:
                group = IdentityGroup.create(instance=self.config["instance"],
                                             name=name,
                                             pwd=pwd)
            if exists and not group.pwd == pwd:
                text = "{} Password incorrect".format(CROSS)
            elif not self.config["multiple"] and any(
                    link.network == msg.user.plug.network_id
                    for link in group.links):
                text = "{} Already identified on {}".format(
                    CROSS, msg.user.plug.network_name)
            else:
                IdentityLink.create(group=group,
                                    network=msg.user.plug.network_id,
                                    user=msg.user.id)
                text = "{} {}".format(TICK, "Added" if exists else "Claimed")
        await msg.channel.send(immp.Message(text=text))

    @command("id-rename", scope=CommandScope.private, test=_test)
    async def rename(self, msg, name):
        """
        Rename the current identity.
        """
        if not msg.user:
            return
        group = self.find(msg.user)
        if not group:
            text = "{} Not identified".format(CROSS)
        elif group.name == name:
            text = "{} No change".format(TICK)
        elif IdentityGroup.select().where(
                IdentityGroup.instance == self.config["instance"],
                IdentityGroup.name == name).exists():
            text = "{} Name already in use".format(CROSS)
        else:
            group.name = name
            group.save()
            text = "{} Claimed".format(TICK)
        await msg.channel.send(immp.Message(text=text))

    @command("id-password", scope=CommandScope.private, test=_test)
    async def password(self, msg, pwd):
        """
        Update the password for the current identity.
        """
        if not msg.user:
            return
        group = self.find(msg.user)
        if not group:
            text = "{} Not identified".format(CROSS)
        else:
            group.pwd = IdentityGroup.hash(pwd)
            group.save()
            text = "{} Changed".format(TICK)
        await msg.channel.send(immp.Message(text=text))

    @command("id-reset", scope=CommandScope.private, test=_test)
    async def reset(self, msg):
        """
        Delete the current identity and all linked users.
        """
        if not msg.user:
            return
        group = self.find(msg.user)
        if not group:
            text = "{} Not identified".format(CROSS)
        else:
            group.delete_instance()
            text = "{} Reset".format(TICK)
        await msg.channel.send(immp.Message(text=text))

    @command("id-role",
             scope=CommandScope.private,
             role=CommandRole.admin,
             test=_test)
    async def role(self, msg, name, role=None):
        """
        List roles assigned to an identity, or add/remove a given role.
        """
        try:
            group = IdentityGroup.get(instance=self.config["instance"],
                                      name=name)
        except IdentityGroup.DoesNotExist:
            text = "{} Name not registered".format(CROSS)
        else:
            if role:
                count = IdentityRole.delete().where(
                    IdentityRole.group == group,
                    IdentityRole.role == role).execute()
                if count:
                    text = "{} Removed".format(TICK)
                else:
                    IdentityRole.create(group=group, role=role)
                    text = "{} Added".format(TICK)
            else:
                roles = IdentityRole.select().where(
                    IdentityRole.group == group)
                if roles:
                    labels = [role.role for role in roles]
                    text = "Roles for {}: {}".format(name, ", ".join(labels))
                else:
                    text = "No roles for {}.".format(name)
        await msg.channel.send(immp.Message(text=text))
コード例 #9
0
ファイル: irc.py プロジェクト: Terrance/IMMP
class IRCPlug(immp.Plug):
    """
    Plug for an IRC server.
    """

    schema = immp.Schema({"server": {"host": str,
                                     "port": int,
                                     immp.Optional("ssl", False): bool,
                                     immp.Optional("password"): immp.Nullable(str)},
                          "user": {"nick": str,
                                   "real-name": str},
                          immp.Optional("perform", list): [str],
                          immp.Optional("quit", "Disconnecting"): str,
                          immp.Optional("accept-invites", False): bool,
                          immp.Optional("colour-nicks", False): bool,
                          immp.Optional("quote-reply-to", True): bool,
                          immp.Optional("puppet", False): bool,
                          immp.Optional("puppet-prefix", ""): str,
                          immp.Optional("send-delay", 0.5): float})

    def __init__(self, name, config, host):
        super().__init__(name, config, host)
        self._client = None
        # Don't yield messages for initial self-joins.
        self._joins = set()
        # Maintain puppet clients by nick for cleaner sending.
        self._puppets = {}
        # Queue multiple outgoing messages in quick succession and insert delays between them.
        self._delay_lock = DelayLock(lambda: self.config["send-delay"])
        # Cache the last message in each channel, to suppress reply quoting when directly below.
        self._last_msgs = {}

    @property
    def network_name(self):
        if self._client and self._client.network:
            network = self._client.network
        else:
            network = self.config["server"]["host"]
        return "{} IRC".format(network)

    @property
    def network_id(self):
        return "irc:{}".format(self.config["server"]["host"])

    async def start(self):
        await super().start()
        self._client = IRCClient(self,
                                 self.config["server"]["host"],
                                 self.config["server"]["port"],
                                 self.config["server"]["ssl"],
                                 self.config["user"]["nick"],
                                 self.config["server"]["password"],
                                 "immp",
                                 self.config["user"]["real-name"],
                                 self._connected,
                                 self._handle)
        await self._client.connect()

    async def stop(self):
        await super().stop()
        if self._client:
            await self._client.disconnect(self.config["quit"])
            self._client = None
        for client in self._puppets.values():
            await client.disconnect(self.config["quit"])
        self._puppets.clear()

    def get_user(self, nick):
        return self._client.users.get(nick)

    async def user_from_id(self, id_):
        nick = id_.split("!", 1)[0]
        user = await self.user_from_username(nick)
        return user or IRCUser.from_id(self, id_)

    async def user_from_username(self, username):
        user = self.get_user(username)
        if user:
            return user
        for client in self._puppets.values():
            if username == client.nick:
                return IRCUser.from_id(self, client.nickmask, client.name)
        else:
            return await self._client.whois(username)

    async def user_is_system(self, user):
        if user.id == self._client.nickmask:
            return True
        for client in self._puppets.values():
            if user.id == client.nickmask:
                return True
        else:
            return False

    async def public_channels(self):
        try:
            raw = await self._client.list()
        except IRCTryAgain:
            return None
        channels = (line.args[1] for line in raw)
        return [immp.Channel(self, channel) for channel in channels]

    async def private_channels(self):
        try:
            raw = await self._client.names()
        except IRCTryAgain:
            return None
        names = set(self._client.users)
        for line in raw:
            names.update(name.lstrip(self._client.prefixes) for name in line.args[3].split())
        names.discard(self._client.nick)
        for client in self._puppets.values():
            names.discard(client.nick)
        return [immp.Channel(self, name) for name in names]

    async def channel_for_user(self, user):
        return immp.Channel(self, user.username)

    async def channel_is_private(self, channel):
        return not channel.source.startswith(tuple(self._client.types))

    async def channel_title(self, channel):
        return channel.source

    async def channel_members(self, channel):
        if self._client.closing:
            return None
        try:
            nicks = self._client.members[channel.source]
        except KeyError:
            members = list(await self._client.who(channel.source))
        else:
            members = [await self.user_from_username(nick) for nick in nicks]
        if await channel.is_private() and members[0].id != self._client.nickmask:
            members.append(await self.user_from_id(self._client.nickmask))
        return members

    async def channel_invite(self, channel, user):
        await self._client.invite(channel.source, user.username)

    async def channel_remove(self, channel, user):
        await self._client.kick(channel.source, user.username)

    async def _connected(self):
        for perform in self.config["perform"]:
            self._client._write(Line.parse(perform))
        for channel in self.host.channels.values():
            if channel.plug == self and channel.source.startswith("#"):
                self._joins.add(channel.source)
                await self._client.join(channel.source)

    async def _handle(self, line):
        if line.command in ("JOIN", "PART", "KICK", "PRIVMSG"):
            sent = await IRCMessage.from_line(self, line)
            # Suppress initial joins and final parts.
            if sent.joined and sent.joined[0].id == self._client.nickmask:
                if sent.channel.source in self._joins:
                    self._joins.remove(sent.channel.source)
                    return
            elif sent.left and sent.left[0].id == self._client.nickmask and self._client.closing:
                return
            puppets = [client.nickmask for client in self._puppets.values()]
            # Don't yield messages sent by puppets, or for puppet kicks.
            if sent.user.id in puppets:
                pass
            elif sent.joined and all(user.id in puppets for user in sent.joined):
                pass
            elif sent.left and all(user.id in puppets for user in sent.left):
                pass
            else:
                self.queue(sent)
                self._last_msgs[sent.channel] = [sent]
        elif line.command == "INVITE" and self.config["accept-invites"]:
            await self._client.join(line.args[1])

    def _inline(self, rich):
        # Take an excerpt of some message text, merging multiple lines into one.
        if not rich:
            return rich
        inlined = rich.clone()
        for segment in inlined:
            segment.text = segment.text.replace("\n", "  ")
        return inlined.trim(160)

    def _author_name(self, user):
        name = user.username or user.real_name
        if self.config["colour-nicks"]:
            name = IRCSegment._coloured(IRCUser.nick_colour(name), name)
        return name

    def _author_template(self, user=None, action=False, edited=False, reply=None, quoter=None):
        prefix = []
        suffix = []
        if user:
            prefix.append(("* {} " if action else "<{}> ").format(self._author_name(user)))
        if quoter:
            if isinstance(quoter, immp.User):
                prefix.append("<{}> ".format(self._author_name(quoter)))
            prefix.append("[")
            suffix.append("]")
        if edited:
            prefix.append("[edit] ")
        if reply:
            prefix.append("{} ".format(ARROW if self.config["quote-reply-to"] else ARROW[1:]))
        if action and not user and not quoter:
            prefix.append("\x01ACTION ")
            suffix.append("\x01")
        return "{}{{}}{}".format("".join(reversed(prefix)), "".join(suffix)).strip()

    def _lines(self, rich, user=None, action=False, edited=False, reply=None, quoter=None):
        if not rich:
            return []
        elif not isinstance(rich, immp.RichText):
            rich = immp.RichText([immp.Segment(rich)])
        template = self._author_template(user, action, edited, reply, quoter)
        lines = []
        # Line length isn't well defined (generally 512 bytes for the entire wire line), so set a
        # conservative length limit to allow for long channel names and formatting characters.
        for line in chain(*(chunk.lines() for chunk in rich.chunked(360))):
            text = IRCRichText.to_formatted(line)
            lines.append(template.format(text))
        return lines

    async def _puppet(self, user, create=True):
        username = user.username or user.real_name
        nick = self.config["puppet-prefix"] + "-".join(username.split())
        try:
            puppet = self._puppets[user]
        except KeyError:
            if not create:
                return None
        else:
            log.debug("Reusing puppet %r for user %r", puppet, user)
            if puppet.nick.rstrip("_") != nick:
                await puppet.set_nick(nick)
            return puppet
        if user.plug and user.plug.network_id == self.network_id:
            for puppet in self._puppets.values():
                if user.id == puppet.nickmask:
                    log.debug("Matched nickmask with puppet %r", user.id)
                    return puppet
        log.debug("Adding puppet %r for user %r", nick, user)
        real_name = user.real_name or user.username
        if user.plug:
            real_name = "{} ({}{})".format(real_name, user.plug.network_name,
                                           ": {}".format(user.id) if user.id else "")
        puppet = IRCClient(self,
                           self.config["server"]["host"],
                           self.config["server"]["port"],
                           self.config["server"]["ssl"],
                           nick,
                           self.config["server"]["password"],
                           "immp",
                           real_name)
        self._puppets[user] = puppet
        await puppet.connect()
        return puppet

    async def put(self, channel, msg):
        user = None if self.config["puppet"] else msg.user
        lines = []
        if isinstance(msg.reply_to, immp.Message) and msg.reply_to.text:
            if not self.config["quote-reply-to"]:
                pass
            elif msg.reply_to not in self._last_msgs.get(channel, []):
                lines.append(self._lines(self._inline(msg.reply_to.text), msg.reply_to.user,
                                         msg.reply_to.action, msg.edited, None, True)[0])
        if msg.text:
            lines += self._lines(msg.text, user, msg.action, msg.edited, msg.reply_to)
        for attach in msg.attachments:
            if isinstance(attach, immp.File):
                text = "uploaded a file{}".format(": {}".format(attach) if str(attach) else "")
                lines += self._lines(text, user, True, msg.edited, msg.reply_to)
            elif isinstance(attach, immp.Location):
                text = "shared a location: {}".format(attach)
                lines += self._lines(text, user, True, msg.edited, msg.reply_to)
            elif isinstance(attach, immp.Message) and attach.text:
                lines += self._lines(attach.text, attach.user, attach.action, attach.edited,
                                     msg.reply_to, user)
        arrowed = False
        for i, line in enumerate(lines):
            if arrowed:
                lines[i] = line.replace(ARROW, "   ")
            elif ARROW in line:
                arrowed = True
        receipts = []
        if self.config["puppet"] and msg.user:
            client = await self._puppet(msg.user)
            if not await channel.is_private():
                await client.join(channel.source)
        else:
            client = self._client
        for text in lines:
            async with self._delay_lock:
                line = await client.send(channel.source, text)
            sent = await IRCMessage.from_line(self, line)
            self.queue(sent)
            receipts.append(sent)
        self._last_msgs[channel] = receipts
        if self.config["puppet"]:
            for member in msg.joined:
                puppet = await self._puppet(member, False)
                if puppet:
                    ensure_future(puppet.join(channel.source))
            for member in msg.left:
                puppet = await self._puppet(member, False)
                if puppet:
                    ensure_future(puppet.part(channel.source))
        return receipts
コード例 #10
0
ファイル: access.py プロジェクト: Terrance/IMMP
class ChannelAccessHook(immp.Hook, AccessPredicate):
    """
    Hook for controlling membership of, and joins to, secure channels.
    """

    schema = immp.Schema({immp.Optional("hooks", dict): {str: immp.Nullable([str])},
                          immp.Optional("exclude", dict): {str: [str]},
                          immp.Optional("joins", True): bool,
                          immp.Optional("startup", False): bool,
                          immp.Optional("passive", False): bool,
                          immp.Optional("default", True): bool})

    hooks = immp.ConfigProperty({AccessPredicate: [immp.Channel]})

    @property
    def channels(self):
        inverse = defaultdict(list)
        for hook, channels in self.hooks.items():
            if not channels:
                continue
            for channel in channels:
                inverse[channel].append(hook)
        return inverse

    # This hook acts as an example predicate to block all access.

    async def channel_access_multi(self, channels, users):
        return [], list(product(channels, users))

    async def channel_access(self, channel, user):
        return False

    async def verify(self, members=None):
        """
        Perform verification of each user in each channel, for all configured access predicates.
        Users who are denied access by any predicate will be removed, unless passive mode is set.

        Args:
            members ((.Channel, .User set) dict):
                Mapping from target channels to a subset of users pending verification.

                If ``None`` is given for a channel's set of users, all members present in the
                channel will be verified.  If ``members`` itself is ``None``, access checks will be
                run against all configured channels.
        """
        everywhere = set()
        grouped = {}
        for hook, scope in self.hooks.items():
            interested = await hook.access_channels()
            if scope and interested:
                log.debug("Hook %r using scope and own list", hook)
                wanted = set(interested).intersection(scope)
            elif scope or interested:
                log.debug("Hook %r using %s", hook, "scope" if scope else "own list")
                wanted = set(scope or interested)
            else:
                log.warning("Hook %r has no declared channels for access control", hook)
                continue
            if members is not None:
                wanted.intersection_update(members)
            if wanted:
                everywhere.update(wanted)
                grouped[hook] = wanted
            else:
                log.debug("Skipping hook %r as member filter doesn't overlap", hook)
        targets = defaultdict(set)
        members = members or {}
        for channel in everywhere:
            users = members.get(channel)
            try:
                current = await channel.members()
            except Exception:
                log.warning("Failed to retrieve members for channel %r", channel, exc_info=True)
                continue
            for user in users or current or ():
                if current and user not in current:
                    log.debug("Skipping non-member user %r", user)
                elif user.id in self.config["exclude"].get(user.plug.name, []):
                    log.debug("Skipping excluded user %r", user)
                elif await user.is_system():
                    log.debug("Skipping system user %r", user)
                else:
                    targets[channel].add(user)
        hooks = []
        tasks = []
        for hook, channels in grouped.items():
            known = {channel: users for channel, users in targets.items() if users}
            log.debug("Requesting decisions from %r: %r", hook, set(known))
            hooks.append(hook)
            tasks.append(ensure_future(hook.channel_access_multi(known)))
        allowed = set()
        denied = set()
        for hook, result in zip(hooks, await gather(*tasks, return_exceptions=True)):
            if isinstance(result, Exception):
                log.warning("Failed to verify channel access with hook %r",
                            hook.name, exc_info=result)
                continue
            hook_allowed, hook_denied = result
            allowed.update(hook_allowed)
            if hook_denied:
                log.debug("Hook %r denied %d user-channel pair(s)", hook.name, len(hook_denied))
                denied.update(hook_denied)
        removals = defaultdict(set)
        for channel, users in targets.items():
            for user in users:
                pair = (channel, user)
                if pair in denied:
                    allow = False
                elif pair in allowed:
                    allow = True
                else:
                    allow = self.config["default"]
                if allow:
                    log.debug("Allowing access to %r for %r", channel, user)
                else:
                    log.debug("Denying access to %r for %r", channel, user)
                    removals[channel].add(user)
        active = not self.config["passive"]
        for channel, refused in removals.items():
            log.debug("%s %d user(s) from %r: %r", "Removing" if active else "Would remove",
                      len(refused), channel, refused)
            if active:
                await channel.remove_multi(refused)

    async def _startup_check(self):
        log.debug("Running startup access checks")
        await self.verify()
        log.debug("Finished startup access checks")

    def on_ready(self):
        if self.config["startup"]:
            ensure_future(self._startup_check())

    async def on_receive(self, sent, source, primary):
        await super().on_receive(sent, source, primary)
        if self.config["joins"] and primary and sent == source and source.joined:
            await self.verify({sent.channel: source.joined})
コード例 #11
0
ファイル: sync.py プロジェクト: raveinid/IMMP
class _SyncHookBase(immp.Hook):

    schema = immp.Schema({"channels": {str: [str]},
                          immp.Optional("joins", False): bool,
                          immp.Optional("renames", False): bool,
                          immp.Optional("identities"): immp.Nullable(str),
                          immp.Optional("reset-author", False): bool,
                          immp.Optional("name-format"): immp.Nullable(str),
                          immp.Optional("strip-name-emoji", False): bool})

    _identities = immp.ConfigProperty(IdentityProvider)

    def _accept(self, msg):
        if not self.config["joins"] and (msg.joined or msg.left):
            log.debug("Not syncing join/part message: %r", msg.id)
            return False
        if not self.config["renames"] and msg.title:
            log.debug("Not syncing rename message: %r", msg.id)
            return False
        return True

    def _replace_recurse(self, msg, func, *args):
        # Switch out entire messages for copies or replacements.
        if msg.reply_to:
            msg.reply_to = func(msg.reply_to, *args)
        attachments = []
        for attach in msg.attachments:
            if isinstance(attach, immp.Message):
                attachments.append(func(attach, *args))
            else:
                attachments.append(attach)
        msg.attachments = attachments
        return msg

    async def _alter_recurse(self, msg, func, *args):
        # Alter properties on existing cloned message objects.
        await func(msg, *args)
        if msg.reply_to:
            await func(msg.reply_to, *args)
        for attach in msg.attachments:
            if isinstance(attach, immp.Message):
                await func(attach, *args)

    async def _rename_user(self, user, channel):
        # Use name-format or identities to render a suitable author real name.
        renamed = name = identity = None
        if not self.config["reset-author"]:
            renamed = user
        if self._identities:
            try:
                identity = await self._identities.identity_from_user(user)
            except Exception as e:
                log.warning("Failed to retrieve identity information for %r", user,
                            exc_info=e)
        if self.config["name-format"]:
            if not Template:
                raise immp.PlugError("'jinja2' module not installed")
            title = await channel.title() if channel else None
            context = {"user": user, "identity": identity, "channel": title}
            name = Template(self.config["name-format"]).render(**context)
            if not name and self.config["reset-author"]:
                user = None
        elif self.config["reset-author"]:
            user = None
        elif identity:
            name = "{} ({})".format(user.real_name or user.username, identity.name)
        elif self.config["strip-name-emoji"] and user:
            name = user.real_name or user.username
        if not name:
            return user
        if self.config["strip-name-emoji"]:
            if not EMOJI_REGEX:
                raise immp.PlugError("'emoji' module not installed")
            name = EMOJI_REGEX.sub(_emoji_replace, name).strip()
        if renamed:
            log.debug("Replacing real name: %r -> %r", renamed.real_name, name)
            renamed = copy(renamed)
            renamed.real_name = name
        else:
            log.debug("Adding real name: %r", name)
            renamed = immp.User(real_name=name)
        return renamed

    async def _alter_name(self, msg):
        channel = msg.channel if isinstance(msg, immp.Receipt) else None
        msg.user = await self._rename_user(msg.user, channel)

    async def _alter_identities(self, msg, channel):
        # Replace mentions for identified users in the target channel.
        if not msg.text:
            return
        msg.text = msg.text.clone()
        for segment in msg.text:
            user = segment.mention
            if not user or user.plug == channel.plug:
                # No mention or already matches plug, nothing to do.
                continue
            identity = None
            if self.config["identities"]:
                try:
                    identity = await self._identities.identity_from_user(user)
                except Exception as e:
                    log.warning("Failed to retrieve identity information for %r", user, exc_info=e)
            # Try to find an identity corresponding to the target plug.
            links = identity.links if identity else []
            for user in links:
                if user.plug == channel.plug:
                    log.debug("Replacing mention: %r -> %r", user, user)
                    segment.mention = user
                    break
            else:
                # Fallback case: replace mention with a link to the user's profile.
                if user.link:
                    log.debug("Adding fallback mention link: %r -> %r", user, user.link)
                    segment.link = user.link
                else:
                    log.debug("Removing foreign mention: %r", user)
                segment.mention = None
            # Perform name substitution on the mention text.
            if self.config["name-format"]:
                at = "@" if segment.text.startswith("@") else ""
                renamed = await self._rename_user(user, channel)
                segment.text = "{}{}".format(at, renamed.real_name)

    async def _send(self, channel, msg):
        try:
            ids = await channel.send(msg)
            log.debug("Synced IDs in %r: %r", channel, ids)
            return (channel, ids)
        except Exception:
            log.exception("Failed to relay message to channel: %r", channel)
            return (channel, [])
コード例 #12
0
ファイル: github.py プロジェクト: Terrance/IMMP
class _Schema:

    config = immp.Schema({
        "route": str,
        immp.Optional("secret"): immp.Nullable(str),
        immp.Optional("ignore", list): [str]
    })

    _linked = {"html_url": str}

    _sender = {"id": int, "login": str, "avatar_url": str}

    _repo = {"full_name": str}

    _project = {"name": str, "number": int, **_linked}
    _card = {"note": str, "url": str}

    _release = {
        "tag": str,
        immp.Optional("name"): immp.Nullable(str),
        **_linked
    }

    _issue = {"number": int, "title": str, **_linked}
    _pull = {immp.Optional("merged", False): bool, **_issue}

    _fork = {"full_name": str, **_linked}

    _page = {"action": str, "title": str, **_linked}

    push = immp.Schema({
        "ref":
        str,
        "after":
        str,
        "compare":
        str,
        immp.Optional("created", False):
        bool,
        immp.Optional("deleted", False):
        bool,
        immp.Optional("commits", list): [{
            "id": str,
            "message": str
        }]
    })

    event = immp.Schema({
        "sender": _sender,
        immp.Optional("organization"): immp.Nullable(_sender),
        immp.Optional("repository"): immp.Nullable(_repo),
        immp.Optional("project"): immp.Nullable(_project),
        immp.Optional("project_card"): immp.Nullable(_card),
        immp.Optional("release"): immp.Nullable(_release),
        immp.Optional("issue"): immp.Nullable(_issue),
        immp.Optional("pull_request"): immp.Nullable(_pull),
        immp.Optional("review"): immp.Nullable(_linked),
        immp.Optional("forkee"): immp.Nullable(_fork),
        immp.Optional("pages"): immp.Nullable([_page])
    })
コード例 #13
0
class AsyncShellHook(immp.ResourceHook):
    """
    Hook to launch an asynchonous console alongside a :class:`.Host` instance.

    Attributes:
        buffer (collections.deque):
            Queue of recent messages, the length defined by the ``buffer`` config entry.
        last ((.SentMessage, .Message) tuple):
            Most recent message received from a connected plug.
    """

    schema = immp.Schema({
        "bind": immp.Any(str, int),
        immp.Optional("buffer"): immp.Nullable(int)
    })

    def __init__(self, name, config, host):
        super().__init__(name, config, host)
        if not aioconsole:
            raise immp.PlugError("'aioconsole' module not installed")
        self.buffer = None
        self._server = None

    @property
    def last(self):
        return self.buffer[-1] if self.buffer else None

    async def start(self):
        await super().start()
        if self.config["buffer"] is not None:
            self.buffer = deque(maxlen=self.config["buffer"] or None)
        if isinstance(self.config["bind"], str):
            log.debug("Launching console on socket %s", self.config["bind"])
            bind = {"path": self.config["bind"]}
        else:
            log.debug("Launching console on port %d", self.config["bind"])
            bind = {"port": self.config["bind"]}
        self._server = await aioconsole.start_interactive_server(
            factory=self._factory, **bind)

    async def stop(self):
        await super().stop()
        self.buffer = None
        if self._server:
            log.debug("Stopping console server")
            self._server.close()
            self._server = None

    @staticmethod
    def _pprint(console, obj):
        console.print(pformat(obj))

    def _factory(self, streams=None):
        context = {"host": self.host, "shell": self, "immp": immp}
        console = aioconsole.AsynchronousConsole(locals=context,
                                                 streams=streams)
        context["pprint"] = partial(self._pprint, console)
        return console

    async def on_receive(self, sent, source, primary):
        await super().on_receive(sent, source, primary)
        if self.buffer is not None:
            self.buffer.append((sent, source))
コード例 #14
0
ファイル: sync.py プロジェクト: Terrance/IMMP
class _SyncHookBase(immp.Hook):

    _override_config = {
        immp.Optional("joins", immp.Optional.MISSING): bool,
        immp.Optional("renames", immp.Optional.MISSING): bool,
        immp.Optional("reset-author", immp.Optional.MISSING): bool,
        immp.Optional("name-format", immp.Optional.MISSING):
        immp.Nullable(str),
        immp.Optional("strip-name-emoji", immp.Optional.MISSING): bool
    }

    schema = immp.Schema({
        "channels": {
            str: [str]
        },
        immp.Optional("plugs", dict): {
            str: _override_config
        },
        immp.Optional("joins", False): bool,
        immp.Optional("renames", False): bool,
        immp.Optional("identities"): immp.Nullable(str),
        immp.Optional("reset-author", False): bool,
        immp.Optional("name-format"): immp.Nullable(str),
        immp.Optional("strip-name-emoji", False): bool
    })

    _identities = immp.ConfigProperty(IdentityProvider)

    def _plug_config(self, channel):
        keys = tuple(
            immp.Optional.unwrap(key)[0] for key in self._override_config)
        config = {key: self.config[key] for key in keys}
        if channel and channel.plug:
            override = self.config["plugs"].get(channel.plug.name) or {}
            config.update(
                {key: override[key]
                 for key in keys if key in override})
        return config

    def _accept(self, msg, id_):
        config = self._plug_config(
            msg.channel if isinstance(msg, immp.Receipt) else None)
        if not config["joins"] and (msg.joined or msg.left):
            log.debug("Not syncing join/part message: %r", id_)
            return False
        if not config["renames"] and msg.title:
            log.debug("Not syncing rename message: %r", id_)
            return False
        return True

    async def _replace_recurse(self, msg, func, *args):
        # Switch out entire messages for copies or replacements.
        if msg.reply_to:
            msg.reply_to = await func(msg.reply_to, *args)
        attachments = []
        for attach in msg.attachments:
            if isinstance(attach, immp.Message):
                attachments.append(await func(attach, *args))
            else:
                attachments.append(attach)
        msg.attachments = attachments
        return msg

    async def _alter_recurse(self, msg, func, *args):
        # Alter properties on existing cloned message objects.
        await func(msg, *args)
        if msg.reply_to:
            await func(msg.reply_to, *args)
        for attach in msg.attachments:
            if isinstance(attach, immp.Message):
                await func(attach, *args)

    async def _rename_user(self, user, channel):
        config = self._plug_config(channel)
        # Use name-format or identities to render a suitable author real name.
        base = (user.real_name or user.username) if user else None
        name = None
        identity = None
        force = False
        if user and self._identities:
            try:
                identity = await self._identities.identity_from_user(user)
            except Exception:
                log.warning("Failed to retrieve identity information for %r",
                            user,
                            exc_info=True)
        if config["name-format"]:
            if not Template:
                raise immp.PlugError("'jinja2' module not installed")
            title = await channel.title() if channel else None
            context = {"user": user, "identity": identity, "channel": title}
            try:
                name = Template(config["name-format"]).render(**context)
            except TemplateError:
                log.warning("Bad name format template", exc_info=True)
            else:
                # Remove the user's username, so that this name is always used.
                force = True
        elif identity:
            name = "{} ({})".format(base, identity.name)
        if config["strip-name-emoji"]:
            if not EMOJI_REGEX:
                raise immp.PlugError("'emoji' module not installed")
            current = name or base
            if current:
                name = EMOJI_REGEX.sub(_emoji_replace, current).strip()
        if not name:
            return user
        elif config["reset-author"] or not user:
            log.debug("Creating unlinked user with real name: %r", name)
            return immp.User(real_name=name,
                             suggested=(user.suggested if user else False))
        else:
            log.debug("Copying user with new real name: %r -> %r", user, name)
            return immp.User(id_=user.id,
                             plug=user.plug,
                             real_name=name,
                             username=(None if force else user.username),
                             avatar=user.avatar,
                             link=user.link,
                             suggested=user.suggested)

    async def _alter_name(self, msg):
        channel = msg.channel if isinstance(msg, immp.Receipt) else None
        msg.user = await self._rename_user(msg.user, channel)

    async def _alter_identities(self, msg, channel):
        # Replace mentions for identified users in the target channel.
        if not msg.text:
            return
        msg.text = msg.text.clone()
        for segment in msg.text:
            user = segment.mention
            if not user or user.plug == channel.plug:
                # No mention or already matches plug, nothing to do.
                continue
            identity = None
            if self.config["identities"]:
                try:
                    identity = await self._identities.identity_from_user(user)
                except Exception as e:
                    log.warning(
                        "Failed to retrieve identity information for %r",
                        user,
                        exc_info=e)
            # Try to find an identity corresponding to the target plug.
            links = identity.links if identity else []
            for user in links:
                if user.plug == channel.plug:
                    log.debug("Replacing mention: %r -> %r", user, user)
                    segment.mention = user
                    break
            # Perform name substitution on the mention text.
            config = self._plug_config(channel)
            if config["name-format"]:
                at = "@" if segment.text.startswith("@") else ""
                renamed = await self._rename_user(user, channel)
                segment.text = "{}{}".format(at, renamed.real_name)

    async def _send(self, channel, msg):
        try:
            receipts = await channel.send(msg)
            log.debug("Synced IDs in %r: %r", channel,
                      [receipt.id for receipt in receipts])
            return (channel, receipts)
        except Exception:
            log.exception("Failed to relay message to channel: %r", channel)
            return (channel, [])
コード例 #15
0
ファイル: slack.py プロジェクト: raveinid/IMMP
class _Schema:

    image_sizes = ("original", "512", "192", "72", "48", "32", "24")

    _images = {
        immp.Optional("image_{}".format(size)): immp.Nullable(str)
        for size in image_sizes
    }

    config = immp.Schema({
        "token": str,
        immp.Optional("fallback-name", "IMMP"): str,
        immp.Optional("fallback-image"): immp.Nullable(str),
        immp.Optional("thread-broadcast", False): bool
    })

    team = immp.Schema({
        "id": str,
        "name": str,
        "domain": str,
        "prefs": {
            immp.Optional("display_real_names", False): bool,
            str: immp.Any()
        }
    })

    user = immp.Schema({
        "id": str,
        "name": str,
        "profile": {
            immp.Optional("real_name"): immp.Nullable(str),
            immp.Optional("bot_id"): immp.Nullable(str),
            **_images
        }
    })

    bot = immp.Schema({
        "id": str,
        "app_id": str,
        "name": str,
        "icons": _images
    })

    channel = immp.Schema({"id": str, "name": str})

    direct = immp.Schema({"id": str, "user": str})

    _shares = {str: [{"ts": str}]}

    file = immp.Schema(
        immp.Any(
            {
                "id": str,
                "name": immp.Nullable(str),
                "pretty_type": str,
                "url_private": str,
                immp.Optional("mode"): immp.Nullable(str),
                immp.Optional("shares", dict): {
                    immp.Optional("public", dict): _shares,
                    immp.Optional("private", dict): _shares
                }
            }, {
                "id": str,
                "mode": "tombstone"
            }))

    attachment = immp.Schema({
        immp.Optional("fallback"): immp.Nullable(str),
        immp.Optional("title"): immp.Nullable(str),
        immp.Optional("image_url"): immp.Nullable(str),
        immp.Optional("is_msg_unfurl", False): bool
    })

    msg_unfurl = immp.Schema({"channel_id": str, "ts": str}, attachment)

    _base_msg = immp.Schema({
        "ts": str,
        "type": "message",
        immp.Optional("hidden", False): bool,
        immp.Optional("channel"): immp.Nullable(str),
        immp.Optional("edited", dict): {
            immp.Optional("user"): immp.Nullable(str)
        },
        immp.Optional("thread_ts"): immp.Nullable(str),
        immp.Optional("replies", list): [{
            "ts": str
        }],
        immp.Optional("files", list): [file],
        immp.Optional("attachments", list): [attachment],
        immp.Optional("is_ephemeral", False): bool
    })

    _plain_msg = immp.Schema(
        {
            immp.Optional("user"): immp.Nullable(str),
            immp.Optional("bot_id"): immp.Nullable(str),
            immp.Optional("username"): immp.Nullable(str),
            immp.Optional("icons", dict): dict,
            "text": str
        }, _base_msg)

    message = immp.Schema(
        immp.Any(
            immp.Schema({"subtype": "file_comment"}, _base_msg),
            immp.Schema({"subtype": "message_changed"}, _base_msg),
            immp.Schema({
                "subtype": "message_deleted",
                "deleted_ts": str
            }, _base_msg),
            immp.Schema(
                {
                    "subtype": immp.Any("channel_name", "group_name"),
                    "name": str
                }, _plain_msg),
            immp.Schema({immp.Optional("subtype"): immp.Nullable(str)},
                        _plain_msg)))

    # Circular references to embedded messages.
    message.raw.choices[1].raw.update({
        "message": message,
        "previous_message": message
    })

    event = immp.Schema(
        immp.Any(
            message, {
                "type": "team_pref_change",
                "name": "str",
                "value": immp.Any()
            }, {
                "type": immp.Any("team_join", "user_change"),
                "user": user
            }, {
                "type":
                immp.Any("channel_created", "channel_joined", "channel_rename",
                         "group_created", "group_joined", "group_rename"),
                "channel": {
                    "id": str,
                    "name": str
                }
            }, {
                "type": "im_created",
                "channel": {
                    "id": str
                }
            }, {
                "type": immp.Any("member_joined_channel",
                                 "member_left_channel"),
                "user": str,
                "channel": str
            }, {
                "type": "message",
                immp.Optional("subtype"): immp.Nullable(str)
            }, {"type": str}))

    def _api(nested={}):
        return immp.Schema(
            immp.Any(
                {
                    "ok": True,
                    immp.Optional("response_metadata", dict): {
                        immp.Optional("next_cursor", ""): str
                    },
                    **nested
                }, {
                    "ok": False,
                    "error": str
                }))

    rtm = _api({
        "url": str,
        "self": {
            "id": str
        },
        "team": {
            "id": str,
            "name": str,
            "domain": str
        },
        "users": [user],
        "channels": [channel],
        "groups": [channel],
        "ims": [direct],
        "bots": [{
            "id": str,
            "deleted": bool
        }]
    })

    im_open = _api({"channel": direct})

    members = _api({"members": [str]})

    post = _api({"ts": str})

    upload = _api({"file": file})

    history = _api({"messages": [message]})

    api = _api()
コード例 #16
0
class WebHook(immp.ResourceHook):
    """
    Hook that provides a generic webserver, which other hooks can bind routes to.

    Attributes:
        app (aiohttp.web.Application):
            Web application instance, used to add new routes.
    """

    schema = immp.Schema(
        immp.Any({
            immp.Optional("host"): immp.Nullable(str),
            "port": int
        }, {"path": str}))

    def __init__(self, name, config, host):
        super().__init__(name, config, host)
        self.app = web.Application()
        if aiohttp_jinja2:
            # Empty mapping by default, other hooks can add to this via add_loader().
            self._loader = PrefixLoader({})
            self._jinja = aiohttp_jinja2.setup(self.app, loader=self._loader)
            self._jinja.filters["json"] = json.dumps
            self._jinja.globals["immp"] = immp
            self._jinja.globals["host"] = self.host
        self._runner = web.AppRunner(self.app)
        self._site = None
        self._contexts = {}

    def context(self, prefix, module, path=None, env=None):
        """
        Retrieve a context for the current module.

        Args:
            prefix (str):
                URL prefix acting as the base path.
            module (str):
                Dotted module name of the Python module using this context.  Callers should use
                :data:`__name__` from the root of their module.
            path (str):
                Base path of the module, needed for static routes.  Callers should use
                ``os.path.dirname(__file__)`` from the root of their module.
            env (dict):
                Additional variables to make available in the Jinja context.  See
                :attr:`.WebContext.env` for details.

        Returns:
            .WebContext:
                Linked context instance for that module.
        """
        self._contexts[module] = WebContext(self, prefix, module, path, env)
        return self._contexts[module]

    def add_loader(self, module):
        """
        Register a Jinja2 package loader for the given module.

        Args:
            module (str):
                Module name to register.
        """
        if not aiohttp_jinja2:
            raise immp.HookError("Loaders require Jinja2 and aiohttp_jinja2")
        self._loader.mapping[module] = PackageLoader(module)

    def add_route(self, *args, **kwargs):
        """
        Equivalent to :meth:`aiohttp.web.UrlDispatcher.add_route`.
        """
        return self.app.router.add_route(*args, **kwargs)

    def add_static(self, *args, **kwargs):
        """
        Equivalent to :meth:`aiohttp.web.UrlDispatcher.add_static`.
        """
        return self.app.router.add_static(*args, **kwargs)

    async def start(self):
        await super().start()
        await self._runner.setup()
        if "path" in self.config:
            log.debug("Starting server on socket %s", self.config["path"])
            self._site = web.UnixSite(self._runner, self.config["path"])
        else:
            log.debug("Starting server on host %s:%d", self.config["host"],
                      self.config["port"])
            self._site = web.TCPSite(self._runner, self.config["host"],
                                     self.config["port"])
        await self._site.start()

    async def stop(self):
        await super().stop()
        if self._site:
            log.debug("Stopping server")
            await self._runner.cleanup()
            self._site = None
コード例 #17
0
ファイル: sync.py プロジェクト: raveinid/IMMP
class SyncHook(_SyncHookBase):
    """
    Hook to propagate messages between two or more channels.

    Attributes:
        plug (.SyncPlug):
            Virtual plug for this sync, if configured.
    """

    schema = immp.Schema({immp.Optional("joins", True): bool,
                          immp.Optional("renames", True): bool,
                          immp.Optional("plug"): immp.Nullable(str),
                          immp.Optional("titles", dict): {str: str}}, _SyncHookBase.schema)

    def __init__(self, name, config, host):
        super().__init__(name, config, host)
        self.db = None
        # Message cache, stores IDs of all synced messages by channel.
        self._cache = SyncCache(self)
        # Hook lock, to put a hold on retrieving messages whilst a send is in progress.
        self._lock = BoundedSemaphore()
        # Add a virtual plug to the host, for external subscribers.
        if self.config["plug"]:
            log.debug("Creating virtual plug: %r", self.config["plug"])
            self.plug = SyncPlug(self.config["plug"], self, host)
            host.add_plug(self.plug)
            for label in self.config["channels"]:
                host.add_channel(label, immp.Channel(self.plug, label))
        else:
            self.plug = None

    @property
    def channels(self):
        try:
            return {virtual: [self.host.channels[label] for label in labels]
                    for virtual, labels in self.config["channels"].items()}
        except KeyError as e:
            raise immp.ConfigError("No channel {} on host".format(repr(e.args[0]))) from None

    def label_for_channel(self, channel):
        labels = []
        for label, channels in self.channels.items():
            if channel in channels:
                labels.append(label)
        if not labels:
            raise immp.ConfigError("Channel {} not bridged".format(repr(channel)))
        elif len(labels) > 1:
            raise immp.ConfigError("Channel {} defined more than once".format(repr(channel)))
        else:
            return labels[0]

    async def start(self):
        try:
            self.db = self.host.resources[DatabaseHook].db
        except KeyError:
            pass
        else:
            self.db.create_tables([SyncBackRef], safe=True)

    async def stop(self):
        self.db = None

    def _test(self, channel, user):
        return any(channel in channels for channels in self.channels.values())

    @command("sync-members", test=_test)
    async def members(self, msg):
        """
        List all members of the current conversation, across all channels.
        """
        members = defaultdict(list)
        missing = False
        for synced in self.channels[msg.channel.source]:
            local = (await synced.members())
            if local:
                members[synced.plug.network_name] += local
            else:
                missing = True
        if not members:
            return
        text = immp.RichText([immp.Segment("Members of this conversation:")])
        for network in sorted(members):
            text.append(immp.Segment("\n{}".format(network), bold=True))
            for member in sorted(members[network],
                                 key=lambda member: member.real_name or member.username):
                name = member.real_name or member.username
                text.append(immp.Segment("\n"))
                if member.link:
                    text.append(immp.Segment(name, link=member.link))
                elif member.real_name and member.username:
                    text.append(immp.Segment("{} [{}]".format(name, member.username)))
                else:
                    text.append(immp.Segment(name))
        if missing:
            text.append(immp.Segment("\n"),
                        immp.Segment("(list may be incomplete)"))
        await msg.channel.send(immp.Message(user=immp.User(real_name="Sync"), text=text))

    @command("sync-list", test=_test)
    async def list(self, msg):
        """
        List all channels connected to this conversation.
        """
        text = immp.RichText([immp.Segment("Channels in this sync:")])
        for synced in self.channels[msg.channel.source]:
            text.append(immp.Segment("\n{}".format(synced.plug.network_name)))
            title = await synced.title()
            if title:
                text.append(immp.Segment(": {}".format(title)))
        await msg.channel.send(immp.Message(user=immp.User(real_name="Sync"), text=text))

    async def send(self, label, msg, origin=None, ref=None):
        """
        Send a message to all channels in this sync.

        Args:
            label (str):
                Bridge that defines the underlying synced channels to send to.
            msg (.Message):
                External message to push.  This should be the source copy when syncing a message
                from another channel.
            origin (.Receipt):
                Raw message that triggered this sync; if set and part of the sync, it will be
                skipped (used to avoid retransmitting a message we just received).  This should be
                the plug-native copy of a message when syncing from another channel.
            ref (.SyncRef):
                Existing sync reference, if message has been partially synced.
        """
        base = immp.Message(text=msg.text, user=msg.user, edited=msg.edited, action=msg.action,
                            reply_to=msg.reply_to, joined=msg.joined, left=msg.left,
                            title=msg.title, attachments=msg.attachments, raw=msg)
        queue = []
        for synced in self.channels[label]:
            if origin and synced == origin.channel:
                continue
            elif ref and ref.ids[synced]:
                log.debug("Skipping already-synced target channel %r: %r", synced, ref)
                continue
            local = base.clone()
            self._replace_recurse(local, self._replace_ref, synced)
            await self._alter_recurse(local, self._alter_identities, synced)
            await self._alter_recurse(local, self._alter_name)
            queue.append(self._send(synced, local))
        # Just like with plugs, when sending a new (external) message to all channels in a sync, we
        # need to wait for all plugs to complete and have their IDs cached before processing any
        # further messages.
        async with self._lock:
            ids = dict(await gather(*queue))
            if ref:
                ref.ids.update(ids)
            else:
                ref = SyncRef(ids, source=msg, origin=origin)
            self._cache.add(ref)
        # Push a copy of the message to the sync channel, if running.
        if self.plug:
            sent = immp.SentMessage(id_=ref.key, channel=immp.Channel(self.plug, label),
                                    text=msg.text, user=msg.user, action=msg.action,
                                    reply_to=msg.reply_to, joined=msg.joined, left=msg.left,
                                    title=msg.title, attachments=msg.attachments, raw=msg)
            self.plug.queue(sent)
        return ref

    async def delete(self, ref, sent=None):
        queue = []
        for channel, ids in ref.ids.items():
            for id_ in ids:
                if not (sent and sent.channel == channel and sent.id == id_):
                    queue.append(immp.Receipt(id_, channel).delete())
        if queue:
            await gather(*queue)

    def _replace_ref(self, msg, channel):
        if not isinstance(msg, immp.Receipt):
            log.debug("Not replacing non-receipt message: %r", msg)
            return msg
        base = None
        if isinstance(msg, immp.SentMessage):
            base = immp.Message(text=msg.text, user=msg.user, action=msg.action,
                                reply_to=msg.reply_to, joined=msg.joined, left=msg.left,
                                title=msg.title, attachments=msg.attachments, raw=msg.raw)
        try:
            ref = self._cache[msg]
        except KeyError:
            log.debug("No match for source message: %r", msg)
            return base
        # Given message was a resync of the source message from a synced channel.
        if ref.ids.get(channel):
            log.debug("Found reference to previously synced message: %r", ref.key)
            at = ref.source.at if isinstance(ref.source, immp.Receipt) else None
            best = ref.source or msg
            return immp.SentMessage(id_=ref.ids[channel][0], channel=channel, at=at,
                                    text=best.text, user=best.user, action=best.action,
                                    reply_to=best.reply_to, joined=best.joined, left=best.left,
                                    title=best.title, attachments=best.attachments, raw=best.raw)
        elif channel.plug == msg.channel.plug:
            log.debug("Referenced message has origin plug, not modifying: %r", msg)
            return msg
        else:
            log.debug("Origin message not referenced in the target channel: %r", msg)
            return base

    async def on_receive(self, sent, source, primary):
        await super().on_receive(sent, source, primary)
        try:
            label = self.label_for_channel(sent.channel)
        except immp.ConfigError:
            return
        async with self._lock:
            # No critical section here, just wait for any pending messages to be sent.
            pass
        ref = None
        try:
            ref = self._cache[sent]
        except KeyError:
            if sent.deleted:
                log.debug("Ignoring deleted message not in sync cache: %r", sent)
                return
            else:
                log.debug("Incoming message not in sync cache: %r", sent)
        else:
            if sent.deleted:
                log.debug("Incoming message is a delete, needs sync: %r", sent)
                await self.delete(ref)
                return
            elif (sent.edited and not ref.revisions) or ref.revision(sent):
                log.debug("Incoming message is an update, needs sync: %r", sent)
            elif all(ref.ids[channel] for channel in self.channels[label]):
                log.debug("Incoming message already synced: %r", sent)
                return
            else:
                log.debug("Incoming message partially synced: %r", sent)
        if not self._accept(source):
            return
        log.debug("Sending message to synced channel %r: %r", label, sent.id)
        await self.send(label, source, sent, ref)