async def channel_for_user(self, user): if not isinstance(user, SlackUser): return for direct in self._directs.values(): if direct["user"] == user.id: return immp.Channel(self, direct["id"]) # Private channel doesn't exist yet or isn't cached. params = {"user": user.id, "return_im": "true"} opened = await self._api("im.open", _Schema.im_open, params=params) return immp.Channel(self, opened["channel"]["id"])
def from_event(cls, github, type_, id_, event): """ Convert a `GitHub webhook <https://developer.github.com/webhooks/>`_ payload to a :class:`.Message`. Args: github (.GitHubPlug): Related plug instance that provides the event. type (str): Event type name from the ``X-GitHub-Event`` header. id (str): GUID of the event delivery from the ``X-GitHub-Delivery`` header. event (dict): GitHub webhook payload. Returns: .GitHubMessage: Parsed message object. """ text = None if event["repository"]: channel = immp.Channel(github, event["repository"]["full_name"]) text = cls._repo_text(github, type_, event) if not text: raise NotImplementedError user = GitHubUser.from_sender(github, event["sender"]) return immp.SentMessage(id_=id_, channel=channel, text=text, user=user, action=True, raw=event)
async def channel_for_user(self, user): if not isinstance(user, DiscordUser): return None if not isinstance(user.raw, (discord.Member, discord.User)): return None dm = user.raw.dm_channel or (await user.raw.create_dm()) return immp.Channel(self, dm.id)
def from_backref_map(cls, key, mapped, host): """ Take a mapping generated in :meth:`.SyncBackRef.map_from_key` and produce a local reference suitable for the memory cache. Args: key (str): Synced message identifier. mapped (((str, str), .SyncBackRef list) dict): Generated reference mapping. host (.Host): Parent host instance, needed to resolve network IDs to plugs. Returns: .SyncRef: Newly created reference. """ ids = {} for (network, source), synced in mapped.items(): for plug in host.plugs.values(): if plug.network_id == network: ids[immp.Channel(plug, source)] = [ backref.message for backref in synced ] return cls(ids, key=key)
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 from_line(cls, irc, line): """ Convert a :class:`.Line` into a :class:`.Message`. Args: irc (.IRCPlug): Related plug instance that provides the line. line (.Line): Raw message line from the server. Returns: .IRCMessage: Parsed message object. """ channel = line.args[0] nick = line.source.split("!", 1)[0] if channel == irc.config["user"]["nick"]: # Private messages arrive "from A to B", and should be sent "from B to A". channel = nick user = IRCUser.from_id(irc, line.source) action = False joined = [] left = [] if line.command == "JOIN": text = "joined {}".format(channel) action = True joined.append(user) elif line.command == "PART": text = "left {}".format(channel) action = True left.append(user) elif line.command == "KICK": target = await irc.user_from_username(line.args[1]) text = immp.RichText([ immp.Segment("kicked "), immp.Segment(target.username, bold=True), immp.Segment(" ({})".format(line.args[2])) ]) action = True left.append(target) elif line.command == "PRIVMSG": text = line.args[1] match = re.match(r"\x01ACTION ([^\x01]*)\x01", text) if match: text = match.group(1) action = True else: raise NotImplementedError return immp.SentMessage(id_=Line.next_ts(), channel=immp.Channel(irc, channel), text=text, user=user, action=action, joined=joined, left=left, raw=line)
async def channel_for_user(self, user): for channel in self._filter_channels( hangouts_pb2.CONVERSATION_TYPE_ONE_TO_ONE): if any(part.id == user.id for part in await channel.members()): return channel request = hangouts_pb2.CreateConversationRequest( request_header=self._client.get_request_header(), type=hangouts_pb2.CONVERSATION_TYPE_ONE_TO_ONE, client_generated_id=self._client.get_client_generated_id(), invitee_id=[hangouts_pb2.InviteeID(gaia_id=user.id)]) response = await self._client.create_conversation(request) return immp.Channel(self, response.conversation.conversation_id.id)
def _resolve_channel(self, request): try: if "name" in request.match_info: name = request.match_info["name"] return name, self.host.channels[name] elif "plug" in request.match_info: plug = self.host.plugs[request.match_info["plug"]] return None, immp.Channel(plug, request.match_info["source"]) else: raise web.HTTPBadRequest except KeyError: raise web.HTTPNotFound
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_migration(self, request): _, old = self._resolve_channel(request) post = await request.post() if "name" in post: new = self.host.channels[post["name"]] elif "plug" in post and "source" in post: new = immp.Channel(self.host.plugs[post["plug"]], post["source"]) else: raise web.HTTPBadRequest await self.host.channel_migrate(old, new) raise web.HTTPFound( self.ctx.url_for("channel", plug=old.plug.name, source=old.source))
def from_message(cls, discord_, message, edited=False, deleted=False): """ Convert a :class:`discord.Message` into a :class:`.Message`. Args: discord (.DiscordPlug): Related plug instance that provides the event. message (discord.Message): Discord message object received from a channel. edited (bool): Whether this message comes from an edit event. deleted (bool): Whether this message comes from a delete event. Returns: .DiscordMessage: Parsed message object. """ text = None channel = immp.Channel(discord_, message.channel.id) user = DiscordUser.from_user(discord_, message.author) attachments = [] if message.content: text = DiscordRichText.from_markdown(discord_, message.content, channel) for attach in message.attachments: if attach.filename.endswith((".jpg", ".png", ".gif")): type_ = immp.File.Type.image elif attach.filename.endswith((".mp4", ".webm")): type_ = immp.File.Type.video else: type_ = immp.File.Type.unknown attachments.append(immp.File(title=attach.filename, type_=type_, source=attach.url)) for embed in message.embeds: if embed.image.url and embed.image.url.rsplit(".", 1)[1] in ("jpg", "png", "gif"): attachments.append(immp.File(type_=immp.File.Type.image, source=embed.image.url)) return immp.SentMessage(id_=message.id, channel=channel, # Timestamps are naive but in UTC. at=message.created_at.replace(tzinfo=timezone.utc), # Edited timestamp is blank for new messages, but updated in # existing objects when the message is later edited. revision=(message.edited_at or message.created_at).timestamp(), edited=edited, deleted=deleted, text=text, user=user, attachments=attachments, raw=message)
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 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 from_event(cls, slack, json, parent=True): """ Convert an API event :class:`dict` to a :class:`.Message`. Args: slack (.SlackPlug): Related plug instance that provides the event. json (dict): Slack API `message <https://api.slack.com/events/message>`_ event data. parent (bool): ``True`` (default) to retrieve the thread parent if one exists. Returns: .SlackMessage: Parsed message object. """ event = _Schema.message(json) if event["hidden"]: # Ignore UI-hidden events (e.g. tombstones of deleted files). raise NotImplementedError if event["is_ephemeral"]: # Ignore user-private messages from Slack (e.g. over quota warnings, link unfurling # opt-in prompts etc.) which shouldn't be served to message processors. raise NotImplementedError if event["subtype"] == "file_comment": # Deprecated in favour of file threads, but Slack may still emit these. raise NotImplementedError channel = immp.Channel(slack, event["channel"]) if event["subtype"] == "message_deleted": id_, at = cls._parse_meta(slack, event) return immp.SentMessage(id_=id_, channel=channel, at=at, revision=event["ts"], deleted=True, raw=json) elif event["subtype"] == "message_changed": if event["message"]["hidden"]: # In theory this should match event["hidden"], but redefined here just in case. raise NotImplementedError if event["message"]["text"] == event["previous_message"]["text"]: # Message remains unchanged. Can be caused by link unfurling (adds an attachment) # or deleting replies (reply is removed from event.replies in new *and old*). raise NotImplementedError revision = event["ts"] # Original message details are under a nested "message" key. return await cls._parse_main(slack, json, event["message"], channel, parent, revision) else: return await cls._parse_main(slack, json, event, channel, parent)
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
def sync_for(self, channel): """ Produce a synced channel for the given source. Args: channel (.Channel): Original channel to lookup. Returns: .Channel: Sync channel containing the given channel as a source, or ``None`` if not synced. """ for label, synced in self._hook.channels.items(): if channel in synced: return immp.Channel(self, label) return None
async def named_channel_add(self, request): post = await request.post() try: plug = post["plug"] name = post["name"] source = post["source"] except KeyError: raise web.HTTPBadRequest if not (plug and name and source): raise web.HTTPBadRequest if name in self.host: raise web.HTTPConflict if plug not in self.host.plugs: raise web.HTTPNotFound self.host.add_channel(name, immp.Channel(self.host.plugs[plug], source)) raise web.HTTPFound(self.ctx.url_for("plug", name=plug))
async def commands(self, channel, user): """ Retrieve all commands, and filter against the mappings. Args: channel (.Channel): Source channel where the command will be executed. user (.User): Author of the message to trigger the command. Returns: (str, .BoundCommand) dict: Commands provided by all hooks, in this channel for this user, keyed by name. """ log.debug("Collecting commands for %r in %r", channel, user) if isinstance(channel, immp.Plug): # Look for commands for a generic channel. plug = channel channel = immp.Channel(plug, "") private = False else: plug = None private = await channel.is_private() mappings = [] for mapping in self.config["mapping"].values(): for label in mapping["groups"]: group = self.host.groups[label] if plug and group.has_plug(plug, "anywhere", "named"): mappings.append(mapping) elif not plug and await group.has_channel(channel): mappings.append(mapping) cmds = set() for mapping in mappings: cmds.update(self._mapping_cmds(mapping, channel, user, private)) mapped = {cmd.name: cmd for cmd in cmds} if len(cmds) > len(mapped): # Mapping by name silently overwrote at least one command with a duplicate name. raise immp.ConfigError( "Multiple applicable commands with the same name") return mapped
def config_to_host(config, path, write): host = immp.Host() for name, spec in config["plugs"].items(): cls = immp.resolve_import(spec["path"]) host.add_plug(cls(name, spec["config"], host), spec["enabled"]) for name, spec in config["channels"].items(): plug = host.plugs[spec["plug"]] host.add_channel(name, immp.Channel(plug, spec["source"])) for name, group in config["groups"].items(): host.add_group(immp.Group(name, group, host)) for name, spec in config["hooks"].items(): cls = immp.resolve_import(spec["path"]) host.add_hook(cls(name, spec["config"], host), spec["enabled"], spec["priority"]) try: host.add_hook(RunnerHook("runner", {}, host)) except immp.ConfigError: # Prefer existing hook defined within the config itself. pass host.resources[RunnerHook].load(config, path, write) host.loaded() return host
async def channel_for_user(self, user): return immp.Channel(self, user.username)
async def private_channels(self): return [immp.Channel(self, channel.id) for channel in self._client.private_channels]
async def public_channels(self): return [immp.Channel(self, channel.id) for channel in self._client.get_all_channels() if isinstance(channel, discord.TextChannel)]
async def private_channels(self): return [immp.Channel(self, id_) for id_ in self._directs]
def from_event(cls, github, type_, id_, event): """ Convert a `GitHub webhook <https://developer.github.com/webhooks/>`_ payload to a :class:`.Message`. Args: github (.GitHubPlug): Related plug instance that provides the event. type (str): Event type name from the ``X-GitHub-Event`` header. id (str): GUID of the event delivery from the ``X-GitHub-Delivery`` header. event (dict): GitHub webhook payload. Returns: .GitHubMessage: Parsed message object. """ repo = event["repository"]["full_name"] channel = immp.Channel(github, repo) user = GitHubUser.from_sender(github, event["sender"]) text = None if type_ == "push": count = len(event["commits"]) desc = "{} commits".format( count) if count > 1 else event["after"][:7] ref = event["ref"].split("/")[1:] target = "/".join(ref[1:]) if ref[0] == "tags": action, join = "tagged", "as" elif ref[0] == "heads": action, join = "pushed", "to" else: raise NotImplementedError text = immp.RichText([ immp.Segment("{} ".format(action)), immp.Segment(desc, link=event["compare"]), immp.Segment(" {} {} {}".format(join, repo, target)) ]) for commit in event["commits"]: text.append( immp.Segment("\n* "), immp.Segment(commit["id"][:7], code=True), immp.Segment(" - {}".format( commit["message"].split("\n")[0]))) elif type_ == "release": release = event["release"] desc = ("{} ({} {})".format(release["name"], repo, release["tag_name"]) if release["name"] else release["tag_name"]) text = immp.RichText([ immp.Segment("{} release ".format(event["action"])), immp.Segment(desc, link=release["html_url"]) ]) elif type_ == "issues": issue = event["issue"] desc = "{} ({}#{})".format(issue["title"], repo, issue["number"]) text = immp.RichText([ immp.Segment("{} issue ".format(event["action"])), immp.Segment(desc, link=issue["html_url"]) ]) elif type_ == "issue_comment": issue = event["issue"] comment = event["comment"] desc = "{} ({}#{})".format(issue["title"], repo, issue["number"]) text = immp.RichText([ immp.Segment("{} a ".format(event["action"])), immp.Segment("comment", link=comment["html_url"]), immp.Segment(" on issue "), immp.Segment(desc, link=issue["html_url"]) ]) elif type_ == "pull_request": pull = event["pull_request"] desc = "{} ({}#{})".format(pull["title"], repo, pull["number"]) text = immp.RichText([ immp.Segment("{} pull request ".format(event["action"])), immp.Segment(desc, link=pull["html_url"]) ]) elif type_ == "pull_request_review": pull = event["pull_request"] review = event["review"] desc = "{} ({}#{})".format(pull["title"], repo, pull["number"]) text = immp.RichText([ immp.Segment("{} a ".format(event["action"])), immp.Segment("review", link=review["html_url"]), immp.Segment(" on pull request "), immp.Segment(desc, link=pull["html_url"]) ]) elif type_ == "pull_request_review_comment": pull = event["pull_request"] comment = event["comment"] desc = "{} ({}#{})".format(pull["title"], repo, pull["number"]) text = immp.RichText([ immp.Segment("{} a ".format(event["action"])), immp.Segment("comment", link=comment["html_url"]), immp.Segment(" on pull request "), immp.Segment(desc, link=pull["html_url"]) ]) elif type_ == "project": project = event["project"] desc = "{} ({}#{})".format(project["name"], repo, project["number"]) text = immp.RichText([ immp.Segment("{} project ".format(event["action"])), immp.Segment(desc, link=project["html_url"]) ]) elif type_ == "project_card": card = event["project_card"] text = immp.RichText([ immp.Segment("{} ".format(event["action"])), immp.Segment("card", link=card["html_url"]), immp.Segment(" in project:\n"), immp.Segment(card["note"]) ]) elif type_ == "gollum": text = immp.RichText() for i, page in enumerate(event["pages"]): if i: text.append(immp.Segment(", ")) text.append( immp.Segment("{} {} wiki page ".format( page["action"], repo)), immp.Segment(page["title"], link=page["html_url"])) elif type_ == "fork": fork = event["forkee"] text = immp.RichText([ immp.Segment("forked {} to ".format(repo)), immp.Segment(fork["full_name"], link=fork["html_url"]) ]) elif type_ == "watch": text = immp.RichText([immp.Segment("starred {}".format(repo))]) if text: return immp.SentMessage(id_=id_, channel=channel, text=text, user=user, action=True, raw=event) else: raise NotImplementedError
async def from_event(cls, hangouts, event): """ Convert a :class:`hangups.ChatMessageEvent` into a :class:`.Message`. Args: hangouts (.HangoutsPlug): Related plug instance that provides the event. event (hangups.ChatMessageEvent): Hangups message event emitted from a conversation. Returns: .HangoutsMessage: Parsed message object. """ user = HangoutsUser.from_user(hangouts, hangouts._users.get_user(event.user_id)) action = False joined = None left = None title = None attachments = [] if isinstance(event, hangups.ChatMessageEvent): segments = [ HangoutsSegment.from_segment(segment) for segment in event.segments ] text = immp.RichText(segments) if any(a.type == 4 for a in event._event.chat_message.annotation): # This is a /me message sent from desktop Hangouts. action = True # The user's first name prefixes the message text, so try to strip that. if user.real_name: # We don't have a clear-cut first name, so try to match parts of names. # Try the full name first, then split successive words off the end. parts = user.real_name.split() start = text[0].text for pos in range(len(parts), 0, -1): sub_name = " ".join(parts[:pos]) if start.startswith(sub_name): text[0].text = start[len(sub_name) + 1:] break else: # Couldn't match the user's name to the message text. pass for attach in event._event.chat_message.message_content.attachment: embed = attach.embed_item if any(place in embed.type for place in (hangouts_pb2.ITEM_TYPE_PLACE, hangouts_pb2.ITEM_TYPE_PLACE_V2)): location = HangoutsLocation.from_embed(embed) if str(text) == ( "https://maps.google.com/maps?q={0},{1}".format( location.latitude, location.longitude)): text = None attachments.append(location) elif hangouts_pb2.ITEM_TYPE_PLUS_PHOTO in embed.type: attachments.append(await HangoutsFile.from_embed( hangouts, embed)) elif isinstance(event, hangups.MembershipChangeEvent): action = True is_join = event.type_ == hangouts_pb2.MEMBERSHIP_CHANGE_TYPE_JOIN parts = [ HangoutsUser.from_user(hangouts, hangouts._users.get_user(part_id)) for part_id in event.participant_ids ] if len(parts) == 1 and parts[0].id == user.id: # Membership event is a user acting on themselves. segments = [ HangoutsSegment("{} the hangout".format( "joined" if is_join else "left")) ] else: segments = [ HangoutsSegment("added " if is_join else "removed ") ] for part in parts: link = "https://hangouts.google.com/chat/person/{}".format( part.id) segments.append( HangoutsSegment(part.real_name, bold=True, link=link)) segments.append(HangoutsSegment(", ")) # Replace trailing comma. segments[-1].text = " {} the hangout".format( "to" if is_join else "from") if is_join: joined = parts else: left = parts elif isinstance(event, hangups.OTREvent): action = True is_history = (event.new_otr_status == hangouts_pb2.OFF_THE_RECORD_STATUS_ON_THE_RECORD) segments = [ HangoutsSegment("{}abled hangout message history".format( "en" if is_history else "dis")) ] elif isinstance(event, hangups.RenameEvent): action = True title = event.new_name segments = [ HangoutsSegment("renamed the hangout to "), HangoutsSegment(event.new_name, bold=True) ] elif isinstance(event, hangups.GroupLinkSharingModificationEvent): action = True is_shared = event.new_status == hangouts_pb2.GROUP_LINK_SHARING_STATUS_ON segments = [ HangoutsSegment("{}abled joining the hangout by link".format( "en" if is_shared else "dis")) ] elif isinstance(event, hangups.HangoutEvent): action = True texts = { hangouts_pb2.HANGOUT_EVENT_TYPE_START: "started a call", hangouts_pb2.HANGOUT_EVENT_TYPE_END: "ended the call", hangouts_pb2.HANGOUT_EVENT_TYPE_JOIN: "joined the call", hangouts_pb2.HANGOUT_EVENT_TYPE_LEAVE: "left the call" } try: segments = [HangoutsSegment(texts[event.event_type])] except KeyError: raise NotImplementedError else: raise NotImplementedError if not isinstance(event, hangups.ChatMessageEvent): text = immp.RichText(segments) return immp.SentMessage(id_=event.id_, channel=immp.Channel(hangouts, event.conversation_id), at=event.timestamp, text=text, user=user, action=action, joined=joined, left=left, title=title, attachments=attachments, raw=event)
async def public_channels(self): return [immp.Channel(self, id_) for id_ in self._channels]
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
def _filter_channels(self, type_): convs = self._convs.get_all(include_archived=True) return (immp.Channel(self, conv.id_) for conv in convs if conv._conversation.type == type_)
async def commands(self, channel, user): """ Retrieve all commands, and filter against the mappings. Args: channel (.Channel): Source channel where the command will be executed. user (.User): Author of the message to trigger the command. Returns: (str, .BoundCommand) dict: Commands provided by all hooks, in this channel for this user, keyed by name. """ log.debug("Collecting commands for %r in %r", user, channel) if isinstance(channel, immp.Plug): # Look for commands for a generic channel. plug = channel channel = immp.Channel(plug, "") private = False else: plug = None private = await channel.is_private() mappings = [] identities = {} for label, mapping in self.config["mapping"].items(): providers = mapping["identify"] if providers: for name, roles in providers.items(): if name not in identities: try: provider = self.host.hooks[name] identities[ name] = await provider.identity_from_user(user) except Exception: log.exception( "Exception retrieving identity from %r for map %r", name, label) identities[name] = None continue if not identities[name]: continue elif not roles or set(roles).intersection( identities[name].roles): log.debug("Identified %r as %r for map %r", user, identities[name], label) break else: log.debug("Could not identify %r for map %r, skipping", user, label) continue for name in mapping["groups"]: group = self.host.groups[name] if plug and group.has_plug(plug, "anywhere", "named"): mappings.append(mapping) elif not plug and await group.has_channel(channel): mappings.append(mapping) cmds = set() for mapping in mappings: cmds.update(self._mapping_cmds(mapping, channel, user, private)) mapped = {cmd.name: cmd for cmd in cmds} if len(cmds) > len(mapped): # Mapping by name silently overwrote at least one command with a duplicate name. raise immp.ConfigError( "Multiple applicable commands with the same name") return mapped
async def from_line(cls, irc, line): """ Convert a :class:`.Line` into a :class:`.Message`. Args: irc (.IRCPlug): Related plug instance that provides the line. line (.Line): Raw message line from the server. Returns: .IRCMessage: Parsed message object. """ channel = line.args[0] nick = line.source.split("!", 1)[0] if channel == irc.config["user"]["nick"]: # Private messages arrive "from A to B", and should be sent "from B to A". channel = nick user = IRCUser.from_id(irc, line.source) action = False joined = [] left = [] if line.command == "JOIN": text = "joined {}".format(channel) action = True joined.append(user) elif line.command == "PART": text = "left {}".format(channel) action = True left.append(user) elif line.command == "KICK": target = await irc.user_from_username(line.args[1]) text = immp.RichText([immp.Segment("kicked "), immp.Segment(target.username, bold=True, mention=target), immp.Segment(" ({})".format(line.args[2]))]) action = True left.append(target) elif line.command == "PRIVMSG": plain = line.args[1] match = re.match(r"\x01ACTION ([^\x01]*)\x01", plain) if match: plain = match.group(1) action = True text = immp.RichText() puppets = {client.nick: user for user, client in irc._puppets.items()} for match in re.finditer(r"[\w\d_\-\[\]{}\|`]+", plain): word = match.group(0) if word in puppets: target = puppets[word] else: target = irc.get_user(word) if target: if len(text) < match.start(): text.append(immp.Segment(plain[len(text):match.start()])) text.append(immp.Segment(word, mention=target)) if len(text) < len(plain): text.append(immp.Segment(plain[len(text):])) else: raise NotImplementedError return immp.SentMessage(id_=Line.next_ts(), channel=immp.Channel(irc, channel), text=text, user=user, action=action, joined=joined, left=left, raw=line)