libs.functions.slack_api

libs/functions/slack_api.py

  1"""
  2libs/functions/slack_api.py
  3"""
  4
  5import logging
  6import re
  7from typing import Any, cast
  8
  9from slack_sdk.errors import SlackApiError
 10from slack_sdk.web import SlackResponse
 11
 12import libs.global_value as g
 13from libs.utils import formatter
 14
 15
 16def call_chat_post_message(**kwargs) -> SlackResponse | Any:
 17    """slackにメッセージをポストする
 18
 19    Returns:
 20        SlackResponse | Any: API response
 21    """
 22
 23    res = None
 24    if not kwargs["thread_ts"]:
 25        kwargs.pop("thread_ts")
 26    try:
 27        res = g.app.client.chat_postMessage(**kwargs)
 28    except SlackApiError as e:
 29        logging.critical(e)
 30        logging.error("kwargs=%s", kwargs)
 31        logging.error("msg: %s", vars(g.msg))
 32
 33    return res
 34
 35
 36def call_files_upload(**kwargs) -> SlackResponse | Any:
 37    """slackにファイルをアップロードする
 38
 39    Returns:
 40        SlackResponse | Any: API response
 41    """
 42
 43    res = None
 44    if not kwargs["thread_ts"]:
 45        kwargs.pop("thread_ts")
 46    try:
 47        res = g.app.client.files_upload_v2(**kwargs)
 48    except SlackApiError as e:
 49        logging.critical(e)
 50        logging.error("kwargs=%s", kwargs)
 51        logging.error("msg: %s", vars(g.msg))
 52
 53    return res
 54
 55
 56def post_message(message: str, ts=False) -> SlackResponse | Any:
 57    """chat_postMessageに渡すパラメータを設定
 58
 59    Args:
 60        message (str): ポストするメッセージ
 61        ts (bool, optional): スレッドに返す. Defaults to False.
 62
 63    Returns:
 64        SlackResponse | Any: API response
 65    """
 66
 67    res: dict | Any = {}
 68    if not ts and g.msg.thread_ts:
 69        ts = g.msg.thread_ts
 70
 71    if g.args.testcase:
 72        formatter.debug_out(message)
 73        res["ts"] = 0  # dummy
 74    else:
 75        res = call_chat_post_message(
 76            channel=g.msg.channel_id,
 77            text=f"{message.strip()}",
 78            thread_ts=ts,
 79        )
 80
 81    return res
 82
 83
 84def post_multi_message(msg: dict, ts: bool | None = False, summarize: bool = True) -> None:
 85    """メッセージを分割してポスト
 86
 87    Args:
 88        msg (dict): ポストするメッセージ
 89        ts (bool, optional): スレッドに返す. Defaults to False.
 90        summarize (bool, optional): 可能な限り1つのブロックにまとめる. Defaults to True.
 91    """
 92
 93    if g.args.testcase:
 94        formatter.debug_out("", msg)
 95    else:
 96        if isinstance(msg, dict):
 97            if summarize:  # まとめてポスト
 98                key_list = list(map(str, msg.keys()))
 99                post_msg = msg[key_list[0]]
