Ejemplo n.º 1
0
 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
Ejemplo n.º 2
0
    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)
Ejemplo n.º 3
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))
Ejemplo n.º 4
0
 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))
Ejemplo n.º 5
0
 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)
Ejemplo n.º 6
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))
Ejemplo n.º 7
0
 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))
Ejemplo n.º 8
0
 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))
Ejemplo n.º 9
0
 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))
Ejemplo n.º 10
0
Archivo: irc.py Proyecto: Terrance/IMMP
 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
Ejemplo n.º 11
0
    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
Ejemplo n.º 12
0
 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
Ejemplo n.º 13
0
 async def who(self, msg, name):
     """
     Recall a known identity and all of its links.
     """
     if self.config["public"]:
         providers = self._identities
     else:
         tasks = (provider.identity_from_user(msg.user)
                  for provider in self._identities)
         providers = [
             identity.provider for identity in await gather(*tasks)
             if identity
         ]
     if providers:
         if name[0].mention:
             user = name[0].mention
             tasks = (provider.identity_from_user(user)
                      for provider in providers)
         else:
             tasks = (provider.identity_from_name(str(name))
                      for provider in providers)
         identities = list(filter(None, await gather(*tasks)))
         links = {
             link
             for identity in identities for link in identity.links
         }
         if links:
             text = name.clone()
             for segment in text:
                 segment.bold = True
             text.append(immp.Segment(" may appear as:"))
             for user in sorted(links,
                                key=lambda user: user.plug.network_name):
                 text.append(immp.Segment("\n"))
                 text.append(
                     immp.Segment("({}) ".format(user.plug.network_name)))
                 if user.link:
                     text.append(
                         immp.Segment(user.real_name or user.username,
                                      link=user.link))
                 elif user.real_name and user.username:
                     text.append(
                         immp.Segment("{} [{}]".format(
                             user.real_name, user.username)))
                 else:
                     text.append(
                         immp.Segment(user.real_name or user.username))
         else:
             text = "{} Name not in use".format(CROSS)
     else:
         text = "{} Not identified".format(CROSS)
     await msg.channel.send(immp.Message(text=text))
Ejemplo n.º 14
0
    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
Ejemplo n.º 15
0
 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))
Ejemplo n.º 16
0
 async def _requests(self, dc_channel, webhook, msg):
     name = image = 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:
         # Discord offers no reply mechanism, so instead we just fetch the referenced message
         # and render it manually.
         resolved = await self.resolve_message(msg.reply_to)
         embeds.append((await DiscordMessage.to_embed(self, resolved, True), None))
     if webhook:
         # Sending via webhook: multiple embeds and files supported.
         requests = []
         text = 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))
             lines = DiscordRichText.chunk_split(DiscordRichText.to_markdown(self, rich, True))
             if len(lines) > 1:
                 # Multiple messages required to accommodate the text.
                 requests.extend(webhook.send(content=line, wait=True, username=name,
                                              avatar_url=image) for line in lines)
             else:
                 text = lines[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 = None
         rich = msg.render(link_name=False, edit=msg.edited) or None
         if rich:
             lines = DiscordRichText.chunk_split(DiscordRichText.to_markdown(self, rich))
             if len(lines) > 1:
                 # Multiple messages required to accommodate the text.
                 requests.extend(dc_channel.send(content=line) for line in lines)
             else:
                 text = lines[0]
         if len(embeds) == 1:
             # Attach the only embed to the message text.
             embed, _ = embeds.pop()
         if text or embed or files:
             requests.append(dc_channel.send(content=text, embed=embed, files=files))
         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
Ejemplo n.º 17
0
    def from_markdown(cls, discord_, text, channel=None):
        """
        Convert a string of Markdown into a :class:`.RichText`.

        Args:
            discord (.DiscordPlug):
                Related plug instance that provides the text.
            text (str):
                Markdown formatted text.
            channel (.Channel):
                Related channel, used to retrieve mentioned users as members.

        Returns:
            .DiscordRichText:
                Parsed rich text container.
        """
        changes = defaultdict(dict)
        plain = ""
        done = False
        while not done:
            # Identify pre blocks, parse formatting only outside of them.
            match = cls._pre_regex.search(text)
            if match:
                parse = text[:match.start()]
                pre = match.group(1)
                text = text[match.end():]
            else:
                parse = text
                done = True
            offset = len(plain)
            while True:
                match = cls._format_regex.search(parse)
                if not match:
                    break
                start = match.start()
                end = match.end()
                tag = match.group(1)
                # Strip the tag characters from the message.
                parse = parse[:start] + match.group(2) + parse[end:]
                end -= 2 * len(tag)
                # Record the range where the format is applied.
                field = cls.tags[tag]
                changes[offset + start][field] = True
                changes[offset + end][field] = False
                # Shift any future tags back.
                for pos in sorted(changes):
                    if pos > offset + end:
                        changes[pos - 2 * len(tag)].update(changes.pop(pos))
            plain += parse
            if not done:
                changes[len(plain)]["pre"] = True
                changes[len(plain + pre)]["pre"] = False
                plain += pre
        dc_channel = discord_._get_channel(channel)
        if isinstance(dc_channel, discord.TextChannel):
            getter = dc_channel.guild.get_member
        else:
            getter = discord_._client.get_user
        for match in cls._mention_regex.finditer(plain):
            user = getter(int(match.group(1)))
            if user:
                changes[match.start()]["mention"] = DiscordUser.from_user(discord_, user)
                changes[match.end()]["mention"] = None
        segments = []
        points = list(sorted(changes.keys()))
        formatting = {}
        # Iterate through text in change start/end pairs.
        for start, end in zip([0] + points, points + [len(plain)]):
            formatting.update(changes[start])
            if start == end:
                # Zero-length segment at the start or end, ignore it.
                continue
            if formatting.get("mention"):
                user = formatting["mention"]
                part = "@{}".format(user.username or user.real_name)
            else:
                part = emojize(plain[start:end], use_aliases=True)
                # Strip Discord channel/emoji tags, replace with a plain text representation.
                part = cls._channel_regex.sub(partial(cls._sub_channel, discord_), part)
                part = cls._emoji_regex.sub(r"\1", part)
            segments.append(immp.Segment(part, **formatting))
        return cls(segments)
Ejemplo n.º 18
0
 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 = await 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)
