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 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 unrole(self, msg, name): try: role, member = self._common(msg, str(name)) except _NoSuchRole: await msg.channel.send(immp.Message(text="No such role")) return else: await member.remove_roles(role) await msg.channel.send( immp.Message(text="\N{WHITE HEAVY CHECK MARK} Removed"))
async def write(self, msg): """ Force a write of the live config out to the configured file. """ self.write_config() await msg.channel.send( immp.Message(text="\N{WHITE HEAVY CHECK MARK} Written"))
async def add(self, msg, name, pwd): """ Create a new identity, or link to an existing one from a second user. """ if not msg.user or msg.user.plug not in self._plugs: return if self.find(msg.user): text = "{} Already identified".format(CROSS) else: pwd = IdentityGroup.hash(pwd) exists = False try: group = IdentityGroup.get(instance=self.config["instance"], name=name) exists = True except IdentityGroup.DoesNotExist: group = IdentityGroup.create(instance=self.config["instance"], name=name, pwd=pwd) if exists and not group.pwd == pwd: text = "{} Password incorrect".format(CROSS) elif not self.config["multiple"] and any( link.network == msg.user.plug.network_id for link in group.links): text = "{} Already identified on {}".format( CROSS, msg.user.plug.network_name) else: IdentityLink.create(group=group, network=msg.user.plug.network_id, user=msg.user.id) text = "{} {}".format(TICK, "Added" if exists else "Claimed") await msg.channel.send(immp.Message(text=text))
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 role(self, msg, name, role=None): """ List roles assigned to an identity, or add/remove a given role. """ try: group = IdentityGroup.get(instance=self.config["instance"], name=name) except IdentityGroup.DoesNotExist: text = "{} Name not registered".format(CROSS) else: if role: count = IdentityRole.delete().where( IdentityRole.group == group, IdentityRole.role == role).execute() if count: text = "{} Removed".format(TICK) else: IdentityRole.create(group=group, role=role) text = "{} Added".format(TICK) else: roles = IdentityRole.select().where( IdentityRole.group == group) if roles: labels = [role.role for role in roles] text = "Roles for {}: {}".format(name, ", ".join(labels)) else: text = "No roles for {}.".format(name) await msg.channel.send(immp.Message(text=text))
def _replace_ref(self, msg, channel): if not isinstance(msg, immp.Receipt): log.debug("Not replacing non-receipt message: %r", msg) return msg base = None if isinstance(msg, immp.SentMessage): base = immp.Message(text=msg.text, user=msg.user, action=msg.action, reply_to=msg.reply_to, joined=msg.joined, left=msg.left, title=msg.title, attachments=msg.attachments, raw=msg.raw) try: ref = self._cache[msg] except KeyError: log.debug("No match for source message: %r", msg) return base # Given message was a resync of the source message from a synced channel. if ref.ids.get(channel): log.debug("Found reference to previously synced message: %r", ref.key) at = ref.source.at if isinstance(ref.source, immp.Receipt) else None best = ref.source or msg return immp.SentMessage(id_=ref.ids[channel][0], channel=channel, at=at, text=best.text, user=best.user, action=best.action, reply_to=best.reply_to, joined=best.joined, left=best.left, title=best.title, attachments=best.attachments, raw=best.raw) elif channel.plug == msg.channel.plug: log.debug("Referenced message has origin plug, not modifying: %r", msg) return msg else: log.debug("Origin message not referenced in the target channel: %r", msg) return base
async def add(self, msg, match, response): """ Add a new trigger / response pair. """ text = "Updated" if match in self.responses else "Added" self.responses[match] = response await msg.channel.send(immp.Message(text="{} {}".format(TICK, 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 put(self, channel, msg): conv = self._convs.get(channel.source) # Attempt to find sources for referenced messages. clone = copy(msg) clone.reply_to = await self.resolve_message(clone.reply_to) requests = [] for attach in clone.attachments: # Generate requests for attached messages first. if isinstance(attach, immp.Message): requests += await self._requests( conv, await self.resolve_message(attach)) own_requests = await self._requests(conv, clone) if requests and not own_requests: # Forwarding a message but no content to show who forwarded it. info = immp.Message(user=clone.user, action=True, text="forwarded a message") own_requests = await self._requests(conv, info) requests += own_requests receipts = [] for request in requests: response = await self._client.send_chat_message(request) event = hangups.conversation.Conversation._wrap_event( response.created_event) receipts.append(await HangoutsMessage.from_event(self, event)) return receipts
async def _requests(self, conv, msg): uploads = [] images = [] places = [] for attach in msg.attachments: if isinstance(attach, immp.File) and attach.type in (immp.File.Type.image, immp.File.Type.video): uploads.append(self._upload(attach)) elif isinstance(attach, immp.Location): places.append(HangoutsLocation.to_place(attach)) if uploads: images = await gather(*uploads) requests = [] if msg.text or msg.reply_to: render = msg.render(link_name=False, edit=msg.edited, quote_reply=True) parts = self._serialise(render) media = None if len(images) == 1 and len(parts) == 1: # Attach the only image to the message text. media = images.pop() for segments in parts: requests.append(self._request(conv, segments, media)) if images: segments = [] if msg.user: label = immp.Message(user=msg.user, text="sent an image", action=True) segments = self._serialise(label.render(link_name=False))[0] # Send any additional media items in their own separate messages. for media in images: requests.append(self._request(conv, segments, media)) if places: # Send each location separately. for place in places: requests.append(self._request(conv, place=place)) # Include a label only if we haven't sent a text message earlier. if msg.user and not msg.text: label = immp.Message(user=msg.user, text="sent a location", action=True) segments = self._serialise(label.render(link_name=False))[0] requests.append(self._request(conv, segments)) return requests
async def add(self, msg, *words): """ Add a subscription to your trigger list. """ text = re.sub(r"[^\w ]", "", " ".join(words)).lower() _, created = await SubTrigger.get_or_create(network=msg.channel.plug.network_id, user=msg.user.id, text=text) resp = "{} {}".format(TICK, "Subscribed" if created else "Already subscribed") await msg.channel.send(immp.Message(text=resp))
async def remove(self, msg, *words): """ Remove a subscription from your trigger list. """ text = re.sub(r"[^\w ]", "", " ".join(words)).lower() count = await SubTrigger.filter(network=msg.channel.plug.network_id, user=msg.user.id, text=text).delete() resp = "{} {}".format(TICK, "Unsubscribed" if count else "Not subscribed") await msg.channel.send(immp.Message(text=resp))
async def remove(self, msg, match): """ Remove an existing trigger. """ if match in self.responses: del self.responses[match] text = "{} Removed".format(TICK) else: text = "{} No such response".format(CROSS) 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 sent.user or not sent.text or sent != source: return plain = str(sent.text) raw = None for prefix in self.config["prefix"]: if plain.lower().startswith(prefix): raw = plain[len(prefix):].split(maxsplit=1) break if not raw: return # Sync integration: exclude native channels of syncs from command execution. if isinstance(sent.channel.plug, immp.hook.sync.SyncPlug): log.debug("Suppressing command in virtual sync channel: %r", sent.channel) return synced = immp.hook.sync.SyncPlug.any_sync(self.host, sent.channel) if synced: log.debug("Mapping command channel: %r -> %r", sent.channel, synced) name = raw[0].lower() trailing = sent.text[-len(raw[1])::True] if len(raw) == 2 else None cmds = await self.commands(sent.channel, sent.user) try: cmd = cmds[name] except KeyError: log.debug("No matches for command name %r in %r", name, sent.channel) return else: log.debug("Matched command in %r: %r", sent.channel, cmd) try: args = cmd.parse(trailing) cmd.valid(*args) except ValueError: # Invalid number of arguments passed, return the command usage. await self.help(sent, name) return if synced and not cmd.sync_aware: msg = copy(sent) msg.channel = synced else: msg = sent try: log.debug("Executing command: %r %r", sent.channel, sent.text) await cmd(msg, *args) except BadUsage: await self.help(sent, name) except Exception as e: log.exception("Exception whilst running command: %r", sent.text) if self.config["return-errors"]: text = ": ".join(filter(None, (e.__class__.__name__, str(e)))) await sent.channel.send( immp.Message(text="\N{WARNING SIGN} {}".format(text)))
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))
async def add(self, msg, text): """ Add a new note for this channel. """ await Note.create( network=msg.channel.plug.network_id, channel=msg.channel.source, user=(msg.user.id or msg.user.username) if msg.user else None, text=text.raw()) count = await Note.select_channel(msg.channel).count() await msg.channel.send( immp.Message(text="{} Added #{}".format(TICK, count)))
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))
async def reset(self, msg): """ Delete the current identity and all linked users. """ if not msg.user: return group = self.find(msg.user) if not group: text = "{} Not identified".format(CROSS) else: group.delete_instance() text = "{} Reset".format(TICK) await msg.channel.send(immp.Message(text=text))
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 send(self, label, msg, origin=None, ref=None): """ Send a message to all channels in this sync. Args: label (str): Bridge that defines the underlying synced channels to send to. msg (.Message): External message to push. This should be the source copy when syncing a message from another channel. origin (.Receipt): Raw message that triggered this sync; if set and part of the sync, it will be skipped (used to avoid retransmitting a message we just received). This should be the plug-native copy of a message when syncing from another channel. ref (.SyncRef): Existing sync reference, if message has been partially synced. """ base = immp.Message(text=msg.text, user=msg.user, edited=msg.edited, action=msg.action, reply_to=msg.reply_to, joined=msg.joined, left=msg.left, title=msg.title, attachments=msg.attachments, raw=msg) queue = [] for synced in self.channels[label]: if origin and synced == origin.channel: continue elif ref and ref.ids[synced]: log.debug("Skipping already-synced target channel %r: %r", synced, ref) continue local = base.clone() self._replace_recurse(local, self._replace_ref, synced) await self._alter_recurse(local, self._alter_identities, synced) await self._alter_recurse(local, self._alter_name) queue.append(self._send(synced, local)) # Just like with plugs, when sending a new (external) message to all channels in a sync, we # need to wait for all plugs to complete and have their IDs cached before processing any # further messages. async with self._lock: ids = dict(await gather(*queue)) if ref: ref.ids.update(ids) else: ref = SyncRef(ids, source=msg, origin=origin) self._cache.add(ref) # Push a copy of the message to the sync channel, if running. if self.plug: sent = immp.SentMessage(id_=ref.key, channel=immp.Channel(self.plug, label), text=msg.text, user=msg.user, action=msg.action, reply_to=msg.reply_to, joined=msg.joined, left=msg.left, title=msg.title, attachments=msg.attachments, raw=msg) self.plug.queue(sent) return ref
async def password(self, msg, pwd): """ Update the password for the current identity. """ if not msg.user: return group = self.find(msg.user) if not group: text = "{} Not identified".format(CROSS) else: group.pwd = IdentityGroup.hash(pwd) group.save() text = "{} Changed".format(TICK) await msg.channel.send(immp.Message(text=text))
async def edit(self, msg, num, text): """ Update an existing note from this channel with new text. """ 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: note.text = text.raw() await note.save() text = "{} Edited".format(TICK) 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 await self.group.has_channel(sent.channel): return # Skip our own response messages. if (sent.channel, sent.id) in self._sent: return text = str(source.text) for regex, response in self.config["responses"].items(): match = re.search(regex, text, re.I) if match: log.debug("Matched regex %r in channel: %r", match, sent.channel) response = response.format(*match.groups()) for id_ in await sent.channel.send(immp.Message(text=response) ): self._sent.append((sent.channel, id_))
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 rename(self, msg, name): """ Rename the current identity. """ if not msg.user: return group = await self.find(msg.user) if not group: text = "{} Not identified".format(CROSS) elif group.name == name: text = "{} No change".format(TICK) elif await IdentityGroup.filter(instance=self.config["instance"], name=name).exists(): text = "{} Name already in use".format(CROSS) else: group.name = name await group.save() text = "{} Claimed".format(TICK) await msg.channel.send(immp.Message(text=text))
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))
async def exclude(self, msg, *words): """ Don't trigger a specific subscription in the current channel. """ text = re.sub(r"[^\w ]", "", " ".join(words)).lower() try: trigger = await SubTrigger.get(network=msg.user.plug.network_id, user=msg.user.id, text=text) except DoesNotExist: resp = "{} Not subscribed".format(CROSS) else: exclude, created = await SubExclude.get_or_create(trigger=trigger, network=msg.channel.plug.network_id, channel=msg.channel.source) if not created: await exclude.delete() resp = "{} {}".format(TICK, "Excluded" if created else "No longer excluded") await msg.channel.send(immp.Message(text=resp))