100                for i in key_list[1:]:
101                    if len((post_msg + msg[i])) < 3800:  # 3800文字を超える直前までまとめる
102                        post_msg += msg[i]
103                    else:
104                        post_message(post_msg, ts)
105                        post_msg = msg[i]
106                post_message(post_msg, ts)
107            else:  # そのままポスト
108                for i in msg.keys():
109                    post_message(msg[i], ts)
110        else:
111            post_message(msg, ts)
112
113
114def post_text(event_ts: str, title: str, msg: str) -> SlackResponse | Any:
115    """コードブロック修飾付きポスト
116
117    Args:
118        event_ts (str): スレッドに返す
119        title (str): タイトル行
120        msg (str): 本文
121
122    Returns:
123        SlackResponse | Any: API response
124    """
125
126    # コードブロック修飾付きポスト
127    if len(re.sub(r"\n+", "\n", f"{msg.strip()}").splitlines()) == 1:
128        res = call_chat_post_message(
129            channel=g.msg.channel_id,
130            text=f"{title}\n{msg.strip()}",
131            thread_ts=event_ts,
132        )
133    else:
134        # ポスト予定のメッセージをstep行単位のブロックに分割
135        step = 50
136        post_msg = []
137        for count in range(int(len(msg.splitlines()) / step) + 1):
138            post_msg.append(
139                "\n".join(msg.splitlines()[count * step:(count + 1) * step])
140            )
141
142        # 最終ブロックがstepの半分以下なら直前のブロックにまとめる
143        if len(post_msg) > 1 and step / 2 > len(post_msg[count].splitlines()):
144            post_msg[count - 1] += "\n" + post_msg.pop(count)
145
146        # ブロック単位でポスト
147        for _, val in enumerate(post_msg):
148            res = call_chat_post_message(
149                channel=g.msg.channel_id,
150                text=f"\n{title}\n\n```{val.strip()}```",
151                thread_ts=event_ts,
152            )
153
154    return res
155
156
157def post_fileupload(title: str, file: str | bool, ts: str | bool = False) -> SlackResponse | None:
158    """files_upload_v2に渡すパラメータを設定
159
160    Args:
161        title (str): タイトル行
162        file (str): アップロードファイルパス
163        ts (str | bool, optional): スレッドに返す. Defaults to False.
164
165    Returns:
166        SlackResponse | None: 結果
167    """
168
169    if g.args.testcase:
170        formatter.debug_out(title, file)
171        return None
172
173    if not ts and g.msg.thread_ts:
174        ts = g.msg.thread_ts
175
176    res = call_files_upload(
177        channel=g.msg.channel_id,
178        title=title,
179        file=file,
180        thread_ts=ts,
181        request_file_info=False,
182    )
183
184    return res
185
186
187def slack_post(**kwargs):
188    """パラメータの内容によって呼び出すAPIを振り分ける"""
189
190    logging.debug(kwargs)
191    headline = str(kwargs.get("headline", ""))
192    message = kwargs.get("message")
193    summarize = bool(kwargs.get("summarize", True))
194    file_list = cast(dict, kwargs.get("file_list", {"dummy": ""}))
195
196    # 見出しポスト
197    if (res := post_message(headline)):
198        ts = res.get("ts", False)
199    else:
200        ts = False
201
202    # 本文ポスト
203    for x in file_list:
204        if (file_path := file_list.get(x)):
205            post_fileupload(str(x), str(file_path), ts)
206            message = {}  # ファイルがあるメッセージは不要
207
208    if message:
209        post_multi_message(message, ts, summarize)
210
211
212def call_reactions_add(icon: str, ch: str | None = None, ts: str | None = None):
213    """リアクションを付ける
214
215    Args:
216        icon (str): 付けるリアクション
217        ch (str | None, optional): チャンネルID. Defaults to None.
218        ts (str | None, optional): メッセージのタイムスタンプ. Defaults to None.
219    """
220
221    if not ch:
222        ch = g.msg.channel_id
223    if not ts:
224        ts = g.msg.event_ts
225
226    try:
227        res: SlackResponse = g.app.client.reactions_add(
228            channel=str(ch),
229            name=icon,
230            timestamp=str(ts),
231        )
232        logging.info("ts=%s, ch=%s, icon=%s, %s", ts, ch, icon, res.validate())
233    except SlackApiError as e:
234        match e.response.get("error"):
235            case "already_reacted":
236                pass
237            case _:
238                logging.critical(e)
239                logging.critical("ts=%s, ch=%s, icon=%s", ts, ch, icon)
240                logging.error("msg: %s", vars(g.msg))
241
242
243def call_reactions_remove(icon: str, ch: str | None = None, ts: str | None = None):
244    """リアクションを外す
245
246    Args:
247        icon (str): 外すリアクション
248        ch (str | None, optional): チャンネルID. Defaults to None.
249        ts (str | None, optional): メッセージのタイムスタンプ. Defaults to None.
250    """
251
252    if not ch:
253        ch = g.msg.channel_id
254    if not ts:
255        ts = g.msg.event_ts
256
257    try:
258        res = g.app.client.reactions_remove(
259            channel=ch,
260            name=icon,
261            timestamp=ts,
262        )
263        logging.info("ts=%s, ch=%s, icon=%s, %s", ts, ch, icon, res.validate())
264    except SlackApiError as e:
265        match e.response.get("error"):
266            case "no_reaction":
267                pass
268            case _:
269                logging.critical(e)
270                logging.critical("ts=%s, ch=%s, icon=%s", ts, ch, icon)
271                logging.error("msg: %s", vars(g.msg))
def call_chat_post_message(**kwargs) -> slack_sdk.web.slack_response.SlackResponse | typing.Any:
17def call_chat_post_message(**kwargs) -> SlackResponse | Any:
18    """slackにメッセージをポストする
19
20    Returns:
21        SlackResponse | Any: API response
22    """
23
24    res = None
25    if not kwargs["thread_ts"]:
26        kwargs.pop("thread_ts")
27    try:
28        res = g.app.client.chat_postMessage(**kwargs)
29    except SlackApiError as e:
30        logging.critical(e)
31        logging.error("kwargs=%s", kwargs)
32        logging.error("msg: %s", vars(g.msg))
33
34    return res

slackにメッセージをポストする

Returns:

SlackResponse | Any: API response

