integrations.slack.api
integrations/slack/api.py
1""" 2integrations/slack/api.py 3""" 4 5import logging 6import textwrap 7from pathlib import PosixPath 8from typing import TYPE_CHECKING, Any, cast 9 10import pandas as pd 11 12from integrations.base.interface import APIInterface 13from libs.types import StyleOptions 14from libs.utils import converter, formatter 15 16if TYPE_CHECKING: 17 from slack_sdk.web import SlackResponse 18 from slack_sdk.web.client import WebClient 19 20 from integrations.protocols import MessageParserProtocol 21 22 23class AdapterAPI(APIInterface): 24 """インターフェースAPI操作クラス""" 25 26 # slack object 27 appclient: "WebClient" 28 """WebClient(botトークン使用)""" 29 webclient: "WebClient" 30 """WebClient(userトークン使用)""" 31 32 def __init__(self) -> None: 33 super().__init__() 34 35 try: 36 from slack_sdk.errors import SlackApiError 37 38 self.slack_api_error = SlackApiError 39 except ModuleNotFoundError as err: 40 raise ModuleNotFoundError(err.msg) from None 41 42 def post(self, m: "MessageParserProtocol") -> None: 43 """ 44 メッセージをポストする 45 46 Args: 47 m (MessageParserProtocol): メッセージデータ 48 49 """ 50 51 def _table_data(data: dict[str, Any]) -> list[str]: 52 ret_list: list[str] = [] 53 text_data = iter(data.values()) 54 # 先頭ブロックの処理(ヘッダ追加) 55 v = next(text_data) 56 57 ret_list.append(f"{header}\n```{v}\n```\n" if options.codeblock else f"{header}\n{v}\n") 58 # 残りのブロック 59 for v in text_data: 60 ret_list.append(f"```\n{v}\n```\n" if options.codeblock else f"{v}\n") 61 62 return ret_list 63 64 def _post_header() -> None: 65 res = self._call_chat_post_message( 66 channel=m.data.channel_id, 67 text=f"{header_title}{header_text.rstrip()}", 68 thread_ts=m.reply_ts, 69 ) 70 if res and res.status_code == 200: # 見出しがある場合はスレッドにする 71 m.post.ts = res.get("ts", "undetermined") 72 73 if not m.in_thread: 74 m.post.thread = False 75 76 # 見出しポスト 77 header_title = "" 78 header_text = "" 79 if m.post.headline: 80 header_data, header_option = m.post.headline 81 header_title = header_option.title 82 if isinstance(header_data, str): 83 header_text = header_data 84 if not m.post.message: # メッセージなし 85 _post_header() 86 elif not all(options.header_hidden for _, options in m.post.message): 87 _post_header() 88 89 # 本文 90 options = StyleOptions() 91 post_msg: list[str] = [] 92 block_layout = False 93 for data, options in m.post.message: 94 header = "" 95 96 if isinstance(data, PosixPath) and data.exists(): 97 comment = textwrap.dedent(f"{options.print_title}\n{header_text.rstrip()}") if options.use_comment else "" 98 self._call_files_upload( 99 channel=m.data.channel_id, 100 title=options.title, 101 file=str(data), 102 initial_comment=comment, 103 thread_ts=m.reply_ts, 104 request_file_info=False, 105 ) 106 107 if isinstance(data, str): 108 if options.key_title and (options.title != header_title): 109 header = options.print_title 110 text_body = textwrap.indent(data.rstrip(), "\t" * options.indent) 111 post_msg.append(f"{header}\n```\n{text_body}\n```\n" if options.codeblock else f"{header}\n{text_body}\n") 112 113 if isinstance(data, pd.DataFrame): 114 if options.key_title and (options.title != header_title): 115 header = options.print_title 116 match options.data_kind: 117 case StyleOptions.DataKind.POINTS_TOTAL | StyleOptions.DataKind.POINTS_DIFF: 118 post_msg.extend(_table_data(converter.df_to_text_table(data, options, step=40))) 119 case StyleOptions.DataKind.REMARKS_YAKUMAN | StyleOptions.DataKind.REMARKS_REGULATION | StyleOptions.DataKind.REMARKS_OTHER: 120 options.indent = 1 121 post_msg.extend(_table_data(converter.df_to_remarks(data, options))) 122 case StyleOptions.DataKind.DETAILED_COMPARISON: 123 post_msg.extend(_table_data(converter.df_to_text_table2(data, options, limit=3800))) 124 case StyleOptions.DataKind.SEAT_DATA: 125 options.indent = 1 126 post_msg.extend(_table_data(converter.df_to_seat_data(data, options))) 127 case StyleOptions.DataKind.RECORD_DATA: 128 block_layout = True 129 post_msg.extend(_table_data(converter.df_to_results_simple(data, options, limit=1900))) 130 case StyleOptions.DataKind.RECORD_DATA_ALL: 131 post_msg.extend(_table_data(converter.df_to_results_details(data, options, limit=2600))) 132 case StyleOptions.DataKind.RANKING: 133 post_msg.extend(_table_data(converter.df_to_ranking(data, options.title, step=50))) 134 case StyleOptions.DataKind.RATING: 135 post_msg.extend(_table_data(converter.df_to_text_table(data, options, step=20))) 136 case _: 137 pass 138 139 if options.summarize: 140 post_msg = formatter.group_strings(post_msg) 141 142 for msg in post_msg: 143 if msg != msg.lstrip() or (not msg.find("*【戦績】*") and block_layout): 144 self._call_chat_post_message( 145 channel=m.data.channel_id, 146 text=msg.rstrip(), 147 blocks=[{"type": "section", "text": {"type": "mrkdwn", "text": msg.rstrip()}}], 148 thread_ts=m.reply_ts, 149 ) 150 else: 151 self._call_chat_post_message( 152 channel=m.data.channel_id, 153 text=msg.rstrip(), 154 thread_ts=m.reply_ts, 155 ) 156 157 def _call_chat_post_message(self, **kwargs: Any) -> "SlackResponse": 158 """ 159 slackにメッセージをポストする 160 161 Returns: 162 SlackResponse: API response 163 164 """ 165 res = cast("SlackResponse", {}) 166 if kwargs["thread_ts"] == "0": 167 kwargs.pop("thread_ts") 168 169 if not kwargs.get("text"): 170 return res 171 172 try: 173 res = self.appclient.chat_postMessage(**kwargs) 174 except self.slack_api_error as err: 175 logging.error("slack_api_error: %s", err) 176 logging.error("kwargs=%s", kwargs) 177 178 return res 179 180 def _call_files_upload(self, **kwargs: Any) -> "SlackResponse": 181 """ 182 slackにファイルをアップロードする 183 184 Returns: 185 SlackResponse | Any: API response 186 187 """ 188 res = cast("SlackResponse", {}) 189 if kwargs.get("thread_ts", "0") == "0": 190 kwargs.pop("thread_ts") 191 192 try: 193 res = self.appclient.files_upload_v2(**kwargs) 194 except self.slack_api_error as err: 195 logging.error("slack_api_error: %s", err) 196 logging.error("kwargs=%s", kwargs) 197 198 return res
24class AdapterAPI(APIInterface): 25 """インターフェースAPI操作クラス""" 26 27 # slack object 28 appclient: "WebClient" 29 """WebClient(botトークン使用)""" 30 webclient: "WebClient" 31 """WebClient(userトークン使用)""" 32 33 def __init__(self) -> None: 34 super().__init__() 35 36 try: 37 from slack_sdk.errors import SlackApiError 38 39 self.slack_api_error = SlackApiError 40 except ModuleNotFoundError as err: 41 raise ModuleNotFoundError(err.msg) from None 42 43 def post(self, m: "MessageParserProtocol") -> None: 44 """ 45 メッセージをポストする 46 47 Args: 48 m (MessageParserProtocol): メッセージデータ 49 50 """ 51 52 def _table_data(data: dict[str, Any]) -> list[str]: 53 ret_list: list[str] = [] 54 text_data = iter(data.values()) 55 # 先頭ブロックの処理(ヘッダ追加) 56 v = next(text_data) 57 58 ret_list.append(f"{header}\n```{v}\n```\n" if options.codeblock else f"{header}\n{v}\n") 59 # 残りのブロック 60 for v in text_data: 61 ret_list.append(f"```\n{v}\n```\n" if options.codeblock else f"{v}\n") 62 63 return ret_list 64 65 def _post_header() -> None: 66 res = self._call_chat_post_message( 67 channel=m.data.channel_id, 68 text=f"{header_title}{header_text.rstrip()}", 69 thread_ts=m.reply_ts, 70 ) 71 if res and res.status_code == 200: # 見出しがある場合はスレッドにする 72 m.post.ts = res.get("ts", "undetermined") 73 74 if not m.in_thread: 75 m.post.thread = False 76 77 # 見出しポスト 78 header_title = "" 79 header_text = "" 80 if m.post.headline: 81 header_data, header_option = m.post.headline 82 header_title = header_option.title 83 if isinstance(header_data, str): 84 header_text = header_data 85 if not m.post.message: # メッセージなし 86 _post_header() 87 elif not all(options.header_hidden for _, options in m.post.message): 88 _post_header() 89 90 # 本文 91 options = StyleOptions() 92 post_msg: list[str] = [] 93 block_layout = False 94 for data, options in m.post.message: 95 header = "" 96 97 if isinstance(data, PosixPath) and data.exists(): 98 comment = textwrap.dedent(f"{options.print_title}\n{header_text.rstrip()}") if options.use_comment else "" 99 self._call_files_upload( 100 channel=m.data.channel_id, 101 title=options.title, 102 file=str(data), 103 initial_comment=comment, 104 thread_ts=m.reply_ts, 105 request_file_info=False, 106 ) 107 108 if isinstance(data, str): 109 if options.key_title and (options.title != header_title): 110 header = options.print_title 111 text_body = textwrap.indent(data.rstrip(), "\t" * options.indent) 112 post_msg.append(f"{header}\n```\n{text_body}\n```\n" if options.codeblock else f"{header}\n{text_body}\n") 113 114 if isinstance(data, pd.DataFrame): 115 if options.key_title and (options.title != header_title): 116 header = options.print_title 117 match options.data_kind: 118 case StyleOptions.DataKind.POINTS_TOTAL | StyleOptions.DataKind.POINTS_DIFF: 119 post_msg.extend(_table_data(converter.df_to_text_table(data, options, step=40))) 120 case StyleOptions.DataKind.REMARKS_YAKUMAN | StyleOptions.DataKind.REMARKS_REGULATION | StyleOptions.DataKind.REMARKS_OTHER: 121 options.indent = 1 122 post_msg.extend(_table_data(converter.df_to_remarks(data, options))) 123 case StyleOptions.DataKind.DETAILED_COMPARISON: 124 post_msg.extend(_table_data(converter.df_to_text_table2(data, options, limit=3800))) 125 case StyleOptions.DataKind.SEAT_DATA: 126 options.indent = 1 127 post_msg.extend(_table_data(converter.df_to_seat_data(data, options))) 128 case StyleOptions.DataKind.RECORD_DATA: 129 block_layout = True 130 post_msg.extend(_table_data(converter.df_to_results_simple(data, options, limit=1900))) 131 case StyleOptions.DataKind.RECORD_DATA_ALL: 132 post_msg.extend(_table_data(converter.df_to_results_details(data, options, limit=2600))) 133 case StyleOptions.DataKind.RANKING: 134 post_msg.extend(_table_data(converter.df_to_ranking(data, options.title, step=50))) 135 case StyleOptions.DataKind.RATING: 136 post_msg.extend(_table_data(converter.df_to_text_table(data, options, step=20))) 137 case _: 138 pass 139 140 if options.summarize: 141 post_msg = formatter.group_strings(post_msg) 142 143 for msg in post_msg: 144 if msg != msg.lstrip() or (not msg.find("*【戦績】*") and block_layout): 145 self._call_chat_post_message( 146 channel=m.data.channel_id, 147 text=msg.rstrip(), 148 blocks=[{"type": "section", "text": {"type": "mrkdwn", "text": msg.rstrip()}}], 149 thread_ts=m.reply_ts, 150 ) 151 else: 152 self._call_chat_post_message( 153 channel=m.data.channel_id, 154 text=msg.rstrip(), 155 thread_ts=m.reply_ts, 156 ) 157 158 def _call_chat_post_message(self, **kwargs: Any) -> "SlackResponse": 159 """ 160 slackにメッセージをポストする 161 162 Returns: 163 SlackResponse: API response 164 165 """ 166 res = cast("SlackResponse", {}) 167 if kwargs["thread_ts"] == "0": 168 kwargs.pop("thread_ts") 169 170 if not kwargs.get("text"): 171 return res 172 173 try: 174 res = self.appclient.chat_postMessage(**kwargs) 175 except self.slack_api_error as err: 176 logging.error("slack_api_error: %s", err) 177 logging.error("kwargs=%s", kwargs) 178 179 return res 180 181 def _call_files_upload(self, **kwargs: Any) -> "SlackResponse": 182 """ 183 slackにファイルをアップロードする 184 185 Returns: 186 SlackResponse | Any: API response 187 188 """ 189 res = cast("SlackResponse", {}) 190 if kwargs.get("thread_ts", "0") == "0": 191 kwargs.pop("thread_ts") 192 193 try: 194 res = self.appclient.files_upload_v2(**kwargs) 195 except self.slack_api_error as err: 196 logging.error("slack_api_error: %s", err) 197 logging.error("kwargs=%s", kwargs) 198 199 return res
インターフェースAPI操作クラス
43 def post(self, m: "MessageParserProtocol") -> None: 44 """ 45 メッセージをポストする 46 47 Args: 48 m (MessageParserProtocol): メッセージデータ 49 50 """ 51 52 def _table_data(data: dict[str, Any]) -> list[str]: 53 ret_list: list[str] = [] 54 text_data = iter(data.values()) 55 # 先頭ブロックの処理(ヘッダ追加) 56 v = next(text_data) 57 58 ret_list.append(f"{header}\n```{v}\n```\n" if options.codeblock else f"{header}\n{v}\n") 59 # 残りのブロック 60 for v in text_data: 61 ret_list.append(f"```\n{v}\n```\n" if options.codeblock else f"{v}\n") 62 63 return ret_list 64 65 def _post_header() -> None: 66 res = self._call_chat_post_message( 67 channel=m.data.channel_id, 68 text=f"{header_title}{header_text.rstrip()}", 69 thread_ts=m.reply_ts, 70 ) 71 if res and res.status_code == 200: # 見出しがある場合はスレッドにする 72 m.post.ts = res.get("ts", "undetermined") 73 74 if not m.in_thread: 75 m.post.thread = False 76 77 # 見出しポスト 78 header_title = "" 79 header_text = "" 80 if m.post.headline: 81 header_data, header_option = m.post.headline 82 header_title = header_option.title 83 if isinstance(header_data, str): 84 header_text = header_data 85 if not m.post.message: # メッセージなし 86 _post_header() 87 elif not all(options.header_hidden for _, options in m.post.message): 88 _post_header() 89 90 # 本文 91 options = StyleOptions() 92 post_msg: list[str] = [] 93 block_layout = False 94 for data, options in m.post.message: 95 header = "" 96 97 if isinstance(data, PosixPath) and data.exists(): 98 comment = textwrap.dedent(f"{options.print_title}\n{header_text.rstrip()}") if options.use_comment else "" 99 self._call_files_upload( 100 channel=m.data.channel_id, 101 title=options.title, 102 file=str(data), 103 initial_comment=comment, 104 thread_ts=m.reply_ts, 105 request_file_info=False, 106 ) 107 108 if isinstance(data, str): 109 if options.key_title and (options.title != header_title): 110 header = options.print_title 111 text_body = textwrap.indent(data.rstrip(), "\t" * options.indent) 112 post_msg.append(f"{header}\n```\n{text_body}\n```\n" if options.codeblock else f"{header}\n{text_body}\n") 113 114 if isinstance(data, pd.DataFrame): 115 if options.key_title and (options.title != header_title): 116 header = options.print_title 117 match options.data_kind: 118 case StyleOptions.DataKind.POINTS_TOTAL | StyleOptions.DataKind.POINTS_DIFF: 119 post_msg.extend(_table_data(converter.df_to_text_table(data, options, step=40))) 120 case StyleOptions.DataKind.REMARKS_YAKUMAN | StyleOptions.DataKind.REMARKS_REGULATION | StyleOptions.DataKind.REMARKS_OTHER: 121 options.indent = 1 122 post_msg.extend(_table_data(converter.df_to_remarks(data, options))) 123 case StyleOptions.DataKind.DETAILED_COMPARISON: 124 post_msg.extend(_table_data(converter.df_to_text_table2(data, options, limit=3800))) 125 case StyleOptions.DataKind.SEAT_DATA: 126 options.indent = 1 127 post_msg.extend(_table_data(converter.df_to_seat_data(data, options))) 128 case StyleOptions.DataKind.RECORD_DATA: 129 block_layout = True 130 post_msg.extend(_table_data(converter.df_to_results_simple(data, options, limit=1900))) 131 case StyleOptions.DataKind.RECORD_DATA_ALL: 132 post_msg.extend(_table_data(converter.df_to_results_details(data, options, limit=2600))) 133 case StyleOptions.DataKind.RANKING: 134 post_msg.extend(_table_data(converter.df_to_ranking(data, options.title, step=50))) 135 case StyleOptions.DataKind.RATING: 136 post_msg.extend(_table_data(converter.df_to_text_table(data, options, step=20))) 137 case _: 138 pass 139 140 if options.summarize: 141 post_msg = formatter.group_strings(post_msg) 142 143 for msg in post_msg: 144 if msg != msg.lstrip() or (not msg.find("*【戦績】*") and block_layout): 145 self._call_chat_post_message( 146 channel=m.data.channel_id, 147 text=msg.rstrip(), 148 blocks=[{"type": "section", "text": {"type": "mrkdwn", "text": msg.rstrip()}}], 149 thread_ts=m.reply_ts, 150 ) 151 else: 152 self._call_chat_post_message( 153 channel=m.data.channel_id, 154 text=msg.rstrip(), 155 thread_ts=m.reply_ts, 156 )
メッセージをポストする
Arguments:
- m (MessageParserProtocol): メッセージデータ