diff --git a/ast/snd/broken_national_anthem.mp3 b/ast/snd/broken_national_anthem.mp3 new file mode 100644 index 0000000..ff39bca Binary files /dev/null and b/ast/snd/broken_national_anthem.mp3 differ diff --git a/ast/snd/broken_national_anthem.wav b/ast/snd/broken_national_anthem.wav deleted file mode 100644 index 43a590a..0000000 Binary files a/ast/snd/broken_national_anthem.wav and /dev/null differ diff --git a/config.py b/config.py new file mode 100644 index 0000000..1753a13 --- /dev/null +++ b/config.py @@ -0,0 +1,26 @@ +import os +import re +from typing import List + +from discord import Intents + +DISCORD_TOKEN = os.environ["IMPERIAL_POLICE_TOKEN"] +DISCORD_INTENTS: Intents = Intents.all() + +ROYAL_ROOM_ID: int = int(os.environ["IMPERIAL_POLICE_ROYAL_ROOM_ID"]) +ROYAL_QUALIFICATION_ROLE_ID: int = int(os.environ["IMPERIAL_POLICE_ROYAL_QUALIFICATION_ID"]) +PRISON_CHANNEL_ID: int = int(os.environ["IMPERIAL_POLICE_PRISON_CHANNEL_ID"]) +MESSAGE_CHANNEL_ID: int = int(os.environ["IMPERIAL_POLICE_MESSAGE_CHANNEL_ID"]) +BOT_EXCEPTION_IDS: List[int] = [int(i) for i in os.environ["IMPERIAL_POLICE_BOT_EXCEPTION_IDS"].split("@")] + +NATIONAL_ANTHEM: str = "ast/snd/broken_national_anthem.mp3" +VC_STAY_LENGTH: int = 71 +ROYAL_EMBLEM_URL: str = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/Imperial_Seal_of_Japan.svg/500px-Imperial_Seal_of_Japan.svg.png" +ERROR_CROSS_URL: str = "https://illust8.com/wp-content/uploads/2018/08/mark_batsu_illust_898.png" +EXECUTION_REASON: str = "皇宮警察だ!!!" +MAINTAINER_DISCORD_ID: int = 554985192549515264 + +VCDIFF_REGEXES: List[re.Pattern] = [ + re.compile(r"(.*?)が.*に入りました"), + re.compile(r"(.*?)が.*から抜けました") +] diff --git a/docker-compose.yml b/docker-compose.yml index 920ddae..562ce88 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,4 +7,9 @@ services: container_name: 'imperial-police' environment: - IMPERIAL_POLICE_TOKEN=${IMPERIAL_POLICE_TOKEN} + - IMPERIAL_POLICE_ROYAL_ROOM_ID=${IMPERIAL_POLICE_ROYAL_ROOM_ID} + - IMPERIAL_POLICE_ROYAL_QUALIFICATION_ID=${IMPERIAL_POLICE_ROYAL_QUALIFICATION_ID} + - IMPERIAL_POLICE_PRISON_CHANNEL_ID=${IMPERIAL_POLICE_PRISON_CHANNEL_ID} + - IMPERIAL_POLICE_MESSAGE_CHANNEL_ID=${IMPERIAL_POLICE_MESSAGE_CHANNEL_ID} + - IMPERIAL_POLICE_BOT_EXCEPTION_IDS=${IMPERIAL_POLICE_BOT_EXCEPTION_IDS} tty: true diff --git a/lib/utils.py b/lib/utils.py deleted file mode 100644 index dcb739b..0000000 --- a/lib/utils.py +++ /dev/null @@ -1,4 +0,0 @@ -def is_empty(target: list): - if len(target) == 0: - return True - return False diff --git a/main.py b/main.py index 0977c1c..cbc4bef 100644 --- a/main.py +++ b/main.py @@ -1,127 +1,6 @@ -import asyncio -import os -import random -import re - -import discord -import datetime - -from lib.utils import is_empty - - -ROYAL_EMBLEM_URL = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/Imperial_Seal_of_Japan.svg/500px-Imperial_Seal_of_Japan.svg.png" -NAME_REGEX_IN = re.compile(r"(.*?)が.*に入りました") -NAME_REGEX_OUT = re.compile(r"(.*?)が.*から抜けました") -ROYAL_ROOM_ID = 727133544773845013 -LAWLESS_CHANNEL_ID = 690909527461199922 -PRISON_CHANNEL_ID = 724591472061579295 -ROYAL_QUALIFICATION_ROLE_ID = 727046372456661012 - -NATIONAL_ANTHEM = "ast/snd/broken_national_anthem.wav" - - -class MainClient(discord.Client): - def __init__(self, token) -> None: - super().__init__() - self.token: str = token - self.guild: discord.Guild = None - self.royal_family: list[discord.Member] = [] - self.royal_family_ids: list[int] = [] - self.royal_qualification: discord.Role = None - self.royal_room: discord.VoiceChannel = None - self.prison_channel: discord.VoiceChannel = None - self.lawless_channel: discord.TextChannel = None - self.is_voice_connected: bool = False - - def run(self) -> None: - super().run(self.token) - - async def on_ready(self) -> None: - self.guild = self.guilds[0] - self.royal_qualification = self.guild.get_role(ROYAL_QUALIFICATION_ROLE_ID) - self.royal_room = self.guild.get_channel(ROYAL_ROOM_ID) - self.prison_channel = self.get_channel(PRISON_CHANNEL_ID) - self.lawless_channel = self.get_channel(LAWLESS_CHANNEL_ID) - self.royal_family = self.royal_qualification.members - self.royal_family_ids = [royal_user.id for royal_user in self.royal_family] - - async def on_message(self, message: discord.Message): - if "???" in message.content and not message.author.bot: - length = int(random.randint(1, 30)) - content = questions(length) - await message.channel.send(content) - - if not message.embeds: - return - embed = message.embeds[0] - if includes(embed.thumbnail.url, self.royal_family_ids): - if "皇室" not in embed.title: - return - if embed.description == "何かが始まる予感がする。": - parsed_display_name = re.findall(NAME_REGEX_IN, embed.title)[0] - await message.channel.send(embed=embed_factory(parsed_display_name, self.user.id, self.user.avatar, True)) - elif embed.description == "あいつは良い奴だったよ...": - parsed_display_name = re.findall(NAME_REGEX_OUT, embed.title)[0] - await message.channel.send(embed=embed_factory(parsed_display_name, self.user.id, self.user.avatar, False)) - else: - return - await message.delete(delay=None) - - async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState): - if after.channel != self.royal_room: - return - if member not in self.royal_family: - await self.execution(member) - - async def execution(self, member): - await member.move_to(self.prison_channel, reason="皇宮警察だ!!!") - if not is_empty(self.voice_clients): - return - try: - voice_client = await self.prison_channel.connect(reconnect=False) - voice_client.play(discord.FFmpegPCMAudio(source=NATIONAL_ANTHEM)) - await asyncio.sleep(67) - await voice_client.disconnect(force=True) - except Exception as caught_exception: - await self.lawless_channel.send(caught_exception) - - -def includes(query: str, search_from: list): - for test_case in search_from: - if str(test_case) in str(query): - return True - return False - - -def embed_factory(member_name: str, my_id: int, my_avatar: str, is_in: bool) -> discord.Embed: - message_in_or_out = "還幸" if is_in else "行幸" - embed = discord.Embed( - title="†卍 {} 卍† ".format(message_in_or_out), - description="{} が{}なさいました。".format(member_name, message_in_or_out), - color=0xffd800) - embed.set_author( - name="皇宮警察からのお知らせ", - icon_url="https://cdn.discordapp.com/avatars/{}/{}.png".format(my_id, my_avatar)) - embed.set_thumbnail(url=ROYAL_EMBLEM_URL) - return embed - - -def questions(length: int) -> str: - result = "" - - for n in range(length): - random_num = random.randint(0, 1) - - if random_num: - result += "?" - else: - result += "?" - - return result - - -if __name__ == "__main__": - TOKEN = os.getenv("IMPERIAL_POLICE_TOKEN") - - client = MainClient(TOKEN) - client.run() +from src.client.main_client import MainClient + + +if __name__ == "__main__": + bot: MainClient = MainClient() + bot.launch() diff --git a/requirements.txt b/requirements.txt index c7827af..a8d4fc0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -discord.py~=1.3.4 -requests~=2.20.0 -lxml~=4.5.0 -pynacl~=1.3.0 +discord.py~=1.5.1 +requests>=2.25.1 +lxml==4.6.3 +pynacl==1.4.0 +mutagen==1.45.1 diff --git a/src/client/global_client.py b/src/client/global_client.py new file mode 100644 index 0000000..f1c6028 --- /dev/null +++ b/src/client/global_client.py @@ -0,0 +1,15 @@ +from typing import Optional + +import discord + +from src.utils.discd import * + + +class GlobalClient: + client: Optional[discord.Client] = None + guild: Optional[discord.Guild] = None + + @classmethod + def static_init(cls, client: discord.Client): + cls.client = client + cls.guild = client.guilds[0] diff --git a/src/client/main_client.py b/src/client/main_client.py new file mode 100644 index 0000000..3aa35b3 --- /dev/null +++ b/src/client/main_client.py @@ -0,0 +1,70 @@ +from typing import Optional + +import discord + +from config import DISCORD_TOKEN, DISCORD_INTENTS, BOT_EXCEPTION_IDS, MESSAGE_CHANNEL_ID +from src.client.global_client import GlobalClient +from src.service.embed.exception import ExceptionEmbedFactory +from src.handler.message import MessageHandler +from src.handler.voice import VoiceHandler + + +class MainClient(discord.Client): + def __init__(self) -> None: + super(MainClient, self).__init__(intents=DISCORD_INTENTS) + + self.MESSAGE_CHANNEL: Optional[discord.TextChannel] = None + + def launch(self) -> None: + self.run(DISCORD_TOKEN) + + async def on_ready(self) -> None: + number_of_guilds: int = len(self.guilds) + if number_of_guilds != 1: + raise RuntimeError("Error: This bot can run in only one server." + "But You are trying to run in {} server(s).".format(number_of_guilds)) + + self.MESSAGE_CHANNEL = self.get_channel(MESSAGE_CHANNEL_ID) + + client: discord.Client = super(MainClient, self) + GlobalClient.static_init(client) + + me_as_member: discord.Member = GlobalClient.guild.get_member(self.user.id) + my_voice_state: discord.VoiceState = me_as_member.voice + + if my_voice_state is None: + return + + if my_voice_state.channel is not None: + await me_as_member.move_to(None) + + async def on_message(self, message: discord.Message): + if message.author.bot and message.author.id not in BOT_EXCEPTION_IDS: + return + + try: + await MessageHandler(message).handle() + except Exception as e: + embed = ExceptionEmbedFactory().make(e) + await self.MESSAGE_CHANNEL.send(embed=embed) + raise e + + async def on_voice_state_update( + self, + member: discord.Member, + before: discord.VoiceState, + after: discord.VoiceState + ): + if member.id == self.user.id: + await VoiceHandler(member, before, after).handle(is_me=True) + return + + if member.bot: + return + + try: + await VoiceHandler(member, before, after).handle() + except Exception as e: + embed = ExceptionEmbedFactory().make(e) + await self.MESSAGE_CHANNEL.send(embed=embed) + raise e diff --git a/src/exceptions/misunderstanding.py b/src/exceptions/misunderstanding.py new file mode 100644 index 0000000..9a9fd58 --- /dev/null +++ b/src/exceptions/misunderstanding.py @@ -0,0 +1,2 @@ +class MisunderstandingException(Exception): + pass diff --git a/src/handler/me_handler.py b/src/handler/me_handler.py new file mode 100644 index 0000000..e28c26d --- /dev/null +++ b/src/handler/me_handler.py @@ -0,0 +1,35 @@ +from typing import Optional + +import discord + +from config import PRISON_CHANNEL_ID +from src.service.voice.play_sound import PlaySound +from src.utils.discd import unmute +from src.client.global_client import GlobalClient + + +class MeHandler: + PRISON_CHANNEL: Optional[discord.VoiceChannel] = None + + def __init__(self, me: discord.Member, before: discord.VoiceState, after: discord.VoiceState): + MeHandler.static_checker() + + self.me: discord.Member = me + self.before: discord.VoiceState = before + self.after: discord.VoiceState = after + + @classmethod + def static_checker(cls): + if cls.PRISON_CHANNEL is None: + cls.PRISON_CHANNEL = GlobalClient.client.get_channel(PRISON_CHANNEL_ID) + + async def handle(self): + if self.after.mute: + await unmute(self.me) + + if self.after.channel is None: + await PlaySound.disconnected() + return + + if self.after.channel.id != PRISON_CHANNEL_ID and self.after.channel is not None: + await self.me.move_to(MeHandler.PRISON_CHANNEL) diff --git a/src/handler/message.py b/src/handler/message.py new file mode 100644 index 0000000..ab2f5d1 --- /dev/null +++ b/src/handler/message.py @@ -0,0 +1,22 @@ +import discord + +from src.service.message.questions import ManyQuestions +from src.service.message.vcdiff_cleaner import VCDiffCleaner + + +class MessageHandler: + def __init__(self, message): + self.message: discord.Message = message + + async def handle(self): + questions: ManyQuestions = ManyQuestions(self.message) + vcdiff_cleaner: VCDiffCleaner = VCDiffCleaner(self.message) + + questions_trigger: bool = questions.is_triggered() + vcdiff_cleaner_trigger: bool = vcdiff_cleaner.is_triggered() + + if questions_trigger: + await questions.execute() + + if vcdiff_cleaner_trigger: + await vcdiff_cleaner.execute() diff --git a/src/handler/voice.py b/src/handler/voice.py new file mode 100644 index 0000000..4f5a1d9 --- /dev/null +++ b/src/handler/voice.py @@ -0,0 +1,46 @@ +import discord + +from src.handler.me_handler import MeHandler +from src.service.voice.royal_embed import RoyalEmbed +from src.service.voice.mover import Mover +from src.service.voice.play_sound import PlaySound + + +class VoiceHandler: + def __init__(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState): + self.member: discord.Member = member + self.before: discord.VoiceState = before + self.after: discord.VoiceState = after + + self.is_join: bool + + if before.channel is None and after.channel is not None: + self.is_join = True + elif before.channel is not None and after.channel is None: + self.is_join = False + elif before.channel != after.channel: + self.is_join = True + else: + self.is_join = None + + async def handle(self, is_me: bool = False): + if is_me: + my_handler: MeHandler = MeHandler(self.member, self.before, self.after) + await my_handler.handle() + + royals: RoyalEmbed = RoyalEmbed(self.before, self.after, self.member, self.is_join) + mover: Mover = Mover(self.member, self.after, self.is_join) + sound: PlaySound = PlaySound(self.member, self.after, self.is_join) + + royals_trigger: bool = royals.is_triggered() + mover_trigger: bool = mover.is_triggered() + sound_trigger: bool = await sound.is_triggered() + + if royals_trigger: + await royals.execute() + + if mover_trigger: + await mover.execute() + + if sound_trigger: + await sound.execute() diff --git a/src/service/embed/embed_factory.py b/src/service/embed/embed_factory.py new file mode 100644 index 0000000..7d28fab --- /dev/null +++ b/src/service/embed/embed_factory.py @@ -0,0 +1,49 @@ +from typing import Optional + +import discord + +from config import ROYAL_EMBLEM_URL +from src.utils.discd import get_user_icon_url +from src.client.global_client import GlobalClient + + +class EmbedFactory: + MY_AVATAR_URL: Optional[str] = None + BASE_EMBED: Optional[discord.Embed] = None + + def __init__(self) -> None: + EmbedFactory.static_check() + + @classmethod + def static_check(cls): + if cls.MY_AVATAR_URL is None: + cls.MY_AVATAR_URL = get_user_icon_url(GlobalClient.client.user) + if cls.BASE_EMBED is None: + cls.BASE_EMBED = cls._make_base_embed() + + def make(self, member: discord.Member, is_join: bool) -> discord.Embed: + embed: discord.Embed = EmbedFactory.BASE_EMBED + + customize_message: str + if is_join: + customize_message = "還幸" + else: + customize_message = "行幸" + + embed.title = "†卍 {} 卍†".format(customize_message) + embed.description = "{} が{}なさいました。".format(member.display_name, customize_message) + + return embed + + @staticmethod + def _make_base_embed() -> discord.Embed: + embed = discord.Embed( + color=0xffd800 + ) + embed.set_author( + name="皇宮警察からのお知らせ", + icon_url=EmbedFactory.MY_AVATAR_URL + ) + embed.set_thumbnail(url=ROYAL_EMBLEM_URL) + + return embed diff --git a/src/service/embed/exception.py b/src/service/embed/exception.py new file mode 100644 index 0000000..2f8473e --- /dev/null +++ b/src/service/embed/exception.py @@ -0,0 +1,37 @@ +from typing import Optional + +import discord + +from config import ERROR_CROSS_URL, MAINTAINER_DISCORD_ID +from src.exceptions.misunderstanding import MisunderstandingException +from src.utils.discd import get_user_icon_url +from src.client.global_client import GlobalClient + + +class ExceptionEmbedFactory: + MY_AVATAR_URL: Optional[str] = None + + def __init__(self) -> None: + ExceptionEmbedFactory.static_check() + + @classmethod + def static_check(cls): + if cls.MY_AVATAR_URL is None: + cls.MY_AVATAR_URL = get_user_icon_url(GlobalClient.client.user) + + def make(self, caught_error: Exception) -> discord.Embed: + embed: discord.Embed = discord.Embed( + title="お例外が発生あそばされました", + color=0xed2102 + ) + + embed.set_author(name="皇宮警察からのお知らせ", icon_url=ExceptionEmbedFactory.MY_AVATAR_URL) + embed.set_thumbnail(url=ERROR_CROSS_URL) + + embed.add_field(name="例外の内容", value="```\n{}\n```".format(caught_error)) + + if type(caught_error) is MisunderstandingException: + embed.description = "この例外は <@!{}> が仕様を適当に推測して実装した箇所で、".format(MAINTAINER_DISCORD_ID) + \ + "起こらないと思っていた事態が起こっていることを示しています。" + + return embed diff --git a/src/service/message/message_abs.py b/src/service/message/message_abs.py new file mode 100644 index 0000000..bffc843 --- /dev/null +++ b/src/service/message/message_abs.py @@ -0,0 +1,17 @@ +import discord + +import abc + + +class MessageFunctionAbstract(metaclass=abc.ABCMeta): + @abc.abstractmethod + def __init__(self, message: discord.Message): + pass + + @abc.abstractmethod + def is_triggered(self) -> bool: + pass + + @abc.abstractmethod + async def execute(self, **kwargs): + pass diff --git a/src/service/message/questions.py b/src/service/message/questions.py new file mode 100644 index 0000000..9db1aa7 --- /dev/null +++ b/src/service/message/questions.py @@ -0,0 +1,45 @@ +import random +from abc import ABC +from typing import Optional + +import discord + +from src.service.message.message_abs import MessageFunctionAbstract + + +class ManyQuestions(MessageFunctionAbstract, ABC): + def __init__(self, message: discord.Message): + self.message: discord.Message = message + self._is_triggered: Optional[bool] = None + + def is_triggered(self) -> bool: + if self.message.author.bot: + return self._is_triggered + if "???" in self.message.content: + self._is_triggered = True + else: + self._is_triggered = False + + return self._is_triggered + + async def execute(self): + if (self._is_triggered is None) or (not self._is_triggered): + return + + length = int(random.randint(1, 30)) + content = ManyQuestions._create_questions_content(length) + await self.message.channel.send(content) + + @staticmethod + def _create_questions_content(length: int) -> str: + result = "" + + for n in range(length): + random_num = random.randint(0, 1) + + if random_num: + result += "?" + else: + result += "?" + + return result diff --git a/src/service/message/vcdiff_cleaner.py b/src/service/message/vcdiff_cleaner.py new file mode 100644 index 0000000..95182f9 --- /dev/null +++ b/src/service/message/vcdiff_cleaner.py @@ -0,0 +1,41 @@ +import re +from abc import ABC +from typing import Optional, List + +import discord + +from config import VCDIFF_REGEXES +from src.service.message.message_abs import MessageFunctionAbstract +from src.service.misc.royal_judge import RoyalJudge +from src.utils import utils + + +class VCDiffCleaner(MessageFunctionAbstract, ABC): + def __init__(self, message: discord.Message): + self.message: discord.Message = message + self._is_triggered: Optional[bool] = False + + def is_triggered(self) -> bool: + self._is_triggered = False + + if len(self.message.embeds) != 1: + return self._is_triggered + + embed = self.message.embeds[0] + + royal_family_ids: List[int] = RoyalJudge.get_royal_member_id_list() + if not utils.includes(embed.thumbnail.url, royal_family_ids): + return self._is_triggered + + if RoyalJudge.get_royal_room().name not in embed.title: + return self._is_triggered + + for regex in VCDIFF_REGEXES: + if re.search(regex, embed.title): + self._is_triggered = True + return self._is_triggered + + return self._is_triggered + + async def execute(self): + await self.message.delete(delay=None) diff --git a/src/service/misc/royal_judge.py b/src/service/misc/royal_judge.py new file mode 100644 index 0000000..ceaeaa5 --- /dev/null +++ b/src/service/misc/royal_judge.py @@ -0,0 +1,48 @@ +from typing import List, Optional + +import discord + +from config import ROYAL_QUALIFICATION_ROLE_ID, ROYAL_ROOM_ID +from src.client.global_client import GlobalClient + + +class RoyalJudge: + ROYAL_ROOM: Optional[discord.VoiceChannel] = None + ROYAL_QUALIFICATION_ROLE: Optional[discord.Role] = None + + @classmethod + def static_checker(cls): + if cls.ROYAL_ROOM is None: + cls.ROYAL_ROOM = GlobalClient.client.get_channel(ROYAL_ROOM_ID) + if cls.ROYAL_QUALIFICATION_ROLE is None: + cls.ROYAL_QUALIFICATION_ROLE = GlobalClient.guild.get_role(ROYAL_QUALIFICATION_ROLE_ID) + + @classmethod + def get_royal_role(cls) -> discord.Role: + cls.static_checker() + + return cls.ROYAL_QUALIFICATION_ROLE + + @classmethod + def get_royal_room(cls) -> discord.VoiceChannel: + cls.static_checker() + + return cls.ROYAL_ROOM + + @classmethod + def get_royal_member_id_list(cls) -> List[int]: + cls.static_checker() + + return [m.id for m in cls.ROYAL_QUALIFICATION_ROLE.members] + + @classmethod + def is_royal_family_member_from_id(cls, member_id: int) -> bool: + cls.static_checker() + + return member_id in cls.get_royal_member_id_list() + + @classmethod + def is_royal_family_member(cls, member: discord.Member) -> bool: + cls.static_checker() + + return cls.is_royal_family_member_from_id(member.id) diff --git a/src/service/voice/mover.py b/src/service/voice/mover.py new file mode 100644 index 0000000..275cef4 --- /dev/null +++ b/src/service/voice/mover.py @@ -0,0 +1,45 @@ +from typing import Optional +from abc import ABC + +import discord + +from config import PRISON_CHANNEL_ID, EXECUTION_REASON +from src.service.voice.voice_abs import VoiceFunctionAbstract +from src.service.misc.royal_judge import RoyalJudge +from src.client.global_client import GlobalClient + + +class Mover(VoiceFunctionAbstract, ABC): + PRISON_CHANNEL: Optional[discord.VoiceChannel] = None + + @classmethod + def static_check(cls): + if cls.PRISON_CHANNEL is None: + cls.PRISON_CHANNEL = GlobalClient.client.get_channel(PRISON_CHANNEL_ID) + + def __init__(self, member: discord.Member, after: discord.VoiceState, is_join: bool): + Mover.static_check() + + self._is_triggered: bool = False + + self.member: discord.Member = member + self.after: discord.VoiceState = after + self.is_join: bool = is_join + + def is_triggered(self) -> bool: + self._is_triggered = False + + if not self.is_join: + return self._is_triggered + + if self.after.channel.id != RoyalJudge.get_royal_room().id: + return self._is_triggered + + if RoyalJudge.is_royal_family_member_from_id(self.member.id): + return self._is_triggered + + self._is_triggered = True + return self._is_triggered + + async def execute(self): + await self.member.move_to(Mover.PRISON_CHANNEL, reason=EXECUTION_REASON) diff --git a/src/service/voice/play_sound.py b/src/service/voice/play_sound.py new file mode 100644 index 0000000..557eda3 --- /dev/null +++ b/src/service/voice/play_sound.py @@ -0,0 +1,91 @@ +import asyncio +from abc import ABC +from typing import Optional + +import discord + +from config import NATIONAL_ANTHEM, VC_STAY_LENGTH, PRISON_CHANNEL_ID +from src.service.voice.voice_abs import VoiceFunctionAbstract +from src.service.misc.royal_judge import RoyalJudge +from src.exceptions.misunderstanding import MisunderstandingException +from src.client.global_client import GlobalClient + + +class PlaySound(VoiceFunctionAbstract, ABC): + IS_EXECUTING: bool = False + PRISON_CHANNEL: Optional[discord.VoiceChannel] = None + + @classmethod + async def disconnected(cls): + cls.IS_EXECUTING = False + + number_of_voice_clients: int = len(GlobalClient.client.voice_clients) + if number_of_voice_clients > 1: + raise MisunderstandingException("The coder thought there can be only one or no voice client," + "but got {}.".format(number_of_voice_clients)) + + if number_of_voice_clients == 1: + await GlobalClient.client.voice_clients[0].disconnect() + + def __init__(self, member: discord.Member, after: discord.VoiceState, is_join: bool): + PlaySound.static_check() + + self.member: discord.Member = member + self.is_join: bool = is_join + self.after: discord.VoiceState = after + self._is_triggered: Optional[bool] = False + + @classmethod + def static_check(cls): + if cls.PRISON_CHANNEL is None: + cls.PRISON_CHANNEL = GlobalClient.client.get_channel(PRISON_CHANNEL_ID) + + async def is_triggered(self) -> bool: + self._is_triggered = False + + if PlaySound.IS_EXECUTING: + return self._is_triggered + + if not self.is_join and self.is_join is not None: + return self._is_triggered + + if self.after.channel.id != RoyalJudge.get_royal_room().id: + return self._is_triggered + + if RoyalJudge.is_royal_family_member_from_id(self.member.id): + return self._is_triggered + + number_of_voice_clients: int = len(GlobalClient.client.voice_clients) + if number_of_voice_clients > 1: + raise MisunderstandingException("The coder thought there can be only one or no voice client," + "but got {}.".format(number_of_voice_clients)) + + self._is_triggered = True + return self._is_triggered + + async def execute(self): + if not self._is_triggered: + return + + voice_client: discord.VoiceClient = await PlaySound.PRISON_CHANNEL.connect(reconnect=False) + voice_client.play(discord.FFmpegPCMAudio(NATIONAL_ANTHEM)) + PlaySound.IS_EXECUTING = True + + await asyncio.sleep(VC_STAY_LENGTH) + await voice_client.disconnect(force=True) + + # async def do(self): + # """ + # 実際にPartyIchiyoを実行する + # """ + # voice_client = await self.base_voice_channel.connect(reconnect=False) + # if self.music == 0: + # chosen_music = random.choice(list(PartyIchiyo.MUSICS_LIST.values())) + # voice_client.play(discord.FFmpegPCMAudio(chosen_music)) + # else: + # chosen_music = PartyIchiyo.MUSICS_LIST[self.music] + # voice_client.play(discord.FFmpegPCMAudio(chosen_music)) + # await self.kikisen_channel.send("パーティー Nigth") + # sleep_time = MP3(chosen_music).info.length + # await asyncio.sleep(sleep_time + 0.5) + # await voice_client.disconnect(force=True) diff --git a/src/service/voice/royal_embed.py b/src/service/voice/royal_embed.py new file mode 100644 index 0000000..64a0fb7 --- /dev/null +++ b/src/service/voice/royal_embed.py @@ -0,0 +1,56 @@ +from abc import ABC +from typing import Optional + +import discord + +from config import MESSAGE_CHANNEL_ID, ROYAL_ROOM_ID +from src.service.voice.voice_abs import VoiceFunctionAbstract +from src.service.embed.embed_factory import EmbedFactory +from src.service.misc.royal_judge import RoyalJudge +from src.client.global_client import GlobalClient + + +class RoyalEmbed(VoiceFunctionAbstract, ABC): + MESSAGE_CHANNEL: Optional[discord.TextChannel] = None + + def __init__( + self, + before: discord.VoiceState, + after: discord.VoiceState, + member: discord.Member, + is_join: Optional[bool] + ): + RoyalEmbed.static_check() + + self._is_triggered = False + self.before: discord.VoiceState = before + self.after: discord.VoiceState = after + self.member: discord.Member = member + self.is_join: Optional[bool] = is_join + + @classmethod + def static_check(cls): + if cls.MESSAGE_CHANNEL is None: + cls.MESSAGE_CHANNEL = GlobalClient.client.get_channel(MESSAGE_CHANNEL_ID) + + def is_triggered(self) -> bool: + self._is_triggered = False + + if not RoyalJudge.is_royal_family_member_from_id(self.member.id): + return self._is_triggered + + if self.is_join is None: + return self._is_triggered + + if self.after.channel is None: + if self.before.channel.id == ROYAL_ROOM_ID: + self._is_triggered = True + return self._is_triggered + else: + if self.after.channel.id == ROYAL_ROOM_ID: + self._is_triggered = True + return self._is_triggered + + async def execute(self): + embed: discord.Embed = EmbedFactory().make(self.member, self.is_join) + await RoyalEmbed.MESSAGE_CHANNEL.send(embed=embed) diff --git a/src/service/voice/voice_abs.py b/src/service/voice/voice_abs.py new file mode 100644 index 0000000..93ac78f --- /dev/null +++ b/src/service/voice/voice_abs.py @@ -0,0 +1,17 @@ +import discord + +import abc + + +class VoiceFunctionAbstract(metaclass=abc.ABCMeta): + @abc.abstractmethod + def __init__(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState): + pass + + @abc.abstractmethod + def is_triggered(self) -> bool: + pass + + @abc.abstractmethod + async def execute(self, **kwargs): + pass diff --git a/src/utils/discd.py b/src/utils/discd.py new file mode 100644 index 0000000..63fd588 --- /dev/null +++ b/src/utils/discd.py @@ -0,0 +1,11 @@ +from typing import Union + +import discord + + +async def unmute(member: discord.Member): + await member.edit(mute=False) + + +def get_user_icon_url(user: Union[discord.Member, discord.ClientUser]) -> str: + return "https://cdn.discordapp.com/avatars/{}/{}.png".format(user.id, user.avatar) diff --git a/src/utils/singleton.py b/src/utils/singleton.py new file mode 100644 index 0000000..9d93fc6 --- /dev/null +++ b/src/utils/singleton.py @@ -0,0 +1,5 @@ +class Singleton(object): + def __new__(cls, *_, **__): + if not hasattr(cls, "_instance"): + cls._instance = super(Singleton, cls).__new__(cls) + return cls._instance diff --git a/src/utils/utils.py b/src/utils/utils.py new file mode 100644 index 0000000..2bf2e6f --- /dev/null +++ b/src/utils/utils.py @@ -0,0 +1,11 @@ +def is_empty(target: list): + if len(target) == 0: + return True + return False + + +def includes(query: str, search_from: list): + for test_case in search_from: + if str(test_case) in str(query): + return True + return False