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 _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
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))
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)
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))
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)
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_)
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
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
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)
def from_id(cls, irc, id_): nick = id_.split("!", 1)[0] return immp.User(id_=id_, plug=irc, username=nick, raw=id_)