def call_files_upload(**kwargs) -> slack_sdk.web.slack_response.SlackResponse | typing.Any:
37def call_files_upload(**kwargs) -> SlackResponse | Any:
38    """slackにファイルをアップロードする
39
40    Returns:
41        SlackResponse | Any: API response
42    """
43
44    res = None
45    if not kwargs["thread_ts"]:
46        kwargs.pop("thread_ts")
47    try:
48        res = g.app.client.files_upload_v2(**kwargs)
49    except SlackApiError as e:
50        logging.critical(e)
51        logging.error("kwargs=%s", kwargs)
52        logging.error("msg: %s", vars(g.msg))
53
54    return res

slackにファイルをアップロードする

Returns:

SlackResponse | Any: API response

def post_message( message: str, ts=False) -> slack_sdk.web.slack_response.SlackResponse | typing.Any:
57def post_message(message: str, ts=False) -> SlackResponse | Any:
58    """chat_postMessageに渡すパラメータを設定
59
60    Args:
61        message (str): ポストするメッセージ
62        ts (bool, optional): スレッドに返す. Defaults to False.
63
64    Returns:
65        SlackResponse | Any: API response
66    """
67
68    res: dict | Any = {}
69    if not ts and g.msg.thread_ts:
70        ts = g.msg.thread_ts
71
72    if g.args.testcase:
73        formatter.debug_out(message)
74        res["ts"] = 0  # dummy
75    else:
76        res = call_chat_post_message(
77            channel=g.msg.channel_id,
78            text=f"{message.strip()}",
79            thread_ts=ts,
80        )
81
82    return res

chat_postMessageに渡すパラメータを設定

Arguments:
  • message (str): ポストするメッセージ
  • ts (bool, optional): スレッドに返す. Defaults to False.
Returns:

SlackResponse | Any: API response

def post_multi_message(msg: dict, ts: bool | None = False, summarize: bool = True) -> None:
 85def post_multi_message(msg: dict, ts: bool | None = False, summarize: bool = True) -> None:
 86    """メッセージを分割してポスト
 87
 88    Args:
 89        msg (dict): ポストするメッセージ
 90        ts (bool, optional): スレッドに返す. Defaults to False.
 91        summarize (bool, optional): 可能な限り1つのブロックにまとめる. Defaults to True.
 92    """
 93
 94    if g.args.testcase:
 95        formatter.debug_out("", msg)
 96    else:
 97        if isinstance(msg, dict):
 98            if summarize:  # まとめてポスト
 99                key_list = list(map(str, msg.keys()))
100                post_msg = msg[key_list[0]]
101                for i in key_list[1:]:
102                    if len((post_msg + msg[i])) < 3800:  # 3800文字を超える直前までまとめる
103                        post_msg += msg[i]
104                    else:
105                        post_message(post_msg, ts)
106                        post_msg = msg[i]
107                post_message(post_msg, ts)
108            else:  # そのままポスト
109                for i in msg.keys():
110                    post_message(msg[i], ts)
111        else:
112            post_message(msg, ts)

メッセージを分割してポスト

Arguments:
  • msg (dict): ポストするメッセージ
  • ts (bool, optional): スレッドに返す. Defaults to False.
  • summarize (bool, optional): 可能な限り1つのブロックにまとめる. Defaults to True.
def post_text( event_ts: str, title: str, msg: str) -> slack_sdk.web.slack_response.SlackResponse | typing.Any:
115def post_text(event_ts: str, title: str, msg: str) -> SlackResponse | Any:
116    """コードブロック修飾付きポスト
117
118    Args:
119        event_ts (str): スレッドに返す
120        title (str): タイトル行
121        msg (str): 本文
122
123    Returns:
124        SlackResponse | Any: API response
125    """
126
127    # コードブロック修飾付きポスト
128    if len(re.sub(r"\n+", "\n", f"{msg.strip()}").splitlines()) == 1:
129        res = call_chat_post_message(
130            channel=g.msg.channel_id,
131            text=f"{title}\n{msg.strip()}",
132            thread_ts=event_ts,
133        )
134    else:
135        # ポスト予定のメッセージをstep行単位のブロックに分割
136        step = 50
137        post_msg = []
138        for count in range(int(len(msg.splitlines()) / step) + 1):
139            post_msg.append(
140                "\n".join(msg.splitlines()[count * step:(count + 1) * step])
141            )
142
143        # 最終ブロックがstepの半分以下なら直前のブロックにまとめる
144        if len(post_msg) > 1 and step / 2 > len(post_msg[count].splitlines()):
145            post_msg[count - 1] += "\n" + post_msg.pop(count)
146
147        # ブロック単位でポスト
148        for _, val in enumerate(post_msg):
149            res = call_chat_post_message(
150                channel=g.msg.channel_id,
151                text=f"\n{title}\n\n```{val.strip()}```",
152                thread_ts=event_ts,
153            )
154
155    return res

コードブロック修飾付きポスト

