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, 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):
 33        super().__init__()
 34
 35        try:
 36            from slack_sdk.errors import SlackApiError
 37            self.slack_api_error = SlackApiError
 38        except ModuleNotFoundError as err:
 39            raise ModuleNotFoundError(err.msg) from None
 40
 41    def post(self, m: "MessageParserProtocol"):
 42        """メッセージをポストする
 43
 44        Args:
 45            m (MessageParserProtocol): メッセージデータ
 46        """
 47
 48        def _header_text(title: str) -> str:
 49            if not title.isnumeric() and title:  # 数値のキーはヘッダにしない
 50                return f"*【{title}】*\n"
 51            return ""
 52
 53        def _table_data(data: dict) -> list:
 54            ret_list: list = []
 55            text_data = iter(data.values())
 56            # 先頭ブロックの処理(ヘッダ追加)
 57            v = next(text_data)
 58            ret_list.append(f"{header}```\n{v}\n```\n" if style.codeblock else f"{header}{v}\n")
 59            # 残りのブロック
 60            for v in text_data:
 61                ret_list.append(f"```\n{v}\n```\n" if style.codeblock else f"```\n{v}\n```\n")
 62            return ret_list
 63
 64        if not m.in_thread:
 65            m.post.thread = False
 66
 67        # 見出しポスト
 68        header_title = ""
 69        header_text = ""
 70        if m.post.headline:
 71            header_title, header_text = next(iter(m.post.headline.items()))
 72            if not all(v["options"].header_hidden for x in m.post.message for _, v in x.items()):
 73                res = self._call_chat_post_message(
 74                    channel=m.data.channel_id,
 75                    text=f"{_header_text(header_title)}{header_text.rstrip()}",
 76                    thread_ts=m.reply_ts,
 77                )
 78                if res.status_code == 200:  # 見出しがある場合はスレッドにする
 79                    m.post.ts = res.get("ts", "undetermined")
 80                else:
 81                    m.post.ts = "undetermined"
 82
 83        # 本文
 84        post_msg: list[str] = []
 85        style = StyleOptions()
 86        for data in m.post.message:
 87            for title, val in data.items():
 88                msg = val.get("data")
 89                style = val.get("options", StyleOptions())
 90                header = ""
 91
 92                if isinstance(msg, PosixPath) and msg.exists():
 93                    comment = textwrap.dedent(
 94                        f"{_header_text(header_title)}{header_text.rstrip()}"
 95                    ) if style.use_comment else ""
 96                    self._call_files_upload(
 97                        channel=m.data.channel_id,
 98                        title=title,
 99                        file=str(msg),
100                        initial_comment=comment,
101                        thread_ts=m.reply_ts,
102                        request_file_info=False,
103                    )
104
105                if isinstance(msg, str):
106                    if style.key_title and (title != header_title):
107                        header = _header_text(title)
108                    post_msg.append(
109                        f"{header}```\n{msg.rstrip()}\n```\n" if style.codeblock else f"{header}{msg.rstrip()}\n"
110                    )
111
112                if isinstance(msg, pd.DataFrame):
113                    if style.key_title and (title != header_title):
114                        header = _header_text(title)
115
116                    match m.status.command_type:
117                        case "results":
118                            match title:
119                                case "通算ポイント" | "ポイント差分":
120                                    post_msg.extend(_table_data(converter.df_to_text_table(msg, step=40)))
121                                case "役満和了" | "卓外ポイント" | "その他":
122                                    if "回数" in msg.columns:
123                                        post_msg.extend(_table_data(converter.df_to_count(msg, title, 1)))
124                                    else:
125                                        post_msg.extend(_table_data(converter.df_to_remarks(msg)))
126                                case "座席データ":
127                                    post_msg.extend(_table_data(converter.df_to_seat_data(msg, 1)))
128                                case "戦績":
129                                    if "東家 名前" in msg.columns:  # 縦持ちデータ
130                                        post_msg.extend(_table_data(converter.df_to_results_details(msg)))
131                                    else:
132                                        post_msg.extend(_table_data(converter.df_to_results_simple(msg)))
133                                case _:
134                                    post_msg.extend(_table_data(converter.df_to_remarks(msg)))
135                        case "rating":
136                            post_msg.extend(_table_data(converter.df_to_text_table(msg, step=20)))
137                        case "ranking":
138                            post_msg.extend(_table_data(converter.df_to_ranking(msg, title, step=50)))
139
140        if style.summarize:
141            post_msg = formatter.group_strings(post_msg)
142
143        for msg in post_msg:
144            self._call_chat_post_message(
145                channel=m.data.channel_id,
146                text=msg,
147                thread_ts=m.reply_ts,
148            )
149
150    def _call_chat_post_message(self, **kwargs) -> "SlackResponse":
151        """slackにメッセージをポストする
152
153        Returns:
154            SlackResponse: API response
155        """
156
157        res = cast("SlackResponse", {})
158        if kwargs["thread_ts"] == "0":
159            kwargs.pop("thread_ts")
160
161        try:
162            res = self.appclient.chat_postMessage(**kwargs)
163        except self.slack_api_error as err:
164            logging.critical(err)
165            logging.error("kwargs=%s", kwargs)
166
167        return res
168
169    def _call_files_upload(self, **kwargs) -> "SlackResponse":
170        """slackにファイルをアップロードする
171
172        Returns:
173            SlackResponse | Any: API response
174        """
175
176        res = cast("SlackResponse", {})
177        if kwargs.get("thread_ts", "0") == "0":
178            kwargs.pop("thread_ts")
179
180        try:
181            res = self.appclient.files_upload_v2(**kwargs)
182        except self.slack_api_error as err:
183            logging.critical(err)
184            logging.error("kwargs=%s", kwargs)
185
186        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):
 34        super().__init__()
 35
 36        try:
 37            from slack_sdk.errors import SlackApiError
 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"):
 43        """メッセージをポストする
 44
 45        Args:
 46            m (MessageParserProtocol): メッセージデータ
 47        """
 48
 49        def _header_text(title: str) -> str:
 50            if not title.isnumeric() and title:  # 数値のキーはヘッダにしない
 51                return f"*【{title}】*\n"
 52            return ""
 53
 54        def _table_data(data: dict) -> list:
 55            ret_list: list = []
 56            text_data = iter(data.values())
 57            # 先頭ブロックの処理(ヘッダ追加)
 58            v = next(text_data)
 59            ret_list.append(f"{header}```\n{v}\n```\n" if style.codeblock else f"{header}{v}\n")
 60            # 残りのブロック
 61            for v in text_data:
 62                ret_list.append(f"```\n{v}\n```\n" if style.codeblock else f"```\n{v}\n```\n")
 63            return ret_list
 64
 65        if not m.in_thread:
 66            m.post.thread = False
 67
 68        # 見出しポスト
 69        header_title = ""
 70        header_text = ""
 71        if m.post.headline:
 72            header_title, header_text = next(iter(m.post.headline.items()))
 73            if not all(v["options"].header_hidden for x in m.post.message for _, v in x.items()):
 74                res = self._call_chat_post_message(
 75                    channel=m.data.channel_id,
 76                    text=f"{_header_text(header_title)}{header_text.rstrip()}",
 77                    thread_ts=m.reply_ts,
 78                )
 79                if res.status_code == 200:  # 見出しがある場合はスレッドにする
 80                    m.post.ts = res.get("ts", "undetermined")
 81                else:
 82                    m.post.ts = "undetermined"
 83
 84        # 本文
 85        post_msg: list[str] = []
 86        style = StyleOptions()
 87        for data in m.post.message:
 88            for title, val in data.items():
 89                msg = val.get("data")
 90                style = val.get("options", StyleOptions())
 91                header = ""
 92
 93                if isinstance(msg, PosixPath) and msg.exists():
 94                    comment = textwrap.dedent(
 95                        f"{_header_text(header_title)}{header_text.rstrip()}"
 96                    ) if style.use_comment else ""
 97                    self._call_files_upload(
 98                        channel=m.data.channel_id,
 99                        title=title,
100                        file=str(msg),
101                        initial_comment=comment,
102                        thread_ts=m.reply_ts,
103                        request_file_info=False,
104                    )
105
106                if isinstance(msg, str):
107                    if style.key_title and (title != header_title):
108                        header = _header_text(title)
109                    post_msg.append(
110                        f"{header}```\n{msg.rstrip()}\n```\n" if style.codeblock else f"{header}{msg.rstrip()}\n"
111                    )
112
113                if isinstance(msg, pd.DataFrame):
114                    if style.key_title and (title != header_title):
115                        header = _header_text(title)
116
117                    match m.status.command_type:
118                        case "results":
119                            match title:
120                                case "通算ポイント" | "ポイント差分":
121                                    post_msg.extend(_table_data(converter.df_to_text_table(msg, step=40)))
122                                case "役満和了" | "卓外ポイント" | "その他":
123                                    if "回数" in msg.columns:
124                                        post_msg.extend(_table_data(converter.df_to_count(msg, title, 1)))
125                                    else:
126                                        post_msg.extend(_table_data(converter.df_to_remarks(msg)))
127                                case "座席データ":
128                                    post_msg.extend(_table_data(converter.df_to_seat_data(msg, 1)))
129                                case "戦績":
130                                    if "東家 名前" in msg.columns:  # 縦持ちデータ
131                                        post_msg.extend(_table_data(converter.df_to_results_details(msg)))
132                                    else:
133                                        post_msg.extend(_table_data(converter.df_to_results_simple(msg)))
134                                case _:
135                                    post_msg.extend(_table_data(converter.df_to_remarks(msg)))
136                        case "rating":
137                            post_msg.extend(_table_data(converter.df_to_text_table(msg, step=20)))
138                        case "ranking":
139                            post_msg.extend(_table_data(converter.df_to_ranking(msg, title, step=50)))
140
141        if style.summarize:
142            post_msg = formatter.group_strings(post_msg)
143
144        for msg in post_msg:
145            self._call_chat_post_message(
146                channel=m.data.channel_id,
147                text=msg,
148                thread_ts=m.reply_ts,
149            )
150
151    def _call_chat_post_message(self, **kwargs) -> "SlackResponse":
152        """slackにメッセージをポストする
153
154        Returns:
155            SlackResponse: API response
156        """
157
158        res = cast("SlackResponse", {})
159        if kwargs["thread_ts"] == "0":
160            kwargs.pop("thread_ts")
161
162        try:
163            res = self.appclient.chat_postMessage(**kwargs)
164        except self.slack_api_error as err:
165            logging.critical(err)
166            logging.error("kwargs=%s", kwargs)
167
168        return res
169
170    def _call_files_upload(self, **kwargs) -> "SlackResponse":
171        """slackにファイルをアップロードする
172
173        Returns:
174            SlackResponse | Any: API response
175        """
176
177        res = cast("SlackResponse", {})
178        if kwargs.get("thread_ts", "0") == "0":
179            kwargs.pop("thread_ts")
180
181        try:
182            res = self.appclient.files_upload_v2(**kwargs)
183        except self.slack_api_error as err:
184            logging.critical(err)
185            logging.error("kwargs=%s", kwargs)
186
187        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):
 42    def post(self, m: "MessageParserProtocol"):
 43        """メッセージをポストする
 44
 45        Args:
 46            m (MessageParserProtocol): メッセージデータ
 47        """
 48
 49        def _header_text(title: str) -> str:
 50            if not title.isnumeric() and title:  # 数値のキーはヘッダにしない
 51                return f"*【{title}】*\n"
 52            return ""
 53
 54        def _table_data(data: dict) -> list:
 55            ret_list: list = []
 56            text_data = iter(data.values())
 57            # 先頭ブロックの処理(ヘッダ追加)
 58            v = next(text_data)
 59            ret_list.append(f"{header}```\n{v}\n```\n" if style.codeblock else f"{header}{v}\n")
 60            # 残りのブロック
 61            for v in text_data:
 62                ret_list.append(f"```\n{v}\n```\n" if style.codeblock else f"```\n{v}\n```\n")
 63            return ret_list
 64
 65        if not m.in_thread:
 66            m.post.thread = False
 67
 68        # 見出しポスト
 69        header_title = ""
 70        header_text = ""
 71        if m.post.headline:
 72            header_title, header_text = next(iter(m.post.headline.items()))
 73            if not all(v["options"].header_hidden for x in m.post.message for _, v in x.items()):
 74                res = self._call_chat_post_message(
 75                    channel=m.data.channel_id,
 76                    text=f"{_header_text(header_title)}{header_text.rstrip()}",
 77                    thread_ts=m.reply_ts,
 78                )
 79                if res.status_code == 200:  # 見出しがある場合はスレッドにする
 80                    m.post.ts = res.get("ts", "undetermined")
 81                else:
 82                    m.post.ts = "undetermined"
 83
 84        # 本文
 85        post_msg: list[str] = []
 86        style = StyleOptions()
 87        for data in m.post.message:
 88            for title, val in data.items():
 89                msg = val.get("data")
 90                style = val.get("options", StyleOptions())
 91                header = ""
 92
 93                if isinstance(msg, PosixPath) and msg.exists():
 94                    comment = textwrap.dedent(
 95                        f"{_header_text(header_title)}{header_text.rstrip()}"
 96                    ) if style.use_comment else ""
 97                    self._call_files_upload(
 98                        channel=m.data.channel_id,
 99                        title=title,
100                        file=str(msg),
101                        initial_comment=comment,
102                        thread_ts=m.reply_ts,
103                        request_file_info=False,
104                    )
105
106                if isinstance(msg, str):
107                    if style.key_title and (title != header_title):
108                        header = _header_text(title)
109                    post_msg.append(
110                        f"{header}```\n{msg.rstrip()}\n```\n" if style.codeblock else f"{header}{msg.rstrip()}\n"
111                    )
112
113                if isinstance(msg, pd.DataFrame):
114                    if style.key_title and (title != header_title):
115                        header = _header_text(title)
116
117                    match m.status.command_type:
118                        case "results":
119                            match title:
120                                case "通算ポイント" | "ポイント差分":
121                                    post_msg.extend(_table_data(converter.df_to_text_table(msg, step=40)))
122                                case "役満和了" | "卓外ポイント" | "その他":
123                                    if "回数" in msg.columns:
124                                        post_msg.extend(_table_data(converter.df_to_count(msg, title, 1)))
125                                    else:
126                                        post_msg.extend(_table_data(converter.df_to_remarks(msg)))
127                                case "座席データ":
128                                    post_msg.extend(_table_data(converter.df_to_seat_data(msg, 1)))
129                                case "戦績":
130                                    if "東家 名前" in msg.columns:  # 縦持ちデータ
131                                        post_msg.extend(_table_data(converter.df_to_results_details(msg)))
132                                    else:
133                                        post_msg.extend(_table_data(converter.df_to_results_simple(msg)))
134                                case _:
135                                    post_msg.extend(_table_data(converter.df_to_remarks(msg)))
136                        case "rating":
137                            post_msg.extend(_table_data(converter.df_to_text_table(msg, step=20)))
138                        case "ranking":
139                            post_msg.extend(_table_data(converter.df_to_ranking(msg, title, step=50)))
140
141        if style.summarize:
142            post_msg = formatter.group_strings(post_msg)
143
144        for msg in post_msg:
145            self._call_chat_post_message(
146                channel=m.data.channel_id,
147                text=msg,
148                thread_ts=m.reply_ts,
149            )

メッセージをポストする

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