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

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

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

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

Arguments:
  • m (MessageParserProtocol): メッセージデータ
async def post_async(self, m: integrations.protocols.MessageParserProtocol) -> None:
 58    async def post_async(self, m: "MessageParserProtocol") -> None:
 59        """
 60        メッセージをポストする
 61
 62        Args:
 63            m (MessageParserProtocol): メッセージデータ
 64
 65        """
 66        self.response = cast("Message", self.response)
 67
 68        def _table_data(data: dict[str, str]) -> list[str]:
 69            ret_list: list[str] = []
 70            text_data = iter(data.values())
 71            # 先頭ブロックの処理(ヘッダ追加)
 72            v = next(text_data)
 73
 74            ret_list.append(f"{header}\n```\n{v}\n```\n" if options.codeblock else f"{header}\n{v}\n")
 75            # 残りのブロック
 76            for v in text_data:
 77                ret_list.append(f"```\n{v}\n```\n" if options.codeblock else f"{v}\n")
 78
 79            return ret_list
 80
 81        if not m.in_thread:
 82            m.post.thread = False
 83
 84        # 見出しポスト
 85        header_title = ""
 86        header_text = ""
 87        if m.post.headline:
 88            header_data, header_option = m.post.headline
 89            header_title = f"{header_option.print_title}"
 90            if isinstance(header_data, str):
 91                header_text = header_data
 92            m.post.thread_title = header_title
 93        if not m.post.message:
 94            thread_msg = await self.response.reply(f"{header_title}\n{header_text.rstrip()}")
 95            m.post.thread = True
 96        elif not all(options.header_hidden for _, options in m.post.message):
 97            thread_msg = await self.response.reply(f"{m.post.thread_title}\n{header_text.rstrip()}")
 98            m.post.thread = True
 99        elif m.post.thread_title:
100            thread_msg = self.response
101            m.post.thread = True
102
103        # 本文
104        options = StyleOptions()
105        post_msg: list[str] = []
106        for data, options in m.post.message:
107            header = ""
108
109            if isinstance(data, PosixPath) and data.exists():
110                comment = textwrap.dedent(f"{header_title}\n{header_text.rstrip()}") if options.use_comment else ""
111                file = self.discord_file(
112                    str(data),
113                    description=comment,
114                )
115                asyncio.create_task(self.response.channel.send(file=file))
116
117            if isinstance(data, str):
118                if options.key_title and (options.title != header_title):
119                    header = f"{options.print_title}"
120                message_text = textwrap.indent(data.rstrip().replace("<@>", f"<@{self.response.author.id}>"), "\t" * options.indent)
121                post_msg.append(f"{header}\n```\n{message_text}\n```\n" if options.codeblock else f"{header}\n{message_text}\n")
122
123            if isinstance(data, pd.DataFrame):
124                if options.key_title and (options.title != header_title):
125                    header = f"** {options.print_title} **"
126                match options.data_kind:
127                    case StyleOptions.DataKind.POINTS_TOTAL | StyleOptions.DataKind.POINTS_DIFF:
128                        post_msg.extend(_table_data(converter.df_to_text_table(data, options, step=40)))
129                    case StyleOptions.DataKind.REMARKS_YAKUMAN | StyleOptions.DataKind.REMARKS_REGULATION | StyleOptions.DataKind.REMARKS_OTHER:
130                        options.indent = 1
131                        post_msg.extend(_table_data(converter.df_to_remarks(data, options)))
132                    case StyleOptions.DataKind.DETAILED_COMPARISON:
133                        post_msg.extend(_table_data(converter.df_to_text_table2(data, options, limit=2000)))
134                    case StyleOptions.DataKind.SEAT_DATA:
135                        options.indent = 1
136                        post_msg.extend(_table_data(converter.df_to_seat_data(data, options)))
137                    case StyleOptions.DataKind.RECORD_DATA:
138                        options.summarize = False
139                        post_msg.extend(_table_data(converter.df_to_results_simple(data, options, limit=1200)))
140                    case StyleOptions.DataKind.RECORD_DATA_ALL:
141                        options.summarize = False
142                        post_msg.extend(_table_data(converter.df_to_results_details(data, options, limit=1200)))
143                    case StyleOptions.DataKind.RANKING:
144                        post_msg.extend(_table_data(converter.df_to_ranking(data, options.title, step=0)))
145                    case StyleOptions.DataKind.RATING:
146                        post_msg.extend(_table_data(converter.df_to_text_table(data, options, step=20)))
147                    case _:
148                        pass
149
150        if options.summarize:
151            if m.status.command_type == CommandType.RANKING:
152                post_msg = textutil.split_text_blocks("".join(post_msg), 1900)
153            else:
154                post_msg = formatter.group_strings(post_msg, limit=1800)
155
156        if thread_msg and m.post.thread:
157            date_suffix = ExtDt(float(m.data.event_ts)).format(Format.YMDHMS, Delimiter.SLASH)
158            if not m.post.thread_title.isnumeric() and m.post.thread_title:
159                thread = await thread_msg.create_thread(name=f"{m.post.thread_title} - {date_suffix}")
160                for msg in post_msg:
161                    for split_msg in formatter.split_strings(msg, limit=1800):
162                        await thread.send(split_msg)
163            else:  # 数字タイトルはスレッドにしない
164                for msg in post_msg:
165                    for split_msg in formatter.split_strings(msg, limit=1800):
166                        await self.response.reply(split_msg)
167        else:
168            for msg in post_msg:
169                for split_msg in formatter.split_strings(msg, limit=1800):
170                    await self.response.reply(split_msg)

メッセージをポストする

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

スラッシュコマンド応答

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