Ejemplo n.º 19
0
 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
Ejemplo n.º 20
0
Archivo: irc.py Proyecto: Terrance/IMMP
    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)
Ejemplo n.º 21
0
 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
Ejemplo n.º 22
0
    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
Ejemplo n.º 23
0
    async def from_mrkdwn(cls, slack, text):
        """
        Convert a string of Slack's Mrkdwn into a :class:`.RichText`.

        Args:
            slack (.SlackPlug):
                Related plug instance that provides the text.
            text (str):
                Slack-style formatted text.

        Returns:
            .SlackRichText:
                Parsed rich text container.
        """
        changes = defaultdict(dict)
        plain = ""
        done = False
        while not done:
            # Identify pre blocks, parse formatting only outside of them.
            match = cls._pre_regex.search(text)
            if match:
                parse = text[:match.start()]
                pre = match.group(1)
                text = text[match.end():]
            else:
                parse = text
                done = True
            offset = len(plain)
            while True:
                match = cls._format_regex.search(parse)
                if not match:
                    break
                start = match.start()
                end = match.end()
                tag = match.group(1)
                # Strip the tag characters from the message.
                parse = parse[:start] + match.group(2) + parse[end:]
                end -= 2 * len(tag)
                # Record the range where the format is applied.
                field = cls.tags[tag]
                changes[offset + start][field] = True
                changes[offset + end][field] = False
                # Shift any future tags back.
                for pos in sorted(changes):
                    if pos > offset + end:
                        changes[pos - 2 * len(tag)].update(changes.pop(pos))
            plain += parse
            if not done:
                changes[len(plain)]["pre"] = True
                changes[len(plain + pre)]["pre"] = False
                plain += pre
        for match in cls._link_regex.finditer(plain):
            # Store the link target; the link tag will be removed after segmenting.
            changes[match.start()]["link"] = cls._unescape(match.group(1))
            changes[match.end()]["link"] = None
        for match in cls._mention_regex.finditer(plain):
            changes[match.start()]["mention"] = await slack.user_from_id(
                match.group(1))
            changes[match.end()]["mention"] = None
        segments = []
        points = list(sorted(changes.keys()))
        formatting = {}
        # Iterate through text in change start/end pairs.
        for start, end in zip([0] + points, points + [len(plain)]):
            formatting.update(changes[start])
            if start == end:
                # Zero-length segment at the start or end, ignore it.
                continue
            if formatting.get("mention"):
                user = formatting["mention"]
                part = "@{}".format(user.real_name)
            else:
                part = plain[start:end]
                # Strip Slack channel tags, replace with a plain-text representation.
                part = cls._channel_regex.sub(partial(cls._sub_channel, slack),
                                              part)
                part = cls._link_regex.sub(cls._sub_link, part)
                part = emojize(cls._unescape(part), use_aliases=True)
            segments.append(immp.Segment(part, **formatting))
        return cls(segments)
Ejemplo n.º 24
0
 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