integrations.discord.api

integrations/discord/api.py

  1"""
  2integrations/discord/api.py
  3"""
  4
  5import asyncio
  6import sys
  7import textwrap
  8from pathlib import PosixPath
  9from typing import TYPE_CHECKING, Optional, Union, cast
 10
 11import pandas as pd
 12from table2ascii import PresetStyle, table2ascii
 13
 14import integrations.discord.events.audioop as _audioop
 15from cls.timekit import ExtendedDatetime as ExtDt
 16from integrations.base.interface import APIInterface
 17from libs.types import StyleOptions
 18from libs.utils import converter, formatter, textutil
 19
 20sys.modules["audioop"] = _audioop
 21
 22if TYPE_CHECKING:
 23    from discord import ApplicationContext, Bot, Message
 24
 25    from integrations.protocols import MessageParserProtocol
 26
 27
 28class AdapterAPI(APIInterface):
 29    """インターフェースAPI操作クラス"""
 30
 31    bot: "Bot"
 32
 33    def __init__(self):
 34        super().__init__()
 35
 36        from discord import File as discord_file
 37        self.discord_file = discord_file
 38
 39        # discord object
 40        self.response: Union["Message", "ApplicationContext"]
 41
 42    def post(self, m: "MessageParserProtocol"):
 43        """メッセージをポストする(非同期処理ラッパー)
 44
 45        Args:
 46            m (MessageParserProtocol): メッセージデータ
 47        """
 48
 49        if m.status.command_flg:
 50            asyncio.create_task(self.command_respond(m))
 51        else:
 52            asyncio.create_task(self.post_async(m))
 53
 54    async def post_async(self, m: "MessageParserProtocol"):
 55        """メッセージをポストする
 56
 57        Args:
 58            m (MessageParserProtocol): メッセージデータ
 59        """
 60
 61        self.response = cast("Message", self.response)
 62
 63        def _header_text(title: str) -> str:
 64            if not title.isnumeric() and title:  # 数値のキーはヘッダにしない
 65                return f"**【{title}】**\n"
 66            return ""
 67
 68        def _table_data(data: dict) -> list:
 69            ret_list: list = []
 70            text_data = iter(data.values())
 71            # 先頭ブロックの処理(ヘッダ追加)
 72            v = next(text_data)
 73            ret_list.append(f"{header}```\n{v}\n```\n" if style.codeblock else f"{header}{v}\n")
 74            # 残りのブロック
 75            for v in text_data:
 76                ret_list.append(f"```\n{v}\n```\n" if style.codeblock else f"```\n{v}\n```\n")
 77            return ret_list
 78
 79        if not m.in_thread:
 80            m.post.thread = False
 81
 82        # 見出しポスト
 83        header_title = ""
 84        header_text = ""
 85        header_msg: Optional["Message"] = None
 86
 87        if m.post.headline:
 88            header_title, header_text = next(iter(m.post.headline.items()))
 89            if not all(v["options"].header_hidden for x in m.post.message for _, v in x.items()):
 90                header_msg = await self.response.reply(f"{_header_text(header_title)}{header_text.rstrip()}")
 91                m.post.thread = True
 92
 93        # 本文
 94        post_msg: list[str] = []
 95        style = StyleOptions()
 96        for data in m.post.message:
 97            for title, val in data.items():
 98                msg = val.get("data")
 99                style = val.get("options", StyleOptions())
