mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-26 18:35:38 +08:00
feat: add discord eba adapter
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.platform.adapters.discord.adapter import DiscordAdapter
|
||||
|
||||
__all__ = ['DiscordAdapter']
|
||||
@@ -0,0 +1,253 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import traceback
|
||||
import typing
|
||||
|
||||
import discord
|
||||
import pydantic
|
||||
|
||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
||||
import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger
|
||||
from langbot.pkg.platform.adapters.discord.api_impl import DiscordAPIMixin
|
||||
from langbot.pkg.platform.adapters.discord.event_converter import DiscordEventConverter
|
||||
from langbot.pkg.platform.adapters.discord.message_converter import DiscordMessageConverter
|
||||
from langbot.pkg.platform.adapters.discord.platform_api import PLATFORM_API_MAP
|
||||
from langbot_plugin.api.entities.builtin.platform import events as platform_events
|
||||
from langbot_plugin.api.entities.builtin.platform import message as platform_message
|
||||
|
||||
|
||||
class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatformAdapter):
|
||||
bot: discord.Client = pydantic.Field(exclude=True)
|
||||
|
||||
message_converter: DiscordMessageConverter = DiscordMessageConverter()
|
||||
event_converter: DiscordEventConverter = DiscordEventConverter()
|
||||
|
||||
config: dict
|
||||
listeners: dict[
|
||||
typing.Type[platform_events.Event],
|
||||
typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None],
|
||||
] = {}
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger):
|
||||
adapter_self = self
|
||||
|
||||
class LangBotDiscordClient(discord.Client):
|
||||
async def on_ready(self: discord.Client):
|
||||
adapter_self.bot_account_id = str(self.user.id) if self.user else ''
|
||||
await adapter_self.logger.info(f'Discord adapter running as {self.user}')
|
||||
|
||||
async def on_message(self: discord.Client, message: discord.Message):
|
||||
if self.user and message.author.id == self.user.id:
|
||||
return
|
||||
if message.author.bot:
|
||||
return
|
||||
try:
|
||||
if (
|
||||
platform_events.FriendMessage in adapter_self.listeners
|
||||
or platform_events.GroupMessage in adapter_self.listeners
|
||||
):
|
||||
legacy_event = await adapter_self.event_converter.target2legacy(message)
|
||||
callback = adapter_self.listeners.get(type(legacy_event))
|
||||
if callback:
|
||||
await callback(legacy_event, adapter_self)
|
||||
|
||||
eba_event = await adapter_self.event_converter.target2yiri(
|
||||
message, self.user.id if self.user else None
|
||||
)
|
||||
if eba_event:
|
||||
await adapter_self._dispatch_eba_event(eba_event)
|
||||
except Exception:
|
||||
await adapter_self.logger.error(f'Error in discord on_message: {traceback.format_exc()}')
|
||||
|
||||
async def on_message_edit(self: discord.Client, before: discord.Message, after: discord.Message):
|
||||
await adapter_self._dispatch_gateway_tuple(
|
||||
'message_edit', (before, after), self.user.id if self.user else None
|
||||
)
|
||||
|
||||
async def on_message_delete(self: discord.Client, message: discord.Message):
|
||||
await adapter_self._dispatch_gateway_tuple(
|
||||
'message_delete', message, self.user.id if self.user else None
|
||||
)
|
||||
|
||||
async def on_raw_message_delete(self: discord.Client, payload: discord.RawMessageDeleteEvent):
|
||||
await adapter_self._dispatch_gateway_tuple(
|
||||
'raw_message_delete',
|
||||
payload,
|
||||
self.user.id if self.user else None,
|
||||
)
|
||||
|
||||
async def on_reaction_add(
|
||||
self: discord.Client, reaction: discord.Reaction, user: discord.User | discord.Member
|
||||
):
|
||||
if self.user and user.id == self.user.id:
|
||||
return
|
||||
await adapter_self._dispatch_gateway_tuple(
|
||||
'reaction_add', (reaction, user), self.user.id if self.user else None
|
||||
)
|
||||
|
||||
async def on_reaction_remove(
|
||||
self: discord.Client, reaction: discord.Reaction, user: discord.User | discord.Member
|
||||
):
|
||||
if self.user and user.id == self.user.id:
|
||||
return
|
||||
await adapter_self._dispatch_gateway_tuple(
|
||||
'reaction_remove', (reaction, user), self.user.id if self.user else None
|
||||
)
|
||||
|
||||
async def on_raw_reaction_add(self: discord.Client, payload: discord.RawReactionActionEvent):
|
||||
if self.user and payload.user_id == self.user.id:
|
||||
return
|
||||
await adapter_self._dispatch_gateway_tuple(
|
||||
'raw_reaction_add',
|
||||
payload,
|
||||
self.user.id if self.user else None,
|
||||
)
|
||||
|
||||
async def on_raw_reaction_remove(self: discord.Client, payload: discord.RawReactionActionEvent):
|
||||
if self.user and payload.user_id == self.user.id:
|
||||
return
|
||||
await adapter_self._dispatch_gateway_tuple(
|
||||
'raw_reaction_remove',
|
||||
payload,
|
||||
self.user.id if self.user else None,
|
||||
)
|
||||
|
||||
async def on_member_join(self: discord.Client, member: discord.Member):
|
||||
await adapter_self._dispatch_gateway_tuple('member_join', member, self.user.id if self.user else None)
|
||||
|
||||
async def on_member_remove(self: discord.Client, member: discord.Member):
|
||||
await adapter_self._dispatch_gateway_tuple('member_remove', member, self.user.id if self.user else None)
|
||||
|
||||
async def on_guild_join(self: discord.Client, guild: discord.Guild):
|
||||
await adapter_self._dispatch_gateway_tuple('guild_join', guild, self.user.id if self.user else None)
|
||||
|
||||
async def on_guild_remove(self: discord.Client, guild: discord.Guild):
|
||||
await adapter_self._dispatch_gateway_tuple('guild_remove', guild, self.user.id if self.user else None)
|
||||
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
intents.members = True
|
||||
intents.reactions = True
|
||||
|
||||
args = {}
|
||||
if os.getenv('http_proxy'):
|
||||
args['proxy'] = os.getenv('http_proxy')
|
||||
bot = LangBotDiscordClient(intents=intents, **args)
|
||||
|
||||
super().__init__(
|
||||
config=config,
|
||||
logger=logger,
|
||||
bot_account_id=config.get('client_id', ''),
|
||||
listeners={},
|
||||
bot=bot,
|
||||
)
|
||||
|
||||
def get_supported_events(self) -> list[str]:
|
||||
return [
|
||||
'message.received',
|
||||
'message.edited',
|
||||
'message.deleted',
|
||||
'message.reaction',
|
||||
'group.member_joined',
|
||||
'group.member_left',
|
||||
'bot.invited_to_group',
|
||||
'bot.removed_from_group',
|
||||
'platform.specific',
|
||||
]
|
||||
|
||||
def get_supported_apis(self) -> list[str]:
|
||||
return [
|
||||
'send_message',
|
||||
'reply_message',
|
||||
'edit_message',
|
||||
'delete_message',
|
||||
'forward_message',
|
||||
'get_group_info',
|
||||
'get_group_member_list',
|
||||
'get_group_member_info',
|
||||
'get_user_info',
|
||||
'get_file_url',
|
||||
'mute_member',
|
||||
'unmute_member',
|
||||
'kick_member',
|
||||
'leave_group',
|
||||
'call_platform_api',
|
||||
]
|
||||
|
||||
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
||||
content, files = await self.message_converter.yiri2target(message)
|
||||
channel = await self._get_channel(target_id)
|
||||
kwargs = {'content': content}
|
||||
if files:
|
||||
kwargs['files'] = files
|
||||
sent = await channel.send(**kwargs)
|
||||
return platform_events.MessageResult(message_id=sent.id, raw={'message_id': sent.id})
|
||||
|
||||
async def reply_message(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
message: platform_message.MessageChain,
|
||||
quote_origin: bool = False,
|
||||
):
|
||||
assert isinstance(message_source.source_platform_object, discord.Message)
|
||||
content, files = await self.message_converter.yiri2target(message)
|
||||
kwargs = {'content': content}
|
||||
if files:
|
||||
kwargs['files'] = files
|
||||
if quote_origin:
|
||||
kwargs['reference'] = message_source.source_platform_object
|
||||
kwargs['mention_author'] = any(isinstance(component, platform_message.At) for component in message.root)
|
||||
sent = await message_source.source_platform_object.channel.send(**kwargs)
|
||||
return platform_events.MessageResult(message_id=sent.id, raw={'message_id': sent.id})
|
||||
|
||||
async def _dispatch_gateway_tuple(self, kind: str, payload, bot_user_id: int | None):
|
||||
try:
|
||||
event = await self.event_converter.target2yiri((kind, payload), bot_user_id)
|
||||
if event:
|
||||
await self._dispatch_eba_event(event)
|
||||
except Exception:
|
||||
await self.logger.error(f'Error in discord {kind}: {traceback.format_exc()}')
|
||||
|
||||
async def _dispatch_eba_event(self, event: platform_events.EBAEvent):
|
||||
for event_type in (type(event), platform_events.EBAEvent, platform_events.Event):
|
||||
callback = self.listeners.get(event_type)
|
||||
if callback:
|
||||
await callback(event, self)
|
||||
return
|
||||
|
||||
def register_listener(
|
||||
self,
|
||||
event_type: typing.Type[platform_events.Event],
|
||||
callback: typing.Callable[
|
||||
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
|
||||
],
|
||||
):
|
||||
self.listeners[event_type] = callback
|
||||
|
||||
def unregister_listener(
|
||||
self,
|
||||
event_type: typing.Type[platform_events.Event],
|
||||
callback: typing.Callable[
|
||||
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
|
||||
],
|
||||
):
|
||||
self.listeners.pop(event_type, None)
|
||||
|
||||
async def call_platform_api(self, action: str, params: dict = {}) -> dict:
|
||||
handler = PLATFORM_API_MAP.get(action)
|
||||
if handler is None:
|
||||
from langbot_plugin.api.entities.builtin.platform.errors import NotSupportedError
|
||||
|
||||
raise NotSupportedError(f'call_platform_api:{action}')
|
||||
return await handler(self.bot, params)
|
||||
|
||||
async def run_async(self):
|
||||
await self.bot.start(self.config['token'], reconnect=True)
|
||||
|
||||
async def kill(self) -> bool:
|
||||
await self.bot.close()
|
||||
return True
|
||||
@@ -0,0 +1,153 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import typing
|
||||
|
||||
import discord
|
||||
|
||||
from langbot.pkg.platform.adapters.discord.event_converter import DiscordEventConverter
|
||||
from langbot.pkg.platform.adapters.discord.message_converter import DiscordMessageConverter
|
||||
from langbot_plugin.api.entities.builtin.platform import entities as platform_entities
|
||||
from langbot_plugin.api.entities.builtin.platform import events as platform_events
|
||||
from langbot_plugin.api.entities.builtin.platform import message as platform_message
|
||||
|
||||
|
||||
class DiscordAPIMixin:
|
||||
bot: discord.Client
|
||||
|
||||
async def edit_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
chat_id: typing.Union[int, str],
|
||||
message_id: typing.Union[int, str],
|
||||
new_content: platform_message.MessageChain,
|
||||
) -> None:
|
||||
channel = await self._get_channel(chat_id)
|
||||
message = await channel.fetch_message(int(message_id))
|
||||
content, files = await DiscordMessageConverter.yiri2target(new_content)
|
||||
if files:
|
||||
await message.edit(content=content, attachments=[])
|
||||
await channel.send(content=content, files=files)
|
||||
return
|
||||
await message.edit(content=content)
|
||||
|
||||
async def delete_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
chat_id: typing.Union[int, str],
|
||||
message_id: typing.Union[int, str],
|
||||
) -> None:
|
||||
channel = await self._get_channel(chat_id)
|
||||
message = await channel.fetch_message(int(message_id))
|
||||
await message.delete()
|
||||
|
||||
async def forward_message(
|
||||
self,
|
||||
from_chat_type: str,
|
||||
from_chat_id: typing.Union[int, str],
|
||||
message_id: typing.Union[int, str],
|
||||
to_chat_type: str,
|
||||
to_chat_id: typing.Union[int, str],
|
||||
) -> platform_events.MessageResult:
|
||||
from_channel = await self._get_channel(from_chat_id)
|
||||
to_channel = await self._get_channel(to_chat_id)
|
||||
message = await from_channel.fetch_message(int(message_id))
|
||||
files = [await attachment.to_file() for attachment in message.attachments]
|
||||
sent = await to_channel.send(content=message.content, files=files)
|
||||
return platform_events.MessageResult(message_id=sent.id, raw={'message_id': sent.id})
|
||||
|
||||
async def get_group_info(self, group_id: typing.Union[int, str]) -> platform_entities.UserGroup:
|
||||
guild = await self._get_guild(group_id)
|
||||
return DiscordEventConverter.group_from_guild(guild)
|
||||
|
||||
async def get_group_member_list(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
) -> list[platform_entities.UserGroupMember]:
|
||||
guild = await self._get_guild(group_id)
|
||||
members = guild.members or [member async for member in guild.fetch_members(limit=None)]
|
||||
return [self._member_to_entity(member) for member in members]
|
||||
|
||||
async def get_group_member_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
user_id: typing.Union[int, str],
|
||||
) -> platform_entities.UserGroupMember:
|
||||
guild = await self._get_guild(group_id)
|
||||
member = guild.get_member(int(user_id)) or await guild.fetch_member(int(user_id))
|
||||
return self._member_to_entity(member)
|
||||
|
||||
async def get_user_info(self, user_id: typing.Union[int, str]) -> platform_entities.User:
|
||||
user = self.bot.get_user(int(user_id)) or await self.bot.fetch_user(int(user_id))
|
||||
return DiscordEventConverter.user_from_author(user)
|
||||
|
||||
async def upload_file(self, file_data: bytes, filename: str) -> str:
|
||||
from langbot_plugin.api.entities.builtin.platform.errors import NotSupportedError
|
||||
|
||||
raise NotSupportedError('upload_file')
|
||||
|
||||
async def get_file_url(self, file_id: str) -> str:
|
||||
return file_id
|
||||
|
||||
async def mute_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
user_id: typing.Union[int, str],
|
||||
duration: int = 0,
|
||||
) -> None:
|
||||
guild = await self._get_guild(group_id)
|
||||
member = guild.get_member(int(user_id)) or await guild.fetch_member(int(user_id))
|
||||
until = None
|
||||
if duration > 0:
|
||||
until = datetime.datetime.now(datetime.UTC) + datetime.timedelta(seconds=duration)
|
||||
await member.timeout(until, reason='LangBot EBA mute_member')
|
||||
|
||||
async def unmute_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
user_id: typing.Union[int, str],
|
||||
) -> None:
|
||||
guild = await self._get_guild(group_id)
|
||||
member = guild.get_member(int(user_id)) or await guild.fetch_member(int(user_id))
|
||||
await member.timeout(None, reason='LangBot EBA unmute_member')
|
||||
|
||||
async def kick_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
user_id: typing.Union[int, str],
|
||||
) -> None:
|
||||
guild = await self._get_guild(group_id)
|
||||
member = guild.get_member(int(user_id)) or await guild.fetch_member(int(user_id))
|
||||
await member.kick(reason='LangBot EBA kick_member')
|
||||
|
||||
async def leave_group(self, group_id: typing.Union[int, str]) -> None:
|
||||
guild = await self._get_guild(group_id)
|
||||
await guild.leave()
|
||||
|
||||
async def _get_channel(self, channel_id: typing.Union[int, str]) -> discord.abc.Messageable:
|
||||
channel = self.bot.get_channel(int(channel_id))
|
||||
if channel is None:
|
||||
channel = await self.bot.fetch_channel(int(channel_id))
|
||||
return channel
|
||||
|
||||
async def _get_guild(self, guild_id: typing.Union[int, str]) -> discord.Guild:
|
||||
guild = self.bot.get_guild(int(guild_id))
|
||||
if guild is None:
|
||||
guild = await self.bot.fetch_guild(int(guild_id))
|
||||
return guild
|
||||
|
||||
@staticmethod
|
||||
def _member_to_entity(member: discord.Member) -> platform_entities.UserGroupMember:
|
||||
role = platform_entities.MemberRole.MEMBER
|
||||
if member.guild.owner_id == member.id:
|
||||
role = platform_entities.MemberRole.OWNER
|
||||
elif member.guild_permissions.administrator or member.guild_permissions.manage_guild:
|
||||
role = platform_entities.MemberRole.ADMIN
|
||||
return platform_entities.UserGroupMember(
|
||||
user=DiscordEventConverter.user_from_author(member),
|
||||
group_id=member.guild.id,
|
||||
role=role,
|
||||
display_name=member.display_name,
|
||||
joined_at=member.joined_at.timestamp() if member.joined_at else None,
|
||||
title=member.top_role.name if member.top_role else None,
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
␍<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="80px" height="80px" viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid" fill="#000000">
|
||||
␍<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
␍<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
␍<g id="SVGRepo_iconCarrier"> <g> <path d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z" fill="#5865F2" fill-rule="nonzero"> </path> </g> </g>
|
||||
␍</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,296 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
import discord
|
||||
|
||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
||||
from langbot.pkg.platform.adapters.discord.message_converter import DiscordMessageConverter
|
||||
from langbot_plugin.api.entities.builtin.platform import entities as platform_entities
|
||||
from langbot_plugin.api.entities.builtin.platform import events as platform_events
|
||||
|
||||
|
||||
class DiscordEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
@staticmethod
|
||||
async def yiri2target(event: platform_events.Event) -> discord.Message:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
async def target2yiri(event: typing.Any, bot_user_id: int | None = None) -> platform_events.Event | None:
|
||||
if isinstance(event, discord.Message):
|
||||
return await DiscordEventConverter.message_to_eba(event)
|
||||
if isinstance(event, tuple) and len(event) == 2:
|
||||
kind, payload = event
|
||||
if kind == 'message_edit':
|
||||
before, after = payload
|
||||
return await DiscordEventConverter.message_edit_to_eba(before, after)
|
||||
if kind == 'message_delete':
|
||||
return await DiscordEventConverter.message_delete_to_eba(payload)
|
||||
if kind == 'raw_message_delete':
|
||||
return DiscordEventConverter.raw_message_delete_to_eba(payload)
|
||||
if kind == 'reaction_add':
|
||||
reaction, user = payload
|
||||
return DiscordEventConverter.reaction_to_eba(reaction, user, True)
|
||||
if kind == 'reaction_remove':
|
||||
reaction, user = payload
|
||||
return DiscordEventConverter.reaction_to_eba(reaction, user, False)
|
||||
if kind == 'raw_reaction_add':
|
||||
return DiscordEventConverter.raw_reaction_to_eba(payload, True)
|
||||
if kind == 'raw_reaction_remove':
|
||||
return DiscordEventConverter.raw_reaction_to_eba(payload, False)
|
||||
if kind == 'member_join':
|
||||
return DiscordEventConverter.member_join_to_eba(payload, bot_user_id)
|
||||
if kind == 'member_remove':
|
||||
return DiscordEventConverter.member_left_to_eba(payload, bot_user_id)
|
||||
if kind == 'guild_join':
|
||||
return DiscordEventConverter.guild_join_to_eba(payload)
|
||||
if kind == 'guild_remove':
|
||||
return DiscordEventConverter.guild_remove_to_eba(payload)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def message_to_eba(message: discord.Message) -> platform_events.MessageReceivedEvent:
|
||||
message_chain = await DiscordMessageConverter.target2yiri(message)
|
||||
group = DiscordEventConverter.group_from_message(message)
|
||||
return platform_events.MessageReceivedEvent(
|
||||
type='message.received',
|
||||
adapter_name='discord',
|
||||
message_id=message.id,
|
||||
message_chain=message_chain,
|
||||
sender=DiscordEventConverter.user_from_author(message.author),
|
||||
chat_type=platform_entities.ChatType.PRIVATE
|
||||
if isinstance(message.channel, discord.DMChannel)
|
||||
else platform_entities.ChatType.GROUP,
|
||||
chat_id=message.channel.id,
|
||||
group=group,
|
||||
timestamp=message.created_at.timestamp(),
|
||||
source_platform_object=message,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def message_edit_to_eba(
|
||||
before: discord.Message, after: discord.Message
|
||||
) -> platform_events.MessageEditedEvent:
|
||||
return platform_events.MessageEditedEvent(
|
||||
type='message.edited',
|
||||
adapter_name='discord',
|
||||
message_id=after.id,
|
||||
new_content=await DiscordMessageConverter.target2yiri(after),
|
||||
editor=DiscordEventConverter.user_from_author(after.author),
|
||||
chat_type=platform_entities.ChatType.PRIVATE
|
||||
if isinstance(after.channel, discord.DMChannel)
|
||||
else platform_entities.ChatType.GROUP,
|
||||
chat_id=after.channel.id,
|
||||
group=DiscordEventConverter.group_from_message(after),
|
||||
timestamp=after.edited_at.timestamp() if after.edited_at else after.created_at.timestamp(),
|
||||
source_platform_object=after,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def message_delete_to_eba(message: discord.Message) -> platform_events.MessageDeletedEvent:
|
||||
return platform_events.MessageDeletedEvent(
|
||||
type='message.deleted',
|
||||
adapter_name='discord',
|
||||
message_id=message.id,
|
||||
operator=None,
|
||||
chat_type=platform_entities.ChatType.PRIVATE
|
||||
if isinstance(message.channel, discord.DMChannel)
|
||||
else platform_entities.ChatType.GROUP,
|
||||
chat_id=message.channel.id,
|
||||
group=DiscordEventConverter.group_from_message(message),
|
||||
timestamp=message.created_at.timestamp() if message.created_at else 0.0,
|
||||
source_platform_object=message,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def raw_message_delete_to_eba(payload: discord.RawMessageDeleteEvent) -> platform_events.MessageDeletedEvent:
|
||||
return platform_events.MessageDeletedEvent(
|
||||
type='message.deleted',
|
||||
adapter_name='discord',
|
||||
message_id=payload.message_id,
|
||||
operator=None,
|
||||
chat_type=platform_entities.ChatType.PRIVATE
|
||||
if payload.guild_id is None
|
||||
else platform_entities.ChatType.GROUP,
|
||||
chat_id=payload.channel_id,
|
||||
group=platform_entities.UserGroup(id=payload.guild_id) if payload.guild_id is not None else None,
|
||||
source_platform_object=payload,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def reaction_to_eba(
|
||||
reaction: discord.Reaction,
|
||||
user: discord.User | discord.Member,
|
||||
is_add: bool,
|
||||
) -> platform_events.MessageReactionEvent:
|
||||
message = reaction.message
|
||||
return platform_events.MessageReactionEvent(
|
||||
type='message.reaction',
|
||||
adapter_name='discord',
|
||||
message_id=message.id,
|
||||
user=DiscordEventConverter.user_from_author(user),
|
||||
reaction=str(reaction.emoji),
|
||||
is_add=is_add,
|
||||
chat_type=platform_entities.ChatType.PRIVATE
|
||||
if isinstance(message.channel, discord.DMChannel)
|
||||
else platform_entities.ChatType.GROUP,
|
||||
chat_id=message.channel.id,
|
||||
group=DiscordEventConverter.group_from_message(message),
|
||||
source_platform_object=reaction,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def raw_reaction_to_eba(
|
||||
payload: discord.RawReactionActionEvent,
|
||||
is_add: bool,
|
||||
) -> platform_events.MessageReactionEvent:
|
||||
member = getattr(payload, 'member', None)
|
||||
user = member or getattr(payload, 'user', None)
|
||||
if user is None:
|
||||
user = platform_entities.User(id=payload.user_id)
|
||||
else:
|
||||
user = DiscordEventConverter.user_from_author(user)
|
||||
return platform_events.MessageReactionEvent(
|
||||
type='message.reaction',
|
||||
adapter_name='discord',
|
||||
message_id=payload.message_id,
|
||||
user=user,
|
||||
reaction=str(payload.emoji),
|
||||
is_add=is_add,
|
||||
chat_type=platform_entities.ChatType.PRIVATE
|
||||
if payload.guild_id is None
|
||||
else platform_entities.ChatType.GROUP,
|
||||
chat_id=payload.channel_id,
|
||||
group=platform_entities.UserGroup(id=payload.guild_id) if payload.guild_id is not None else None,
|
||||
source_platform_object=payload,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def member_join_to_eba(
|
||||
member: discord.Member,
|
||||
bot_user_id: int | None,
|
||||
) -> platform_events.BotInvitedToGroupEvent | platform_events.MemberJoinedEvent:
|
||||
group = DiscordEventConverter.group_from_guild(member.guild)
|
||||
user = DiscordEventConverter.user_from_author(member)
|
||||
if bot_user_id is not None and member.id == bot_user_id:
|
||||
return platform_events.BotInvitedToGroupEvent(
|
||||
type='bot.invited_to_group',
|
||||
adapter_name='discord',
|
||||
group=group,
|
||||
inviter=None,
|
||||
timestamp=member.joined_at.timestamp() if member.joined_at else 0.0,
|
||||
source_platform_object=member,
|
||||
)
|
||||
return platform_events.MemberJoinedEvent(
|
||||
type='group.member_joined',
|
||||
adapter_name='discord',
|
||||
group=group,
|
||||
member=user,
|
||||
inviter=None,
|
||||
join_type='direct',
|
||||
timestamp=member.joined_at.timestamp() if member.joined_at else 0.0,
|
||||
source_platform_object=member,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def member_left_to_eba(
|
||||
member: discord.Member,
|
||||
bot_user_id: int | None,
|
||||
) -> platform_events.BotRemovedFromGroupEvent | platform_events.MemberLeftEvent:
|
||||
group = DiscordEventConverter.group_from_guild(member.guild)
|
||||
user = DiscordEventConverter.user_from_author(member)
|
||||
if bot_user_id is not None and member.id == bot_user_id:
|
||||
return platform_events.BotRemovedFromGroupEvent(
|
||||
type='bot.removed_from_group',
|
||||
adapter_name='discord',
|
||||
group=group,
|
||||
operator=None,
|
||||
source_platform_object=member,
|
||||
)
|
||||
return platform_events.MemberLeftEvent(
|
||||
type='group.member_left',
|
||||
adapter_name='discord',
|
||||
group=group,
|
||||
member=user,
|
||||
is_kicked=False,
|
||||
operator=None,
|
||||
source_platform_object=member,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def guild_join_to_eba(guild: discord.Guild) -> platform_events.BotInvitedToGroupEvent:
|
||||
return platform_events.BotInvitedToGroupEvent(
|
||||
type='bot.invited_to_group',
|
||||
adapter_name='discord',
|
||||
group=DiscordEventConverter.group_from_guild(guild),
|
||||
inviter=None,
|
||||
source_platform_object=guild,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def guild_remove_to_eba(guild: discord.Guild) -> platform_events.BotRemovedFromGroupEvent:
|
||||
return platform_events.BotRemovedFromGroupEvent(
|
||||
type='bot.removed_from_group',
|
||||
adapter_name='discord',
|
||||
group=DiscordEventConverter.group_from_guild(guild),
|
||||
operator=None,
|
||||
source_platform_object=guild,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def target2legacy(message: discord.Message) -> platform_events.FriendMessage | platform_events.GroupMessage:
|
||||
message_chain = await DiscordMessageConverter.target2yiri(message)
|
||||
if isinstance(message.channel, discord.DMChannel):
|
||||
return platform_events.FriendMessage(
|
||||
sender=platform_entities.Friend(
|
||||
id=message.author.id,
|
||||
nickname=message.author.name,
|
||||
remark=str(message.channel.id),
|
||||
),
|
||||
message_chain=message_chain,
|
||||
time=message.created_at.timestamp(),
|
||||
source_platform_object=message,
|
||||
)
|
||||
return platform_events.GroupMessage(
|
||||
sender=platform_entities.GroupMember(
|
||||
id=message.author.id,
|
||||
member_name=message.author.display_name,
|
||||
permission=platform_entities.Permission.Member,
|
||||
group=platform_entities.Group(
|
||||
id=message.channel.id,
|
||||
name=message.channel.name,
|
||||
permission=platform_entities.Permission.Member,
|
||||
),
|
||||
special_title='',
|
||||
),
|
||||
message_chain=message_chain,
|
||||
time=message.created_at.timestamp(),
|
||||
source_platform_object=message,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def user_from_author(author: discord.User | discord.Member) -> platform_entities.User:
|
||||
return platform_entities.User(
|
||||
id=author.id,
|
||||
nickname=getattr(author, 'display_name', None) or author.name,
|
||||
avatar_url=str(author.display_avatar.url) if getattr(author, 'display_avatar', None) else None,
|
||||
is_bot=author.bot,
|
||||
username=author.name,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def group_from_message(message: discord.Message) -> platform_entities.UserGroup | None:
|
||||
guild = getattr(message, 'guild', None)
|
||||
if guild is None:
|
||||
return None
|
||||
return DiscordEventConverter.group_from_guild(guild)
|
||||
|
||||
@staticmethod
|
||||
def group_from_guild(guild: discord.Guild) -> platform_entities.UserGroup:
|
||||
return platform_entities.UserGroup(
|
||||
id=guild.id,
|
||||
name=guild.name,
|
||||
member_count=guild.member_count,
|
||||
avatar_url=str(guild.icon.url) if guild.icon else None,
|
||||
owner_id=guild.owner_id,
|
||||
)
|
||||
@@ -0,0 +1,89 @@
|
||||
apiVersion: v1
|
||||
kind: MessagePlatformAdapter
|
||||
|
||||
metadata:
|
||||
name: discord-eba
|
||||
label:
|
||||
en_US: Discord (EBA)
|
||||
zh_Hans: Discord (EBA)
|
||||
description:
|
||||
en_US: Discord adapter (EBA architecture)
|
||||
zh_Hans: Discord 适配器(EBA 架构版本)
|
||||
icon: discord.svg
|
||||
|
||||
spec:
|
||||
categories:
|
||||
- popular
|
||||
- global
|
||||
config:
|
||||
- name: client_id
|
||||
label:
|
||||
en_US: Client ID
|
||||
zh_Hans: 客户端 ID
|
||||
type: string
|
||||
required: true
|
||||
default: ""
|
||||
- name: token
|
||||
label:
|
||||
en_US: Token
|
||||
zh_Hans: 令牌
|
||||
type: string
|
||||
required: true
|
||||
default: ""
|
||||
|
||||
supported_events:
|
||||
- message.received
|
||||
- message.edited
|
||||
- message.deleted
|
||||
- message.reaction
|
||||
- group.member_joined
|
||||
- group.member_left
|
||||
- bot.invited_to_group
|
||||
- bot.removed_from_group
|
||||
- platform.specific
|
||||
|
||||
supported_apis:
|
||||
required:
|
||||
- send_message
|
||||
- reply_message
|
||||
optional:
|
||||
- edit_message
|
||||
- delete_message
|
||||
- forward_message
|
||||
- get_group_info
|
||||
- get_group_member_list
|
||||
- get_group_member_info
|
||||
- get_user_info
|
||||
- get_file_url
|
||||
- mute_member
|
||||
- unmute_member
|
||||
- kick_member
|
||||
- leave_group
|
||||
- call_platform_api
|
||||
|
||||
platform_specific_apis:
|
||||
- action: get_channel
|
||||
description: { en_US: "Get channel information", zh_Hans: "获取频道信息" }
|
||||
- action: get_guild
|
||||
description: { en_US: "Get guild information", zh_Hans: "获取服务器信息" }
|
||||
- action: get_guild_channels
|
||||
description: { en_US: "Get guild channels", zh_Hans: "获取服务器频道列表" }
|
||||
- action: get_guild_roles
|
||||
description: { en_US: "Get guild roles", zh_Hans: "获取服务器角色列表" }
|
||||
- action: create_invite
|
||||
description: { en_US: "Create channel invite", zh_Hans: "创建频道邀请链接" }
|
||||
- action: pin_message
|
||||
description: { en_US: "Pin a message", zh_Hans: "置顶消息" }
|
||||
- action: unpin_message
|
||||
description: { en_US: "Unpin a message", zh_Hans: "取消置顶消息" }
|
||||
- action: add_reaction
|
||||
description: { en_US: "Add a reaction", zh_Hans: "添加表情回应" }
|
||||
- action: remove_reaction
|
||||
description: { en_US: "Remove a reaction", zh_Hans: "移除表情回应" }
|
||||
- action: typing
|
||||
description: { en_US: "Send typing indicator", zh_Hans: "发送正在输入状态" }
|
||||
|
||||
execution:
|
||||
python:
|
||||
path: ./adapter.py
|
||||
attr: DiscordAdapter
|
||||
@@ -0,0 +1,162 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
|
||||
import discord
|
||||
|
||||
from langbot.pkg.utils import httpclient
|
||||
from langbot_plugin.api.entities.builtin.platform import message as platform_message
|
||||
|
||||
|
||||
class DiscordMessageConverter:
|
||||
@staticmethod
|
||||
async def yiri2target(
|
||||
message_chain: platform_message.MessageChain,
|
||||
) -> tuple[str, list[discord.File]]:
|
||||
text_parts: list[str] = []
|
||||
files: list[discord.File] = []
|
||||
|
||||
for element in list(message_chain):
|
||||
if isinstance(element, platform_message.At):
|
||||
text_parts.append(f'<@{element.target}>')
|
||||
elif isinstance(element, platform_message.AtAll):
|
||||
text_parts.append('@everyone')
|
||||
elif isinstance(element, platform_message.Plain):
|
||||
text_parts.append(element.text)
|
||||
elif isinstance(element, platform_message.Image):
|
||||
file_bytes, filename = await DiscordMessageConverter._load_image(element)
|
||||
if file_bytes:
|
||||
files.append(discord.File(fp=io.BytesIO(file_bytes), filename=filename))
|
||||
elif isinstance(element, platform_message.Voice):
|
||||
file_bytes, filename = await DiscordMessageConverter._load_voice(element)
|
||||
if file_bytes:
|
||||
files.append(discord.File(fp=io.BytesIO(file_bytes), filename=filename))
|
||||
elif isinstance(element, platform_message.File):
|
||||
file_bytes = await DiscordMessageConverter._load_file(element)
|
||||
if file_bytes:
|
||||
filename = element.name or f'{uuid.uuid4()}.bin'
|
||||
files.append(discord.File(fp=io.BytesIO(file_bytes), filename=filename))
|
||||
elif isinstance(element, platform_message.Forward):
|
||||
for node in element.node_list:
|
||||
node_text, node_files = await DiscordMessageConverter.yiri2target(node.message_chain)
|
||||
text_parts.append(node_text)
|
||||
files.extend(node_files)
|
||||
|
||||
return ''.join(text_parts), files
|
||||
|
||||
@staticmethod
|
||||
async def target2yiri(message: discord.Message) -> platform_message.MessageChain:
|
||||
message_time = datetime.datetime.fromtimestamp(int(message.created_at.timestamp()))
|
||||
elements: list[platform_message.MessageComponent] = [platform_message.Source(id=message.id, time=message_time)]
|
||||
elements.extend(DiscordMessageConverter._text_components(message.content))
|
||||
|
||||
for attachment in message.attachments:
|
||||
if DiscordMessageConverter._is_image_attachment(attachment):
|
||||
elements.append(platform_message.Image(url=attachment.url))
|
||||
else:
|
||||
elements.append(
|
||||
platform_message.File(
|
||||
name=attachment.filename,
|
||||
size=attachment.size or 0,
|
||||
url=attachment.url,
|
||||
)
|
||||
)
|
||||
|
||||
return platform_message.MessageChain(elements)
|
||||
|
||||
@staticmethod
|
||||
def _text_components(text: str) -> list[platform_message.MessageComponent]:
|
||||
if not text:
|
||||
return []
|
||||
|
||||
pattern = re.compile(r'(@everyone|@here|<@!?(\d+)>)')
|
||||
components: list[platform_message.MessageComponent] = []
|
||||
last = 0
|
||||
for match in pattern.finditer(text):
|
||||
if match.start() > last:
|
||||
components.append(platform_message.Plain(text=text[last : match.start()]))
|
||||
if match.group(1) in ('@everyone', '@here'):
|
||||
components.append(platform_message.AtAll())
|
||||
else:
|
||||
components.append(platform_message.At(target=match.group(2)))
|
||||
last = match.end()
|
||||
if last < len(text):
|
||||
components.append(platform_message.Plain(text=text[last:]))
|
||||
return components
|
||||
|
||||
@staticmethod
|
||||
async def _load_image(element: platform_message.Image) -> tuple[bytes | None, str]:
|
||||
filename = f'{uuid.uuid4()}.png'
|
||||
if element.base64:
|
||||
header, _, payload = element.base64.partition(',')
|
||||
data = payload or header
|
||||
if 'jpeg' in header or 'jpg' in header:
|
||||
filename = f'{uuid.uuid4()}.jpg'
|
||||
elif 'gif' in header:
|
||||
filename = f'{uuid.uuid4()}.gif'
|
||||
elif 'webp' in header:
|
||||
filename = f'{uuid.uuid4()}.webp'
|
||||
return base64.b64decode(data), filename
|
||||
if element.url:
|
||||
data, content_type = await DiscordMessageConverter._download(element.url)
|
||||
if 'jpeg' in content_type or 'jpg' in content_type:
|
||||
filename = f'{uuid.uuid4()}.jpg'
|
||||
elif 'gif' in content_type:
|
||||
filename = f'{uuid.uuid4()}.gif'
|
||||
elif 'webp' in content_type:
|
||||
filename = f'{uuid.uuid4()}.webp'
|
||||
return data, filename
|
||||
if element.path:
|
||||
path = os.path.abspath(element.path.replace('\x00', ''))
|
||||
if not os.path.exists(path):
|
||||
return None, filename
|
||||
with open(path, 'rb') as fp:
|
||||
data = fp.read()
|
||||
ext = os.path.splitext(path)[1]
|
||||
if ext:
|
||||
filename = f'{uuid.uuid4()}{ext}'
|
||||
return data, filename
|
||||
return None, filename
|
||||
|
||||
@staticmethod
|
||||
async def _load_voice(element: platform_message.Voice) -> tuple[bytes | None, str]:
|
||||
filename = f'{uuid.uuid4()}.mp3'
|
||||
if element.base64:
|
||||
header, _, payload = element.base64.partition(',')
|
||||
data = payload or header
|
||||
for ext in ('wav', 'mp3', 'ogg', 'm4a', 'aac', 'flac', 'opus', 'webm'):
|
||||
if ext in header:
|
||||
filename = f'{uuid.uuid4()}.{ext}'
|
||||
break
|
||||
return base64.b64decode(data), filename
|
||||
if element.url:
|
||||
data, _ = await DiscordMessageConverter._download(element.url)
|
||||
return data, filename
|
||||
return None, filename
|
||||
|
||||
@staticmethod
|
||||
async def _load_file(element: platform_message.File) -> bytes | None:
|
||||
if element.base64:
|
||||
return base64.b64decode(element.base64.split(',')[-1])
|
||||
if element.url:
|
||||
data, _ = await DiscordMessageConverter._download(element.url)
|
||||
return data
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def _download(url: str) -> tuple[bytes, str]:
|
||||
session = httpclient.get_session(trust_env=True)
|
||||
async with session.get(url) as response:
|
||||
return await response.read(), response.headers.get('Content-Type', '')
|
||||
|
||||
@staticmethod
|
||||
def _is_image_attachment(attachment: discord.Attachment) -> bool:
|
||||
content_type = attachment.content_type or ''
|
||||
return content_type.startswith('image/') or attachment.filename.lower().endswith(
|
||||
('.png', '.jpg', '.jpeg', '.gif', '.webp')
|
||||
)
|
||||
@@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
import discord
|
||||
|
||||
|
||||
async def get_channel(bot: discord.Client, params: dict) -> dict:
|
||||
channel = bot.get_channel(int(params['channel_id'])) or await bot.fetch_channel(int(params['channel_id']))
|
||||
return {
|
||||
'id': channel.id,
|
||||
'name': getattr(channel, 'name', ''),
|
||||
'type': str(channel.type),
|
||||
'guild_id': getattr(getattr(channel, 'guild', None), 'id', None),
|
||||
}
|
||||
|
||||
|
||||
async def get_guild(bot: discord.Client, params: dict) -> dict:
|
||||
guild = bot.get_guild(int(params['guild_id'])) or await bot.fetch_guild(int(params['guild_id']))
|
||||
return {'id': guild.id, 'name': guild.name, 'member_count': guild.member_count, 'owner_id': guild.owner_id}
|
||||
|
||||
|
||||
async def get_guild_channels(bot: discord.Client, params: dict) -> dict:
|
||||
guild = bot.get_guild(int(params['guild_id'])) or await bot.fetch_guild(int(params['guild_id']))
|
||||
channels = guild.channels or await guild.fetch_channels()
|
||||
return {'channels': [{'id': channel.id, 'name': channel.name, 'type': str(channel.type)} for channel in channels]}
|
||||
|
||||
|
||||
async def get_guild_roles(bot: discord.Client, params: dict) -> dict:
|
||||
guild = bot.get_guild(int(params['guild_id'])) or await bot.fetch_guild(int(params['guild_id']))
|
||||
return {'roles': [{'id': role.id, 'name': role.name, 'position': role.position} for role in guild.roles]}
|
||||
|
||||
|
||||
async def create_invite(bot: discord.Client, params: dict) -> dict:
|
||||
channel = bot.get_channel(int(params['channel_id'])) or await bot.fetch_channel(int(params['channel_id']))
|
||||
invite = await channel.create_invite(
|
||||
max_age=params.get('max_age', 0),
|
||||
max_uses=params.get('max_uses', 0),
|
||||
unique=params.get('unique', True),
|
||||
reason=params.get('reason', 'LangBot EBA create_invite'),
|
||||
)
|
||||
return {'url': invite.url, 'code': invite.code}
|
||||
|
||||
|
||||
async def pin_message(bot: discord.Client, params: dict) -> dict:
|
||||
channel = bot.get_channel(int(params['channel_id'])) or await bot.fetch_channel(int(params['channel_id']))
|
||||
message = await channel.fetch_message(int(params['message_id']))
|
||||
await message.pin(reason=params.get('reason', 'LangBot EBA pin_message'))
|
||||
return {'ok': True}
|
||||
|
||||
|
||||
async def unpin_message(bot: discord.Client, params: dict) -> dict:
|
||||
channel = bot.get_channel(int(params['channel_id'])) or await bot.fetch_channel(int(params['channel_id']))
|
||||
message = await channel.fetch_message(int(params['message_id']))
|
||||
await message.unpin(reason=params.get('reason', 'LangBot EBA unpin_message'))
|
||||
return {'ok': True}
|
||||
|
||||
|
||||
async def add_reaction(bot: discord.Client, params: dict) -> dict:
|
||||
channel = bot.get_channel(int(params['channel_id'])) or await bot.fetch_channel(int(params['channel_id']))
|
||||
message = await channel.fetch_message(int(params['message_id']))
|
||||
await message.add_reaction(params['emoji'])
|
||||
return {'ok': True}
|
||||
|
||||
|
||||
async def remove_reaction(bot: discord.Client, params: dict) -> dict:
|
||||
channel = bot.get_channel(int(params['channel_id'])) or await bot.fetch_channel(int(params['channel_id']))
|
||||
message = await channel.fetch_message(int(params['message_id']))
|
||||
user = (
|
||||
bot.user
|
||||
if 'user_id' not in params
|
||||
else bot.get_user(int(params['user_id'])) or await bot.fetch_user(int(params['user_id']))
|
||||
)
|
||||
await message.remove_reaction(params['emoji'], user)
|
||||
return {'ok': True}
|
||||
|
||||
|
||||
async def send_typing(bot: discord.Client, params: dict) -> dict:
|
||||
channel = bot.get_channel(int(params['channel_id'])) or await bot.fetch_channel(int(params['channel_id']))
|
||||
async with channel.typing():
|
||||
return {'ok': True}
|
||||
|
||||
|
||||
PLATFORM_API_MAP: dict[str, typing.Callable[[discord.Client, dict], typing.Awaitable[dict]]] = {
|
||||
'get_channel': get_channel,
|
||||
'get_guild': get_guild,
|
||||
'get_guild_channels': get_guild_channels,
|
||||
'get_guild_roles': get_guild_roles,
|
||||
'create_invite': create_invite,
|
||||
'pin_message': pin_message,
|
||||
'unpin_message': unpin_message,
|
||||
'add_reaction': add_reaction,
|
||||
'remove_reaction': remove_reaction,
|
||||
'typing': send_typing,
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
import pydantic
|
||||
|
||||
|
||||
class DiscordAdapterConfig(pydantic.BaseModel):
|
||||
client_id: str
|
||||
token: str
|
||||
guild_id: typing.Optional[str] = None
|
||||
debug_channel_id: typing.Optional[str] = None
|
||||
@@ -0,0 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# Voice support is still implemented by the legacy Discord source adapter. The
|
||||
# EBA adapter exposes text, guild, member, moderation, and platform-specific APIs
|
||||
# first; voice-specific EBA actions will move here when that surface is migrated.
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#29b6f6" d="M24,4C13,4,4,13,4,24s9,20,20,20s20-9,20-20S35,4,24,4z"/><path fill="#fff" d="M34,15l-3.7,19.1c0,0-0.2,0.9-1.2,0.9c-0.6,0-0.9-0.3-0.9-0.3L20,28l-4-2l-5.1-1.4c0,0-0.9-0.3-0.9-1 c0-0.6,0.9-0.9,0.9-0.9l21.3-8.5c0,0,0.7-0.2,1.1-0.2c0.3,0,0.6,0.1,0.6,0.5C34,14.8,34,15,34,15z"/><path fill="#b0bec5" d="M23,30.5l-3.4,3.4c0,0-0.1,0.1-0.3,0.1c-0.1,0-0.1,0-0.2,0l1-6L23,30.5z"/><path fill="#cfd8dc" d="M29.9,18.2c-0.2-0.2-0.5-0.3-0.7-0.1L16,26c0,0,2.1,5.9,2.4,6.9c0.3,1,0.6,1,0.6,1l1-6l9.8-9.1 C30,18.7,30.1,18.4,29.9,18.2z"/></svg>
|
||||
|
After Width: | Height: | Size: 634 B |
Reference in New Issue
Block a user