async def put(self, channel, msg): clone = copy(msg) if clone.text: clone.text = msg.text.clone() forward_ids = [] for attach in msg.attachments: if isinstance(attach, immp.Receipt): # No public API to share a message, rely on archive link unfurling instead. link = ("https://{}.slack.com/archives/{}/p{}".format( self._team["domain"], channel.source, attach.id.replace(".", ""))) if clone.text: clone.text.append(immp.Segment("\n{}".format(link))) else: clone.text = immp.RichText([immp.Segment(link)]) elif isinstance(attach, immp.Message): forward_ids += await self._post(channel, clone, attach) own_ids = await self._post(channel, clone, clone) if forward_ids and not own_ids and msg.user: # Forwarding a message but no content to show who forwarded it. info = immp.Message(user=msg.user, action=True, text="forwarded a message") own_ids += await self._post(channel, msg, info) return forward_ids + own_ids
async def list(self, msg, query=None): """ Recall all notes for this channel, or search for text across all notes. """ notes = await Note.select_channel(msg.channel) count = len(notes) if query: matches = [(num, note) for num, note in enumerate(notes, 1) if query.lower() in note.text.lower()] count = len(matches) else: matches = enumerate(notes, 1) if count: title = ("{}{} note{} in this channel{}".format( count or "No", " matching" if query else "", "" if count == 1 else "s", ":" if count else ".")) text = immp.RichText([immp.Segment(title, bold=True)]) for num, note in matches: text.append(immp.Segment("\n"), immp.Segment("{}.".format(num), bold=True), immp.Segment("\t"), *immp.RichText.unraw(note.text, self.host), immp.Segment("\t"), immp.Segment(note.ago, italic=True)) else: text = "{} No {}".format(CROSS, "matches" if query and notes else "notes") target = None if count and msg.user: target = await msg.user.private_channel() if target and msg.channel != target: await target.send(immp.Message(text=text)) await msg.channel.send(immp.Message(text="{} Sent".format(TICK))) else: await msg.channel.send(immp.Message(text=text))
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))
async def list(self, msg, query=None): """ Recall all notes for this channel, or search for text across all notes. """ notes = Note.select_channel(msg.channel) if query: matches = notes.where(Note.text.contains(query)) count = len(matches) else: count = len(notes) title = ("{}{} note{} in this channel{}".format( count, " matching" if query else "", "" if count == 1 else "s", ":" if count else ".")) text = immp.RichText([immp.Segment(title, bold=bool(notes))]) for num, note in enumerate(notes, 1): if query and note not in matches: continue text.append(immp.Segment("\n"), immp.Segment("{}.".format(num), bold=True), immp.Segment("\t"), *immp.RichText.unraw(note.text, self.host), immp.Segment("\t"), immp.Segment(note.ago, italic=True)) target = None if msg.user: target = await msg.user.private_channel() if target: await target.send(immp.Message(text=text)) await msg.channel.send(immp.Message(text="{} Sent".format(TICK))) else: await msg.channel.send(immp.Message(text=text))
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 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 _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 list(self, msg): """ Show all active subscriptions. """ subs = await SubTrigger.filter(network=msg.user.plug.network_id, user=msg.user.id).order_by("text") if subs: text = immp.RichText([immp.Segment("Your subscriptions:", bold=True)]) for sub in subs: text.append(immp.Segment("\n- {}".format(sub.text))) else: text = "No active subscriptions." await msg.channel.send(immp.Message(text=text))
async def on_receive(self, sent, source, primary): await super().on_receive(sent, source, primary) if not primary or not source.text or await sent.channel.is_private(): return try: _, members = await self._get_members(sent) except _Skip: return mentioned = set() for mention in re.findall(r"@\S+", str(source.text)): matches = self.match(mention, members) if matches: log.debug("Mention %r applies: %r", mention, matches) mentioned.update(matches) else: log.debug("Mention %r doesn't apply", mention) for segment in source.text: if segment.mention and segment.mention in members: log.debug("Segment mention %r applies: %r", segment.text, segment.mention) mentioned.add(segment.mention) if not mentioned: return text = immp.RichText() if source.user: text.append( immp.Segment(source.user.real_name or source.user.username, bold=True), immp.Segment(" mentioned you")) else: text.append(immp.Segment("You were mentioned")) title = await sent.channel.title() link = await sent.channel.link() if title: text.append(immp.Segment(" in "), immp.Segment(title, italic=True)) text.append(immp.Segment(":\n")) text += source.text if source.user and source.user.link: text.append(immp.Segment("\n"), immp.Segment("Go to user", link=source.user.link)) if link: text.append(immp.Segment("\n"), immp.Segment("Go to channel", link=link)) tasks = [] for member in mentioned: if member == source.user: continue private = await sent.channel.plug.channel_for_user(member) if private: tasks.append(private.send(immp.Message(text=text))) if tasks: await wait(tasks)
async def to_embed(cls, discord_, msg, reply=False): """ Convert a :class:`.Message` to a message embed structure, suitable for embedding within an outgoing message. Args: discord_ (.DiscordPlug): Target plug instance for this attachment. msg (.Message): Original message from another plug or hook. reply (bool): Whether to show a reply icon instead of a quote icon. Returns. discord.Embed: Discord API `embed <https://discord.com/developers/docs/resources/channel>`_ object. """ icon = "\N{RIGHTWARDS ARROW WITH HOOK}" if reply else "\N{SPEECH BALLOON}" embed = discord.Embed() embed.set_footer(text=icon) if isinstance(msg, immp.Receipt): embed.timestamp = msg.at if msg.user: link = discord.Embed.Empty # Exclude platform-specific join protocol URLs. if (msg.user.link or "").startswith("http"): link = msg.user.link embed.set_author(name=(msg.user.real_name or msg.user.username), url=link, icon_url=msg.user.avatar or discord.Embed.Empty) quote = None action = False if msg.text: quote = msg.text.clone() action = msg.action elif msg.attachments: count = len(msg.attachments) what = "{} attachment".format( count) if count > 1 else "this attachment" quote = immp.RichText([immp.Segment("sent {}".format(what))]) action = True if quote: if action: for segment in quote: segment.italic = True embed.description = DiscordRichText.to_markdown(discord_, quote) return embed
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 on_receive(self, sent, source, primary): await super().on_receive(sent, source, primary) if not primary or not source.text or await sent.channel.is_private(): return try: lookup, members = await self._get_members(sent) except _Skip: return present = {(member.plug.network_id, str(member.id)): member for member in members} triggered = self.match(self._clean(str(source.text)), lookup, present) if not triggered: return tasks = [] for member, triggers in triggered.items(): if member == source.user: continue private = await member.private_channel() if not private: continue text = immp.RichText() mentioned = immp.Segment(", ".join(sorted(triggers)), italic=True) if source.user: text.append( immp.Segment(source.user.real_name or source.user.username, bold=True), immp.Segment(" mentioned "), mentioned) else: text.append(mentioned, immp.Segment(" mentioned")) title = await sent.channel.title() link = await sent.channel.link() if title: text.append(immp.Segment(" in "), immp.Segment(title, italic=True)) text.append(immp.Segment(":\n")) text += source.text if source.user and source.user.link: text.append(immp.Segment("\n"), immp.Segment("Go to user", link=source.user.link)) if link: text.append(immp.Segment("\n"), immp.Segment("Go to channel", link=link)) tasks.append(private.send(immp.Message(text=text))) if tasks: await wait(tasks)
async def show(self, msg, num): """ Recall a single note in this channel. """ try: note = await Note.select_position(msg.channel, int(num)) except ValueError: raise BadUsage from None except DoesNotExist: text = "{} Does not exist".format(CROSS) else: text = immp.RichText([ immp.Segment("{}.".format(num), bold=True), immp.Segment("\t"), *immp.RichText.unraw(note.text, self.host), immp.Segment("\t"), immp.Segment(note.ago, italic=True) ]) await msg.channel.send(immp.Message(text=text))
def to_attachment(cls, slack, msg, reply=False): """ Convert a :class:`.Message` to a message attachment structure, suitable for embedding within an outgoing message. Args: slack (.SlackPlug): Target plug instance for this attachment. msg (.Message): Original message from another plug or hook. reply (bool): Whether to show a reply icon instead of a quote icon. Returns. dict: Slack API `attachment <https://api.slack.com/docs/message-attachments>`_ object. """ icon = ":arrow_right_hook:" if reply else ":speech_balloon:" quote = {"footer": icon} if isinstance(msg, immp.SentMessage): quote["ts"] = msg.at.timestamp() if msg.user: quote["author_name"] = msg.user.real_name or msg.user.username quote["author_icon"] = msg.user.avatar quoted_rich = None quoted_action = False if msg.text: quoted_rich = msg.text.clone() quoted_action = msg.action elif msg.attachments: count = len(msg.attachments) what = "{} files".format(count) if count > 1 else "this file" quoted_rich = immp.RichText([immp.Segment("sent {}".format(what))]) quoted_action = True if quoted_rich: if quoted_action: for segment in quoted_rich: segment.italic = True quote["text"] = SlackRichText.to_mrkdwn(slack, quoted_rich) quote["mrkdwn_in"] = ["text"] return quote
async def _requests(self, dc_channel, webhook, msg): name = image = None reply_to = reply_ref = reply_embed = None embeds = [] files = [] if msg.user: name = msg.user.real_name or msg.user.username image = msg.user.avatar for i, attach in enumerate(msg.attachments or []): if isinstance(attach, immp.File): if attach.title: title = attach.title elif attach.type == immp.File.Type.image: title = "image_{}.png".format(i) elif attach.type == immp.File.Type.video: title = "video_{}.mp4".format(i) else: title = "file_{}".format(i) async with (await attach.get_content(self.session)) as img_content: # discord.py expects a file-like object with a synchronous read() method. # NB. The whole file is read into memory by discord.py anyway. files.append( discord.File(BytesIO(await img_content.read()), title)) elif isinstance(attach, immp.Location): embed = discord.Embed() embed.title = attach.name or "Location" embed.url = attach.google_map_url embed.description = attach.address embed.set_thumbnail(url=attach.google_image_url(80)) embed.set_footer( text="{}, {}".format(attach.latitude, attach.longitude)) embeds.append((embed, "sent a location")) elif isinstance(attach, immp.Message): resolved = await self.resolve_message(attach) embeds.append( (await DiscordMessage.to_embed(self, resolved), "sent a message")) if msg.reply_to: if isinstance(msg.reply_to, immp.Receipt): if msg.reply_to.channel.plug.network_id == self.network_id: guild_id = dc_channel.guild.id if dc_channel.guild else None reply_ref = discord.MessageReference( message_id=int(msg.reply_to.id), channel_id=dc_channel.id, guild_id=guild_id) if not reply_to: reply_to = msg.reply_to reply_embed = await DiscordMessage.to_embed(self, reply_to, True) if webhook and msg.user: # Sending via webhook: multiple embeds and files supported. requests = [] rich = None if reply_embed: # Webhooks can't reply to other messages, quote the target in an embed instead. # https://github.com/discord/discord-api-docs/issues/2251 embeds.append((reply_embed, None)) if msg.text: rich = msg.text.clone() if msg.action: for segment in rich: segment.italic = True if msg.edited: if rich: rich.append(immp.Segment(" ")) else: rich = immp.RichText() rich.append(immp.Segment("(edited)", italic=True)) text = None if rich: mark = DiscordRichText.to_markdown(self, rich, True) chunks = immp.RichText.chunked_plain(mark, 2000) if len(chunks) > 1: # Multiple messages required to accommodate the text. requests.extend( webhook.send(content=chunk, wait=True, username=name, avatar_url=image) for chunk in chunks) else: text = chunks[0] if text or embeds or files: requests.append( webhook.send(content=text, wait=True, username=name, avatar_url=image, files=files, embeds=[embed[0] for embed in embeds])) return requests else: # Sending via client: only a single embed per message. requests = [] text = embed = desc = None chunks = [] rich = msg.render(link_name=False, edit=msg.edited) or None if rich: mark = DiscordRichText.to_markdown(self, rich) text, *chunks = immp.RichText.chunked_plain(mark, 2000) if reply_embed and not reply_ref: embeds.append((reply_embed, None)) if len(embeds) == 1: # Attach the only embed to the message text. embed, desc = embeds.pop() if text or embed or files: # Primary message: set reference for reply-to if applicable. requests.append( dc_channel.send(content=text or desc, embed=embed, files=files, reference=reply_ref)) # Send the remaining text if multiple messages were required to accommodate it. requests.extend(dc_channel.send(content=chunk) for chunk in chunks) for embed, desc in embeds: # Send any additional embeds in their own separate messages. content = None if msg.user and desc: label = immp.Message(user=msg.user, text="sent {}".format(desc), action=True) content = DiscordRichText.to_markdown(self, label.render()) requests.append(dc_channel.send(content=content, embed=embed)) return requests
async def help(self, msg, command=None): """ List all available commands in this channel, or show help about a single command. """ if await msg.channel.is_private(): current = None private = msg.channel else: current = msg.channel private = await msg.user.private_channel() parts = defaultdict(dict) if current: parts[current] = await self.commands(current, msg.user) if private: parts[private] = await self.commands(private, msg.user) for name in parts[private]: parts[current].pop(name, None) parts[None] = await self.commands(msg.channel.plug, msg.user) for name in parts[None]: if private: parts[private].pop(name, None) if current: parts[current].pop(name, None) full = dict(parts[None]) full.update(parts[current]) full.update(parts[private]) if command: try: cmd = full[command] except KeyError: text = "\N{CROSS MARK} No such command" else: text = immp.RichText([immp.Segment(cmd.name, bold=True)]) if cmd.spec: text.append(immp.Segment(" {}".format(cmd.spec))) if cmd.doc: text.append(immp.Segment(":", bold=True), immp.Segment("\n"), *immp.RichText.unraw(cmd.doc, self.host)) else: titles = {None: [immp.Segment("Global commands", bold=True)]} if private: titles[private] = [immp.Segment("Private commands", bold=True)] if current: titles[current] = [ immp.Segment("Commands for ", bold=True), immp.Segment(await current.title(), bold=True, italic=True) ] text = immp.RichText() for channel, cmds in parts.items(): if not cmds: continue if text: text.append(immp.Segment("\n")) text.append(*titles[channel]) for name, cmd in sorted(cmds.items()): text.append(immp.Segment("\n- {}".format(name))) if cmd.spec: text.append( immp.Segment(" {}".format(cmd.spec), italic=True)) await msg.channel.send(immp.Message(text=text))
async def _post(self, channel, parent, msg): ids = [] uploads = 0 if msg.user: name = msg.user.real_name or msg.user.username image = msg.user.avatar else: name = self.config["fallback-name"] image = self.config["fallback-image"] for attach in msg.attachments: if isinstance(attach, immp.File): # Upload each file to Slack. data = FormData({ "channels": channel.source, "filename": attach.title or "" }) if isinstance(parent.reply_to, immp.Receipt): # Reply directly to the corresponding thread. Note that thread_ts can be any # message in the thread, it need not be resolved to the parent. data.add_field("thread_ts", msg.reply_to.id) if self.config["thread-broadcast"]: data.add_field("broadcast", "true") if msg.user: comment = immp.RichText([ immp.Segment(name, bold=True, italic=True), immp.Segment(" uploaded this file", italic=True) ]) data.add_field("initial_comment", SlackRichText.to_mrkdwn(self, comment)) img_resp = await attach.get_content(self.session) data.add_field("file", img_resp.content, filename="file") upload = await self._api("files.upload", _Schema.upload, data=data) uploads += 1 for shared in upload["file"]["shares"].values(): if channel.source in shared: ids += [ share["ts"] for share in shared[channel.source] ] if len(ids) < uploads: log.warning("Missing some file shares: sent %d, got %d", uploads, len(ids)) data = { "channel": channel.source, "as_user": msg.user is None, "username": name, "icon_url": image } rich = None if msg.text: rich = msg.text.clone() if msg.action: for segment in rich: segment.italic = True if msg.edited: rich.append(immp.Segment(" (edited)", italic=True)) attachments = [] if isinstance(parent.reply_to, immp.Receipt): data["thread_ts"] = msg.reply_to.id if self.config["thread-broadcast"]: data["reply_broadcast"] = "true" elif isinstance(msg.reply_to, immp.Message): attachments.append( SlackMessage.to_attachment(self, msg.reply_to, True)) for attach in msg.attachments: if isinstance(attach, immp.Location): coords = "{}, {}".format(attach.latitude, attach.longitude) fallback = "{} ({})".format( attach.address, coords) if attach.address else coords attachments.append({ "fallback": fallback, "title": attach.name or "Location", "title_link": attach.google_map_url, "text": attach.address, "footer": "{}, {}".format(attach.latitude, attach.longitude) }) if rich or attachments: if rich: data["text"] = SlackRichText.to_mrkdwn(self, rich) if attachments: data["attachments"] = json_dumps(attachments) post = await self._api("chat.postMessage", _Schema.post, data=data) ids.append(post["ts"]) return ids
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_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)
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)
def _repo_text(cls, github, type_, event): text = None repo = event["repository"] name = repo["full_name"] name_seg = immp.Segment(name, link=repo["html_url"]) action = cls._action_text(github, event.get("action")) issue = event.get("issue") pull = event.get("pull_request") if type_ == "repository": text = immp.RichText( [immp.Segment("{} repository ".format(action)), name_seg]) elif type_ == "push": push = _Schema.push(event) count = len(push["commits"]) desc = "{} commits".format( count) if count > 1 else push["after"][:7] root, target = push["ref"].split("/", 2)[1:] join = None if root == "tags": tag = True elif root == "heads": tag = False else: raise NotImplementedError if push["deleted"]: action = "deleted {}".format("tag" if tag else "branch") elif tag: action, join = "tagged", "as" else: action, join = "pushed", ("to new branch" if push["created"] else "to") text = immp.RichText([immp.Segment("{} ".format(action))]) if join: text.append(immp.Segment(desc, link=push["compare"]), immp.Segment(" {} ".format(join))) text.append(immp.Segment("{} of {}".format(target, name))) for commit in push["commits"]: text.append( immp.Segment("\n\N{BULLET} {}: {}".format( commit["id"][:7], commit["message"].split("\n")[0]))) elif type_ == "release": release = event["release"] desc = ("{} ({} {})".format(release["name"], name, release["tag_name"]) if release["name"] else release["tag_name"]) text = immp.RichText([ immp.Segment("{} release ".format(action)), immp.Segment(desc, link=release["html_url"]) ]) elif type_ == "issues": desc = "{} ({}#{})".format(issue["title"], name, issue["number"]) text = immp.RichText([ immp.Segment("{} issue ".format(action)), immp.Segment(desc, link=issue["html_url"]) ]) elif type_ == "issue_comment": comment = event["comment"] desc = "{} ({}#{})".format(issue["title"], name, issue["number"]) text = immp.RichText([ immp.Segment("{} a ".format(action)), immp.Segment("comment", link=comment["html_url"]), immp.Segment(" on issue "), immp.Segment(desc, link=issue["html_url"]) ]) elif type_ == "pull_request": if action == "closed" and pull["merged"]: action = "merged" desc = "{} ({}#{})".format(pull["title"], name, pull["number"]) text = immp.RichText([ immp.Segment("{} pull request ".format(action)), immp.Segment(desc, link=pull["html_url"]) ]) elif type_ == "pull_request_review": review = event["review"] desc = "{} ({}#{})".format(pull["title"], name, pull["number"]) text = immp.RichText([ immp.Segment("{} a ".format(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": comment = event["comment"] desc = "{} ({}#{})".format(pull["title"], name, pull["number"]) text = immp.RichText([ immp.Segment("{} a ".format(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"], name, project["number"]) text = immp.RichText([ immp.Segment("{} project ".format(action)), immp.Segment(desc, link=project["html_url"]) ]) elif type_ == "project_card": card = event["project_card"] text = immp.RichText([ immp.Segment("{} ".format(action)), immp.Segment("card", link=card["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"], name)), immp.Segment(page["title"], link=page["html_url"])) elif type_ == "fork": fork = event["forkee"] text = immp.RichText([ immp.Segment("forked {} to ".format(name)), immp.Segment(fork["full_name"], link=fork["html_url"]) ]) elif type_ == "watch": text = immp.RichText([immp.Segment("starred "), name_seg]) elif type_ == "public": text = immp.RichText( [immp.Segment("made "), name_seg, immp.Segment(" public")]) return text