Arguments:
  • event_ts (str): スレッドに返す
  • title (str): タイトル行
  • msg (str): 本文
Returns:

SlackResponse | Any: API response

def post_fileupload( title: str, file: str | bool, ts: str | bool = False) -> slack_sdk.web.slack_response.SlackResponse | None:
158def post_fileupload(title: str, file: str | bool, ts: str | bool = False) -> SlackResponse | None:
159    """files_upload_v2に渡すパラメータを設定
160
161    Args:
162        title (str): タイトル行
163        file (str): アップロードファイルパス
164        ts (str | bool, optional): スレッドに返す. Defaults to False.
165
166    Returns:
167        SlackResponse | None: 結果
168    """
169
170    if g.args.testcase:
171        formatter.debug_out(title, file)
172        return None
173
174    if not ts and g.msg.thread_ts:
175        ts = g.msg.thread_ts
176
177    res = call_files_upload(
178        channel=g.msg.channel_id,
179        title=title,
180        file=file,
181        thread_ts=ts,
182        request_file_info=False,
183    )
184
185    return res

files_upload_v2に渡すパラメータを設定

Arguments:
  • title (str): タイトル行
  • file (str): アップロードファイルパス
  • ts (str | bool, optional): スレッドに返す. Defaults to False.
Returns:

SlackResponse | None: 結果

def slack_post(**kwargs):
188def slack_post(**kwargs):
189    """パラメータの内容によって呼び出すAPIを振り分ける"""
190
191    logging.debug(kwargs)
192    headline = str(kwargs.get("headline", ""))
193    message = kwargs.get("message")
194    summarize = bool(kwargs.get("summarize", True))
195    file_list = cast(dict, kwargs.get("file_list", {"dummy": ""}))
196
197    # 見出しポスト
198    if (res := post_message(headline)):
199        ts = res.get("ts", False)
200    else:
201        ts = False
202
203    # 本文ポスト
204    for x in file_list:
205        if (file_path := file_list.get(x)):
206            post_fileupload(str(x), str(file_path), ts)
207            message = {}  # ファイルがあるメッセージは不要
208
209    if message:
210        post_multi_message(message, ts, summarize)

パラメータの内容によって呼び出すAPIを振り分ける

def call_reactions_add(icon: str, ch: str | None = None, ts: str | None = None):
213def call_reactions_add(icon: str, ch: str | None = None, ts: str | None = None):
214    """リアクションを付ける
215
216    Args:
217        icon (str): 付けるリアクション
218        ch (str | None, optional): チャンネルID. Defaults to None.
219        ts (str | None, optional): メッセージのタイムスタンプ. Defaults to None.
220    """
221
222    if not ch:
223        ch = g.msg.channel_id
224    if not ts:
225        ts = g.msg.event_ts
226
227    try:
228        res: SlackResponse = g.app.client.reactions_add(
229            channel=str(ch),
230            name=icon,
231            timestamp=str(ts),
232        )
233        logging.info("ts=%s, ch=%s, icon=%s, %s", ts, ch, icon, res.validate())
234    except SlackApiError as e:
235        match e.response.get("error"):
236            case "already_reacted":
237                pass
238            case _:
239                logging.critical(e)
240                logging.critical("ts=%s, ch=%s, icon=%s", ts, ch, icon)
241                logging.error("msg: %s", vars(g.msg))

リアクションを付ける

Arguments:
  • icon (str): 付けるリアクション
  • ch (str | None, optional): チャンネルID. Defaults to None.
  • ts (str | None, optional): メッセージのタイムスタンプ. Defaults to None.
def call_reactions_remove(icon: str, ch: str | None = None, ts: str | None = None):
244def call_reactions_remove(icon: str, ch: str | None = None, ts: str | None = None):
245    """リアクションを外す
246
247    Args:
248        icon (str): 外すリアクション
249        ch (str | None, optional): チャンネルID. Defaults to None.
250        ts (str | None, optional): メッセージのタイムスタンプ. Defaults to None.
251    """
252
253    if not ch:
254        ch = g.msg.channel_id
255    if not ts:
256        ts = g.msg.event_ts
257
258    try:
259        res = g.app.client.reactions_remove(
260            channel=ch,
261            name=icon,
262            timestamp=ts,
263        )
264        logging.info("ts=%s, ch=%s, icon=%s, %s", ts, ch, icon, res.validate())
265    except SlackApiError as e:
266        match e.response.get("error"):
267            case "no_reaction":
268                pass
269            case _:
270                logging.critical(e)
271                logging.critical("ts=%s, ch=%s, icon=%s", ts, ch, icon)
272                logging.error("msg: %s", vars(g.msg))

リアクションを外す

Arguments:
  • icon (str): 外すリアクション
  • ch (str | None, optional): チャンネルID. Defaults to None.
  • ts (str | None, optional): メッセージのタイムスタンプ. Defaults to None.