100                header = ""
101
102                if isinstance(msg, PosixPath) and msg.exists():
103                    comment = textwrap.dedent(
104                        f"{_header_text(header_title)}{header_text.rstrip()}"
105                    ) if style.use_comment else ""
106                    file = self.discord_file(
107                        str(msg),
108                        description=comment,
109                    )
110                    asyncio.create_task(self.response.channel.send(file=file))
111
112                if isinstance(msg, str):
113                    if style.key_title and (title != header_title):
114                        header = _header_text(title)
115
116                    message_text = msg.rstrip().replace("<@>", f"<@{self.response.author.id}>")
117                    post_msg.append(
118                        f"{header}```\n{message_text}\n```\n" if style.codeblock else f"{header}{message_text}\n"
119                    )
120
121                if isinstance(msg, pd.DataFrame):
122                    if style.key_title and (title != header_title):
123                        header = _header_text(title)
124                    match m.status.command_type:
125                        case "results":
126                            match title:
127                                case "通算ポイント" | "ポイント差分":
128                                    post_msg.extend(_table_data(converter.df_to_text_table(msg, step=40)))
129                                case "役満和了" | "卓外ポイント" | "その他":
130                                    if "回数" in msg.columns:
131                                        post_msg.extend(_table_data(converter.df_to_count(msg, title, 1)))
132                                    else:
133                                        post_msg.extend(_table_data(converter.df_to_remarks(msg)))
134                                case "座席データ":
135                                    post_msg.extend(_table_data(converter.df_to_seat_data(msg, 1)))
136                                case "戦績":
137                                    if "東家 名前" in msg.columns:  # 縦持ちデータ
138                                        post_msg.extend(_table_data(converter.df_to_results_details(msg)))
139                                    else:
140                                        post_msg.extend(_table_data(converter.df_to_results_simple(msg)))
141                                case _:
142                                    post_msg.extend(_table_data(converter.df_to_remarks(msg)))
143                        case "rating":
144                            post_msg.extend(_table_data(converter.df_to_text_table(msg, step=20)))
145                        case "ranking":
146                            post_msg.extend(_table_data(converter.df_to_ranking(msg, title, step=0)))
147
148        if style.summarize:
149            if m.status.command_type == "ranking":
150                post_msg = textutil.split_text_blocks("".join(post_msg), 1900)
151            else:
152                post_msg = formatter.group_strings(post_msg, limit=1800)
153
154        if header_msg and m.post.thread:
155            date_suffix = ExtDt(float(m.data.event_ts)).format("ymdhm", delimiter="slash")
156            thread = await header_msg.create_thread(name=f"{header_title} - {date_suffix}")
157            for msg in post_msg:
158                await thread.send(msg)
159        else:
160            for msg in post_msg:
161                await self.response.reply(msg)
162
163    async def command_respond(self, m: "MessageParserProtocol"):
164        """スラッシュコマンド応答
165
166        Args:
167            m (MessageParserProtocol): メッセージデータ
168        """
169
170        self.response = cast("ApplicationContext", self.response)
171
172        style = StyleOptions()
173        for data in m.post.message:
174            for _, val in data.items():
175                msg = val.get("data")
176                style = val.get("options", StyleOptions())
177
178            if isinstance(msg, PosixPath) and msg.exists():
179                file = self.discord_file(str(msg))
180                await self.response.send(file=file)
181
182            if isinstance(msg, str):
183                if style.codeblock:
184                    msg = f"```\n{msg}\n```"
185                await self.response.respond(msg)
186
187            if isinstance(msg, pd.DataFrame):
188                output = table2ascii(
189                    header=msg.columns.to_list(),
190                    body=msg.to_dict(orient="split")["data"],
191                    style=PresetStyle.ascii_borderless,
192                )
193                await self.response.respond(f"```\n{output}\n```")
class AdapterAPI(integrations.base.interface.APIInterface):
 29class AdapterAPI(APIInterface):
 30    """インターフェースAPI操作クラス"""
 31
 32    bot: "Bot"
 33
 34    def __init__(self):
 35        super().__init__()
 36
 37        from discord import File as discord_file
 38        self.discord_file = discord_file
 39
 40        # discord object
 41        self.response: Union["Message", "ApplicationContext"]
 42
 43    def post(self, m: "MessageParserProtocol"):
 44        """メッセージをポストする(非同期処理ラッパー)
 45
 46        Args:
 47            m (MessageParserProtocol): メッセージデータ
 48        """
 49
 50        if m.status.command_flg:
 51            asyncio.create_task(self.command_respond(m))
 52        else:
 53            asyncio.create_task(self.post_async(m))
 54
 55    async def post_async(self, m: "MessageParserProtocol"):
 56        """メッセージをポストする
 57
 58        Args:
 59            m (MessageParserProtocol): メッセージデータ
 60        """
 61
 62        self.response = cast("Message", self.response)
 63
 64        def _header_text(title: str) -> str:
 65            if not title.isnumeric() and title:  # 数値のキーはヘッダにしない
 66                return f"**【{title}】**\n"
 67            return ""
 68
 69        def _table_data(data: dict) -> list:
 70            ret_list: list = []
 71            text_data = iter(data.values())
 72            # 先頭ブロックの処理(ヘッダ追加)
 73            v = next(text_data)
 74            ret_list.append(f"{header}```\n{v}\n```\n" if style.codeblock else f"{header}{v}\n")
 75            # 残りのブロック
 76            for v in text_data:
 77                ret_list.append(f"```\n{v}\n```\n" if style.codeblock else f"```\n{v}\n```\n")
 78            return ret_list
 79
 80        if not m.in_thread:
 81            m.post.thread = False
 82
 83        # 見出しポスト
 84        header_title = ""
 85        header_text = ""
 86        header_msg: Optional["Message"] = None
 87
 88        if m.post.headline:
 89            header_title, header_text = next(iter(m.post.headline.items()))
 90            if not all(v["options"].header_hidden for x in m.post.message for _, v in x.items()):
 91                header_msg = await self.response.reply(f"{_header_text(header_title)}{header_text.rstrip()}")
 92                m.post.thread = True
 93
 94        # 本文
 95        post_msg: list[str] = []
 96        style = StyleOptions()
 97        for data in m.post.message:
 98            for title, val in data.items():
 99                msg = val.get("data")
