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