Skip to content

Commit

Permalink
discord: implement force proxied emoji (resolves #11)
Browse files Browse the repository at this point in the history
This adds a new feature to Séance where reference user reactions of
emoji can automatically be proxied. Any emoji provided by the
`proxied-emoji` config value will be proxied.

This removes the ability of the reference user to react with those emoji
and is primarily assumed to be used for things like custom heart
reactions that are unique per-user, making their usage easier.

The valid config value is a comma or whitespace separated list of
unicode emoji and Discord custom emoji IDs that should be handled this
way. `*` may also be used to indicate that *all* reactions by the
reference user should be proxied.

This commit also revises the README to more clearly list the available
configuration options.
  • Loading branch information
Lunaphied committed Jul 8, 2024
1 parent 412e309 commit 529d2f9
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 27 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ $ seance-discord --token ODDFOFXUpgf7yEntul5ockCA.OFk6Ph.lmsA54bT0Fux1IpsYvey5Xu

Note that the Discord bot also requires the Presence and Server Members Privileged Gateway Intents, which can be enabled in the "Bot" settings of the Discord application page.

### Options

These are the available configuration options, configured as described [below](#config-file).

#### Discord
- `token` - The Discord bot token used to authenticate, **important**: this must be kept secret as it allows anyone to control your bot account.
- `ref-user-id` - The reference user's Discord ID. This is the user account allowed to proxy messages and execute Séance commands.
- `pattern` - The regular expression pattern used to match a message to be proxied. Must contain a group named `content` which should contain the message body that will be in the proxied message.
- `prefix` - A prefix that can be used before a `!` command (such as `!edit`) to uniquely indicate that this instance of Séance should handle the command. Unprefixed commands are always accepted, even when this is set.
- `proxied-emoji` - A whitespace or comma separated list of unicode emoji (`🤝`) and Discord custom emoji ID numbers that will *always* be reproxied by Séance when used as a reaction by the reference user. The reference user will *not* be able to react with this emoji themselves, it will always be removed.

- **TODO: DM Mode configuration**


### Telegram
**TODO**

### Config File

Anything that can be passed as a command-line option can also be specified an INI config file. Options for the Discord bot are placed under a `[Discord]` section, with the name of the INI key being the same as the command-line option without the leading `--`. Words can be separated by dashes, underscores, or spaces. For example, `--ref-user-id 188344527881400991` can be any of the following:
Expand Down Expand Up @@ -80,6 +97,7 @@ Pros of Séance over PluralKit on Discord:
- No webhooks; real Discord account
- Role colors in name
- Real replies
- Easily proxy emoji reactions

Cons of Séance over PluralKit on Discord:
- Requires self-hosting
Expand Down
101 changes: 74 additions & 27 deletions seance/discord_bot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,18 @@
import os
import re
import sys
import json
import asyncio
import argparse
from io import StringIO
from typing import Union, Optional

import discord
from discord import Message, Member, Status, ChannelType
from discord import Message, Member, PartialEmoji, Status, ChannelType
from discord.raw_models import RawReactionActionEvent
from discord import Emoji
from discord.activity import Activity, ActivityType
from discord.activity import Activity
from discord.enums import ActivityType
from discord.errors import HTTPException
from discord.message import PartialMessage

# import discord_slash
# from discord_slash.utils.manage_commands import create_option
# from discord_slash.model import SlashCommandOptionType

import PythonSed
from PythonSed import Sed

Expand Down Expand Up @@ -57,10 +52,12 @@ def running_in_systemd() -> bool:
class SeanceClient(discord.Client):

def __init__(self, ref_user_id, pattern, command_prefix, *args, dm_guild_id=None, dm_manager_options=None,
sdnotify=False, default_status=False, default_presence=False, forward_pings=None, **kwargs
sdnotify=False, default_status=False, default_presence=False, forward_pings=None, proxied_emoji=[],
**kwargs
):

self.ref_user_id = ref_user_id
self.proxied_emoji = proxied_emoji

# If we weren't given an already compiled re.Pattern, compile it now.
if not isinstance(pattern, re.Pattern):
Expand Down Expand Up @@ -555,23 +552,15 @@ async def handle_custom_reaction(self, message: Message, content: str):

target = await self._get_shortcut_target(message)

group_dict = DISCORD_REACTION_SHORTCUT_PATTERN.fullmatch(content).groupdict()

# Find the emoji in the client cache.
if emoji := self.get_emoji(int(group_dict["id"])):
payload = emoji
else:
# Fail over to searching the messaage reactions.
for react in target.reactions:
if react.emoji.id == int(group_dict["id"]):
payload = react.emoji
break
# Fail out.
else:
print(f"Custom Emoji ({content[1:]}) out of scope; not directly accessible by bot or present in message reactions.", file=sys.stderr)
return
print(content)
emoji = PartialEmoji.from_str(content[1:])
print(emoji)
#
# else:
# print(f"Custom Emoji ({content[1:]}) out of scope; not directly accessible by bot or present in message reactions.", file=sys.stderr)
# return

await self._handle_reaction(target, payload, group_dict["action"] == '+')
await self._handle_reaction(target, emoji, content[0] == '+')


async def handle_newdm_command(self, accountish):
Expand Down Expand Up @@ -744,6 +733,61 @@ async def on_presence_update(self, _before: Member, after: Member):
self._cached_status = status


# Needs to be raw because message might not be in the message cache.
async def on_raw_reaction_add(self, payload: RawReactionActionEvent):

# Restrict to only reactions added by our reference user.
if payload.user_id != self.ref_user_id:
return

# Keep track of if this emoji matched our list of force proxied emoji.
# `*` is usable as a global "proxy any reactions by the reference user".
proxied = '*' in self.proxied_emoji

# We have to handle default Unicode emoji slightly differently than Discord emoji.
if payload.emoji.id is None:
# This is a Unicode emoji and the actual value is stored in name.
if payload.emoji.id in self.proxied_emoji:
proxied=True
else:
# This is a custom Discord emoji
if str(payload.emoji.id) in self.proxied_emoji:
proxied=True

# Don't do anything further if this reaction shouldn't be force proxied.
if not proxied:
return

# Try to add the given emoji and clear the other, if the add fails this will prevent clearing the existing
# reaction so you still get the reaction.
# NOTE: Due to a quirk of how Discord/discord.py works, if another user has already added a given emoji
# and that emoji is *still* on the given message when we add the reaction, we don't have to actually
# have that emoji ourselves, this means that force proxied emoji *always* work.
channel = self.get_channel(payload.channel_id)
message = channel.get_partial_message(payload.message_id)
try:
await message.add_reaction(payload.emoji)
await message.remove_reaction(payload.emoji, member=payload.member)
except HTTPException:
print('An error occurred while trying to reproxy a force proxied emoji', file=sys.stdout)


def _split_proxied_emoji(s: str) -> set[str]:
"""Split our list of proxied emoji and cleanup the result to produce a set of emoji IDs and Unicode emoji.."""

emoji = set()

# Split by non-alphanum and commas.
for item in re.split(r'\s+|,', s):
if not len(item):
# Skip blank entries.
continue

# Append to our set of items.
emoji.add(item)

return emoji

def main():

options = [
Expand Down Expand Up @@ -774,6 +818,9 @@ def main():
ConfigOption(name='Forward pings', required=False, default=False, type=bool,
help="Whether to message the proxied user upon the bot getting pinged",
),
ConfigOption(name='proxied emoji', required=False, default=set(),
help="Comma separated list of emoji or emoji IDs to always proxy when used as a reaction by the reference user."
),
]

sdnotify_available = 'sdnotify' in sys.modules
Expand All @@ -792,7 +839,6 @@ def main():
config_handler = ConfigHandler(options, env_var_prefix='SEANCE_DISCORD_', config_section='Discord',
argparse_init_kwargs={ 'prog': 'seance-discord', 'epilog': help_epilog },
)

options = config_handler.parse_all_sources()

try:
Expand Down Expand Up @@ -831,6 +877,7 @@ def main():
default_presence = options.default_presence,
forward_pings=options.forward_pings,
intents=intents,
proxied_emoji=_split_proxied_emoji(options.proxied_emoji),
)
print("Starting Séance Discord bot.")
client.run(options.token)
Expand Down

0 comments on commit 529d2f9

Please sign in to comment.