100                style = val.get("options", StyleOptions())
101                header = ""
102
103                if isinstance(msg, PosixPath) and msg.exists():
104                    comment = textwrap.dedent(
105                        f"{_header_text(header_title)}{header_text.rstrip()}"
106                    ) if style.use_comment else ""
107                    file = self.discord_file(
108                        str(msg),
109                        description=comment,
110                    )
111                    asyncio.create_task(self.response.channel.send(file=file))
112
113                if isinstance(msg, str):
114                    if style.key_title and (title != header_title):
115                        header = _header_text(title)
116
117                    message_text = msg.rstrip().replace("<@>", f"<@{self.response.author.id}>")
118                    post_msg.append(
119                        f"{header}```\n{message_text}\n```\n" if style.codeblock else f"{header}{message_text}\n"
120                    )
121
122                if isinstance(msg, pd.DataFrame):
123                    if style.key_title and (title != header_title):
124                        header = _header_text(title)
125                    match m.status.command_type:
126                        case "results":
127                            match title:
128                                case "通算ポイント" | "ポイント差分":
129                                    post_msg.extend(_table_data(converter.df_to_text_table(msg, step=40)))
130                                case "役満和了" | "卓外ポイント" | "その他":
131                                    if "回数" in msg.columns:
132                                        post_msg.extend(_table_data(converter.df_to_count(msg, title, 1)))
133                                    else:
134                                        post_msg.extend(_table_data(converter.df_to_remarks(msg)))
135                                case "座席データ":
136                                    post_msg.extend(_table_data(converter.df_to_seat_data(msg, 1)))
137                                case "戦績":
138                                    if "東家 名前" in msg.columns:  # 縦持ちデータ
139                                        post_msg.extend(_table_data(converter.df_to_results_details(msg)))
140                                    else:
141                                        post_msg.extend(_table_data(converter.df_to_results_simple(msg)))
142                                case _:
143                                    post_msg.extend(_table_data(converter.df_to_remarks(msg)))
144                        case "rating":
145                            post_msg.extend(_table_data(converter.df_to_text_table(msg, step=20)))
146                        case "ranking":
147                            post_msg.extend(_table_data(converter.df_to_ranking(msg, title, step=0)))
148
149        if style.summarize:
150            if m.status.command_type == "ranking":
151                post_msg = textutil.split_text_blocks("".join(post_msg), 1900)
152            else:
153                post_msg = formatter.group_strings(post_msg, limit=1800)
154
155        if header_msg and m.post.thread:
156            date_suffix = ExtDt(float(m.data.event_ts)).format("ymdhm", delimiter="slash")
157            thread = await header_msg.create_thread(name=f"{header_title} - {date_suffix}")
158            for msg in post_msg:
159                await thread.send(msg)
160        else:
161            for msg in post_msg:
162                await self.response.reply(msg)
163
164    async def command_respond(self, m: "MessageParserProtocol"):
165        """スラッシュコマンド応答
166
167        Args:
168            m (MessageParserProtocol): メッセージデータ
169        """
170
171        self.response = cast("ApplicationContext", self.response)
172
173        style = StyleOptions()
174        for data in m.post.message:
175            for _, val in data.items():
176                msg = val.get("data")
177                style = val.get("options", StyleOptions())
178
179            if isinstance(msg, PosixPath) and msg.exists():
180                file = self.discord_file(str(msg))
181                await self.response.send(file=file)
182
183            if isinstance(msg, str):
184                if style.codeblock:
185                    msg = f"```\n{msg}\n```"
186                await self.response.respond(msg)
187
188            if isinstance(msg, pd.DataFrame):
189                output = table2ascii(
190                    header=msg.columns.to_list(),
191                    body=msg.to_dict(orient="split")["data"],
192                    style=PresetStyle.ascii_borderless,
193                )
194                await self.response.respond(f"```\n{output}\n```")

