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
class AdapterAPI(integrations.base.interface.APIInterface):
 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操作クラス

appclient: slack_sdk.web.client.WebClient

WebClient(botトークン使用)

webclient: slack_sdk.web.client.WebClient

WebClient(userトークン使用)

def post(self, m: integrations.protocols.MessageParserProtocol) -> None:
 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): メッセージデータ