Exemple #1
0
 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)
Exemple #2
0
 async def _parse_author(cls, slack, event=None, author=None):
     user = None
     if author:
         pass
     elif not event:
         raise TypeError("Need either event or author")
     elif event["user"]:
         author = event["user"]
     elif event["bot_id"] in slack._bot_to_user:
         # Event has the bot's app ID, not user ID.
         author = slack._bot_to_user[event["bot_id"]]
     elif event["bot_id"] in slack._bots:
         # Slack app with no bot presence, use the app metadata.
         user = slack._bots[event["bot_id"]]
     if author:
         user = await slack.user_from_id(author) or SlackUser(id_=author,
                                                              plug=slack)
     if event["username"]:
         if user:
             user = copy(user)
             user.real_name = event["username"]
         else:
             user = immp.User(real_name=event["username"])
         icon = SlackUser._best_image(event["icons"])
         if icon:
             user.avatar = icon
     return user
Exemple #3
0
 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))
Exemple #4
0
 def from_who(cls, irc, line):
     id_ = "{}!{}@{}".format(line.args[5], line.args[2], line.args[3])
     username = line.args[5]
     real_name = line.args[-1].split(" ", 1)[-1]
     return immp.User(id_=id_,
                      plug=irc,
                      username=username,
                      real_name=real_name,
                      raw=line)
Exemple #5
0
 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))
Exemple #6
0
    def from_whois(cls, irc, line):
        """
        Convert the response of a ``WHOIS`` query into a :class:`.User`.

        Args:
            irc (.IRCPlug):
                Related plug instance that provides the user.
            line (.Line):
                311-numeric line containing a user's nick, host and real name.

        Returns:
            .User:
                Parsed user object.
        """
        id_ = "{}!{}@{}".format(line.args[1], line.args[2], line.args[3])
        username = line.args[1]
        real_name = line.args[-1]
        return immp.User(id_=id_, plug=irc, username=username, real_name=real_name, raw=line)
Exemple #7
0
    def from_id(cls, irc, id_, real_name=None):
        """
        Extract the nick from a nickmask into a :class:`.User`.

        Args:
            irc (.IRCPlug):
                Related plug instance that provides the user.
            id_ (str):
                Nickmask of the target user.
            real_name (str):
                Display name of the user, if known.

        Returns:
            .User:
                Parsed user object.
        """
        nick = id_.split("!", 1)[0]
        return immp.User(id_=id_, plug=irc, username=nick, real_name=real_name, raw=id_)
Exemple #8
0
 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
Exemple #9
0
 def __init__(self, name, config, host):
     super().__init__(name, config, host)
     self.counter = immp.IDGen()
     self.user = immp.User(id_="dummy", real_name=name)
     self.channel = immp.Channel(self, "dummy")
     self._task = None
Exemple #10
0
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("edits", True): bool,
            immp.Optional("joins", True): bool,
            immp.Optional("renames", True): bool,
            immp.Optional("plug"): immp.Nullable(str),
            immp.Optional("titles", dict): {
                str: str
            }
        }, _SyncHookBase.schema)

    user = immp.User(real_name="Sync", suggested=True)

    def __init__(self, name, config, host):
        super().__init__(name, config, host)
        # 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
        self._db = False

    def on_load(self):
        try:
            self.host.resources[AsyncDatabaseHook].add_models(SyncBackRef)
        except KeyError:
            self._db = False
        else:
            self._db = True

    channels = immp.ConfigProperty({None: [immp.Channel]})

    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]

    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=self.user, 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=self.user, text=text))

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

        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.
            update (bool):
                ``True`` to force resending an updated message to all synced channels.
        """
        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 not update and ref and ref.ids[synced]:
                log.debug("Skipping already-synced target channel %r: %r",
                          synced, ref)
                continue
            local = base.clone()
            await 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:
            all_receipts = dict(await gather(*queue))
            ids = {
                channel: [receipt.id for receipt in receipts]
                for channel, receipts in all_receipts.items()
            }
            if ref:
                ref.ids.update(ids)
            else:
                ref = SyncRef(ids, source=msg, origin=origin)
            await 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)

    async 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 = await self._cache.get(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)
        if not primary or not self._accept(source, sent.id):
            return
        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
        update = False
        try:
            ref = await self._cache.get(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:
                if self.config["edits"]:
                    log.debug("Incoming message is a delete, needs sync: %r",
                              sent)
                    await self.delete(ref)
                else:
                    log.debug("Ignoring deleted message: %r", sent)
                return
            elif (sent.edited and not ref.revisions) or ref.revision(sent):
                if self.config["edits"]:
                    log.debug("Incoming message is an update, needs sync: %r",
                              sent)
                    update = True
                else:
                    log.debug("Ignoring updated message: %r", sent)
                    return
            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)
        log.debug("Sending message to synced channel %r: %r", label, sent.id)
        await self.send(label, source, sent, ref, update)
Exemple #11
0
 def from_id(cls, irc, id_):
     nick = id_.split("!", 1)[0]
     return immp.User(id_=id_, plug=irc, username=nick, raw=id_)