インターフェースAPI操作クラス

bot: discord.bot.Bot
discord_file
response: Union[discord.message.Message, discord.commands.context.ApplicationContext]
def post(self, m: integrations.protocols.MessageParserProtocol):
43    def post(self, m: "MessageParserProtocol"):
44        """メッセージをポストする(非同期処理ラッパー)
45
46        Args:
47            m (MessageParserProtocol): メッセージデータ
48        """
49
50        if m.status.command_flg:
51            asyncio.create_task(self.command_respond(m))
52        else:
53            asyncio.create_task(self.post_async(m))

メッセージをポストする(非同期処理ラッパー)

Arguments:
  • m (MessageParserProtocol): メッセージデータ
async def post_async(self, m: integrations.protocols.MessageParserProtocol):
 55    async def post_async(self, m: "MessageParserProtocol"):
 56        """メッセージをポストする
 57
 58        Args:
 59            m (MessageParserProtocol): メッセージデータ
 60        """
 61
 62        self.response = cast("Message", self.response)
 63
 64        def _header_text(title: str) -> str:
 65            if not title.isnumeric() and title:  # 数値のキーはヘッダにしない
 66                return f"**【{title}】**\n"
 67            return ""
 68
 69        def _table_data(data: dict) -> list:
 70            ret_list: list = []
 71            text_data = iter(data.values())
 72            # 先頭ブロックの処理(ヘッダ追加)
 73            v = next(text_data)
 74            ret_list.append(f"{header}```\n{v}\n```\n" if style.codeblock else f"{header}{v}\n")
 75            # 残りのブロック
 76            for v in text_data:
 77                ret_list.append(f"```\n{v}\n```\n" if style.codeblock else f"```\n{v}\n```\n")
 78            return ret_list
 79
 80        if not m.in_thread:
 81            m.post.thread = False
 82
 83        # 見出しポスト
 84        header_title = ""
 85        header_text = ""
 86        header_msg: Optional["Message"] = None
 87
 88        if m.post.headline:
 89            header_title, header_text = next(iter(m.post.headline.items()))
 90            if not all(v["options"].header_hidden for x in m.post.message for _, v in x.items()):
 91                header_msg = await self.response.reply(f"{_header_text(header_title)}{header_text.rstrip()}")
 92                m.post.thread = True
 93
 94        # 本文
 95        post_msg: list[str] = []
 96        style = StyleOptions()
 97        for data in m.post.message:
 98            for title, val in data.items():
 99                msg = val.get("data")
