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```")
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操作クラス
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): メッセージデータ
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): メッセージデータ
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): メッセージデータ