100                style = val.get("options", StyleOptions())
101                header = ""
102
103                if isinstance(msg, PosixPath) and msg.exists():
104                    comment = textwrap.dedent(
105                        f"{_header_text(header_title)}{header_text.rstrip()}"
106                    ) if style.use_comment else ""
107                    file = self.discord_file(
108                        str(msg),
109                        description=comment,
110                    )
111                    asyncio.create_task(self.response.channel.send(file=file))
112
113                if isinstance(msg, str):
114                    if style.key_title and (title != header_title):
115                        header = _header_text(title)
116
117                    message_text = msg.rstrip().replace("<@>", f"<@{self.response.author.id}>")
118                    post_msg.append(
119                        f"{header}```\n{message_text}\n```\n" if style.codeblock else f"{header}{message_text}\n"
120                    )
121
122                if isinstance(msg, pd.DataFrame):
123                    if style.key_title and (title != header_title):
124                        header = _header_text(title)
125                    match m.status.command_type:
126                        case "results":
127                            match title:
128                                case "通算ポイント" | "ポイント差分":
129                                    post_msg.extend(_table_data(converter.df_to_text_table(msg, step=40)))
130                                case "役満和了" | "卓外ポイント" | "その他":
131                                    if "回数" in msg.columns:
132                                        post_msg.extend(_table_data(converter.df_to_count(msg, title, 1)))
133                                    else:
134                                        post_msg.extend(_table_data(converter.df_to_remarks(msg)))
135                                case "座席データ":
136                                    post_msg.extend(_table_data(converter.df_to_seat_data(msg, 1)))
137                                case "戦績":
138                                    if "東家 名前" in msg.columns:  # 縦持ちデータ
139                                        post_msg.extend(_table_data(converter.df_to_results_details(msg)))
140                                    else:
141                                        post_msg.extend(_table_data(converter.df_to_results_simple(msg)))
142                                case _:
143                                    post_msg.extend(_table_data(converter.df_to_remarks(msg)))
144                        case "rating":
145                            post_msg.extend(_table_data(converter.df_to_text_table(msg, step=20)))
146                        case "ranking":
147                            post_msg.extend(_table_data(converter.df_to_ranking(msg, title, step=0)))
148
149        if style.summarize:
150            if m.status.command_type == "ranking":
151                post_msg = textutil.split_text_blocks("".join(post_msg), 1900)
152            else:
153                post_msg = formatter.group_strings(post_msg, limit=1800)
154
155        if header_msg and m.post.thread:
156            date_suffix = ExtDt(float(m.data.event_ts)).format("ymdhm", delimiter="slash")
157            thread = await header_msg.create_thread(name=f"{header_title} - {date_suffix}")
158            for msg in post_msg:
159                await thread.send(msg)
160        else:
161            for msg in post_msg:
162                await self.response.reply(msg)

メッセージをポストする

Arguments:
  • m (MessageParserProtocol): メッセージデータ
async def command_respond(self, m: integrations.protocols.MessageParserProtocol):
164    async def command_respond(self, m: "MessageParserProtocol"):
165        """スラッシュコマンド応答
166
167        Args:
168            m (MessageParserProtocol): メッセージデータ
169        """
170
171        self.response = cast("ApplicationContext", self.response)
172
173        style = StyleOptions()
174        for data in m.post.message:
175            for _, val in data.items():
176                msg = val.get("data")
177                style = val.get("options", StyleOptions())
178
179            if isinstance(msg, PosixPath) and msg.exists():
180                file = self.discord_file(str(msg))
181                await self.response.send(file=file)
182
183            if isinstance(msg, str):
184                if style.codeblock:
185                    msg = f"```\n{msg}\n```"
186                await self.response.respond(msg)
187
188            if isinstance(msg, pd.DataFrame):
189                output = table2ascii(
190                    header=msg.columns.to_list(),
191                    body=msg.to_dict(orient="split")["data"],
192                    style=PresetStyle.ascii_borderless,
193                )
194                await self.response.respond(f"```\n{output}\n```")

スラッシュコマンド応答

Arguments:
  • m (MessageParserProtocol): メッセージデータ