integrations.slack.functions
integrations/slack/functions.py
1""" 2integrations/slack/functions.py 3""" 4 5import logging 6from typing import TYPE_CHECKING, Any, Union, cast 7 8import libs.global_value as g 9from integrations.base.interface import FunctionsInterface 10from libs.functions import lookup 11from libs.types import ActionStatus 12from libs.utils import validator 13from libs.utils.timekit import Delimiter, Format 14from libs.utils.timekit import ExtendedDatetime as ExtDt 15 16if TYPE_CHECKING: 17 from slack_sdk.web import SlackResponse 18 19 from integrations.protocols import MessageParserProtocol 20 from integrations.slack.adapter import ServiceAdapter 21 from integrations.slack.api import AdapterAPI 22 from integrations.slack.config import SvcConfig 23 24 25class SvcFunctions(FunctionsInterface): 26 """slack専用関数""" 27 28 def __init__(self, api: "AdapterAPI", conf: "SvcConfig"): 29 super().__init__() 30 31 try: 32 from slack_sdk.errors import SlackApiError 33 34 self.slack_api_error = SlackApiError 35 except ModuleNotFoundError as err: 36 raise ModuleNotFoundError(err.msg) from None 37 38 self.api = api 39 """API操作オブジェクト""" 40 self.conf = conf 41 """個別設定""" 42 43 def get_messages(self, words: Union[str, list[str]]) -> list["MessageParserProtocol"]: 44 """ 45 slackログからメッセージを検索して返す 46 47 Args: 48 words (Union[str, list[str]]): 検索するワード 49 50 Returns: 51 list[MessageParserProtocol]: 検索した結果 52 53 """ 54 g.adapter = cast("ServiceAdapter", g.adapter) 55 if isinstance(words, list): 56 words = " ".join(words) 57 58 # 検索クエリ 59 after = ExtDt(days=-self.conf.search_after, hours=g.cfg.setting.time_adjust).format(Format.YMD, Delimiter.HYPHEN) 60 channel = " ".join([f"in:{x}" for x in self.conf.search_channel]) 61 query = f"{words} {channel} after:{after}" 62 logging.info("query=%s, check_db=%s", query, g.cfg.setting.database_file) 63 64 # データ取得 65 response = self.api.webclient.search_messages(query=query, sort="timestamp", sort_dir="asc", count=100) 66 matches = response["messages"]["matches"] # 1ページ目 67 for p in range(2, response["messages"]["paging"]["pages"] + 1): 68 response = self.api.webclient.search_messages(query=query, sort="timestamp", sort_dir="asc", count=100, page=p) 69 matches += response["messages"]["matches"] # 2ページ目以降 70 71 # 必要なデータだけ辞書に格納 72 data: list["MessageParserProtocol"] = [] 73 for x in matches: 74 if isinstance(x, dict): 75 work_m = cast("MessageParserProtocol", g.adapter.parser()) 76 work_m.parser(x) 77 data.append(work_m) 78 79 return data 80 81 def get_message_details(self, matches: list["MessageParserProtocol"]) -> list["MessageParserProtocol"]: 82 """ 83 メッセージ詳細情報取得 84 85 Args: 86 matches (list[MessageParserProtocol]): 対象データ 87 88 Returns: 89 list[MessageParserProtocol]: 詳細情報追加データ 90 91 """ 92 new_matches: list["MessageParserProtocol"] = [] 93 94 # 詳細情報取得 95 for key in matches: 96 conversations = self.api.appclient.conversations_replies(channel=key.data.channel_id, ts=key.data.event_ts) 97 if msg := conversations.get("messages"): 98 res = cast(dict[str, Any], msg[0]) 99 else: 100 continue 101 102 if res: 103 # 各種時間取得 104 key.data.event_ts = str(res.get("ts", "0")) # イベント発生時間 105 key.data.thread_ts = str(res.get("thread_ts", "0")) # スレッドの先頭 106 key.data.edited_ts = str(cast(dict[str, Any], res.get("edited", {})).get("ts", "0")) # 編集時間 107 # リアクション取得 108 key.data.reaction_ok, key.data.reaction_ng = self.get_reactions_list(res) 109 110 new_matches.append(key) 111 112 return new_matches 113 114 def get_conversations(self, m: "MessageParserProtocol") -> dict[str, Any]: 115 """ 116 スレッド情報の取得 117 118 Args: 119 m (MessageParserProtocol): メッセージデータ 120 121 Returns: 122 dict[str, Any]: API response 123 124 """ 125 try: 126 res = self.api.appclient.conversations_replies(channel=m.data.channel_id, ts=m.data.event_ts) 127 logging.trace(res.validate()) # type: ignore 128 return cast(dict[str, Any], res) 129 except self.slack_api_error as err: 130 logging.error("slack_api_error: %s", err) 131 return {} 132 133 def get_reactions_list(self, msg: dict[str, Any]) -> tuple[list[str], list[str]]: 134 """ 135 botが付けたリアクションを取得 136 137 Args: 138 msg (dict[str, Any]): メッセージ内容 139 140 Returns: 141 tuple[list,list]: 142 - reaction_ok: okが付いているメッセージのタイムスタンプ 143 - reaction_ng: ngが付いているメッセージのタイムスタンプ 144 145 """ 146 reaction_ok: list[str] = [] 147 reaction_ng: list[str] = [] 148 149 if msg.get("reactions"): 150 for reactions in msg.get("reactions", {}): 151 if isinstance(reactions, dict) and self.conf.bot_id in reactions.get("users", []): 152 match reactions.get("name"): 153 case self.conf.reaction_ok: 154 reaction_ok.append(msg.get("ts", "")) 155 case self.conf.reaction_ng: 156 reaction_ng.append(msg.get("ts", "")) 157 158 return (reaction_ok, reaction_ng) 159 160 def get_channel_id(self) -> str: 161 """ 162 チャンネルIDを取得する 163 164 Returns: 165 str: チャンネルID 166 167 """ 168 channel_id = "" 169 170 try: 171 response = self.api.webclient.search_messages( 172 query=f"in:{self.conf.search_channel}", 173 count=1, 174 ) 175 messages: dict[str, Any] = response.get("messages", {}) 176 if messages.get("matches"): 177 channel = messages["matches"][0]["channel"] 178 if isinstance(self.conf.search_channel, str): 179 if channel["name"] in self.conf.search_channel: 180 channel_id = channel["id"] 181 else: 182 channel_id = channel["id"] 183 except self.slack_api_error as err: 184 logging.error("slack_api_error: %s", err) 185 186 return channel_id 187 188 def get_dm_channel_id(self, user_id: str) -> str: 189 """ 190 DMのチャンネルIDを取得する 191 192 Args: 193 user_id (str): DMの相手 194 195 Returns: 196 str: チャンネルID 197 198 """ 199 channel_id = "" 200 201 try: 202 response = self.api.appclient.conversations_open(users=[user_id]) 203 channel_id = response["channel"]["id"] 204 except self.slack_api_error as err: 205 logging.error("slack_api_error: %s", err) 206 207 return channel_id 208 209 def reaction_status(self, ch: str, ts: str) -> dict[str, list[str]]: 210 """ 211 botが付けたリアクションの種類を返す 212 213 Args: 214 ch (str): チャンネルID 215 ts (str): メッセージのタイムスタンプ 216 217 Returns: 218 dict[str, list[str]]: リアクション 219 - str: "oK" or "ng" 220 - list[str]: タイムスタンプ 221 222 """ 223 icon: dict[str, list[str]] = { 224 "ok": [], 225 "ng": [], 226 } 227 228 try: # 削除済みメッセージはエラーになるので潰す 229 res = self.api.appclient.reactions_get(channel=ch, timestamp=ts) 230 logging.trace(res.validate()) # type: ignore 231 except self.slack_api_error: 232 return icon 233 234 if reactions := cast(dict[str, Any], res["message"]).get("reactions"): 235 for reaction in cast(list[dict[str, Any]], reactions): 236 if reaction.get("name") == self.conf.reaction_ok and self.conf.bot_id in reaction["users"]: 237 icon["ok"].append(str(res["message"]["ts"])) 238 if reaction.get("name") == self.conf.reaction_ng and self.conf.bot_id in reaction["users"]: 239 icon["ng"].append(str(res["message"]["ts"])) 240 241 logging.debug("ch=%s, ts=%s, icon=%s", ch, ts, icon) 242 return icon 243 244 def reaction_append(self, icon: str, ch: str, ts: str) -> None: 245 """ 246 リアクション追加 247 248 Args: 249 icon (str): リアクション文字 250 ch (str): チャンネルID 251 ts (str): メッセージのタイムスタンプ 252 253 """ 254 if not all([icon, ch, ts]): 255 logging.warning("deficiency: ts=%s, ch=%s, icon=%s", ts, ch, icon) 256 return 257 258 try: 259 res: SlackResponse = self.api.appclient.reactions_add( 260 channel=str(ch), 261 name=icon, 262 timestamp=str(ts), 263 ) 264 logging.debug("ts=%s, ch=%s, icon=%s, %s", ts, ch, icon, res.validate()) # type: ignore[no-untyped-call] 265 except self.slack_api_error as err: 266 match cast(dict[str, Any], err.response).get("error"): 267 case "already_reacted": 268 pass 269 case _: 270 logging.error("slack_api_error: %s", err) 271 logging.error("ts=%s, ch=%s, icon=%s", ts, ch, icon) 272 273 def reaction_remove(self, icon: str, ch: str, ts: str) -> None: 274 """ 275 リアクション削除 276 277 Args: 278 icon (str): リアクション文字 279 ch (str): チャンネルID 280 ts (str): メッセージのタイムスタンプ 281 282 """ 283 if not all([icon, ch, ts]): 284 logging.warning("deficiency: ts=%s, ch=%s, icon=%s", ts, ch, icon) 285 return 286 287 try: 288 res = self.api.appclient.reactions_remove( 289 channel=ch, 290 name=icon, 291 timestamp=ts, 292 ) 293 logging.debug("ch=%s, ts=%s, icon=%s, %s", ch, ts, icon, res.validate()) # type: ignore[no-untyped-call] 294 except self.slack_api_error as err: 295 match cast(dict[str, Any], err.response).get("error"): 296 case "no_reaction": 297 pass 298 case "message_not_found": 299 pass 300 case "channel_not_found": 301 pass 302 case _: 303 logging.error("slack_api_error'(%s): %s", self.slack_api_error, err) 304 logging.error("ch=%s, ts=%s, icon=%s", ch, ts, icon) 305 306 def pickup_score(self) -> list["MessageParserProtocol"]: 307 """ 308 過去ログからスコア記録を検索して返す 309 310 Returns: 311 list[MessageParserProtocol]: 検索した結果 312 313 """ 314 # ゲーム結果の抽出 315 score_matches: list["MessageParserProtocol"] = [] 316 for keyword in g.cfg.rule.keyword_mapping.keys(): 317 for match in self.get_messages(keyword): 318 if validator.check_score(match): 319 if match.ignore_user: # 除外ユーザからのポストは破棄 320 logging.info("skip ignore user: %s", match.data.user_id) 321 continue 322 if match.data.text != match.data.text.replace(keyword, ""): # 検索結果にキーワードが含まれているか 323 score_matches.append(match) 324 logging.debug("found: keyword=%s, %s", keyword, match.data) 325 326 # イベント詳細取得 327 if score_matches: 328 return self.get_message_details(score_matches) 329 return score_matches 330 331 def pickup_remarks(self) -> list["MessageParserProtocol"]: 332 """ 333 slackログからメモを検索して返す 334 335 Returns: 336 list[MessageParserProtocol]: 検索した結果 337 338 """ 339 remarks_matches: list["MessageParserProtocol"] = [] 340 341 # メモの抽出 342 for match in self.get_messages(g.cfg.rule.remarks_words): 343 if match.ignore_user: # 除外ユーザからのポストは破棄 344 logging.info("skip ignore user: %s", match.data.user_id) 345 continue 346 if match.keyword in g.cfg.rule.remarks_words: 347 remarks_matches.append(match) 348 349 # イベント詳細取得 350 if remarks_matches: 351 return self.get_message_details(remarks_matches) 352 return remarks_matches 353 354 def post_processing(self, m: "MessageParserProtocol") -> None: 355 """ 356 後処理 357 358 Args: 359 m (MessageParserProtocol): メッセージデータ 360 361 """ 362 363 # リアクション文字取得 364 def _resolve_reaction(name: str, fallback: str) -> Any: 365 section_name = f"slack_{m.data.channel_id}" 366 if channel_config := g.params.channel_config: 367 if value := lookup.get_config_value( 368 config_file=channel_config, 369 section=section_name, 370 name=name, 371 val_type=str, 372 ): 373 return value 374 375 if value := lookup.get_config_value( 376 config_file=g.cfg.config_file, 377 section=section_name, 378 name=name, 379 val_type=str, 380 ): 381 return value 382 383 return lookup.get_config_value( 384 config_file=g.cfg.config_file, 385 section="slack", 386 name=name, 387 val_type=str, 388 fallback=fallback, 389 ) 390 391 self.conf.reaction_ok = str(_resolve_reaction("reaction_ok", self.conf.reaction_ok)) 392 self.conf.reaction_ng = str(_resolve_reaction("reaction_ng", self.conf.reaction_ng)) 393 394 # リアクション処理 395 match m.status.action: 396 case ActionStatus.NOTHING: 397 return 398 case ActionStatus.CHANGE: 399 for ts in m.status.target_ts: 400 reaction_data = self.reaction_status(ch=m.data.channel_id, ts=ts) 401 if m.status.reaction: # NGを外してOKを付ける 402 if not reaction_data.get("ok"): 403 self.reaction_append(icon=self.conf.reaction_ok, ch=m.data.channel_id, ts=ts) 404 if reaction_data.get("ng"): 405 self.reaction_remove(icon=self.conf.reaction_ng, ch=m.data.channel_id, ts=ts) 406 else: # OKを外してNGを付ける 407 if reaction_data.get("ok"): 408 self.reaction_remove(icon=self.conf.reaction_ok, ch=m.data.channel_id, ts=ts) 409 if not reaction_data.get("ng"): 410 self.reaction_append(icon=self.conf.reaction_ng, ch=m.data.channel_id, ts=ts) 411 case ActionStatus.DELETE: 412 for ts in m.status.target_ts: 413 self.reaction_remove(icon=self.conf.reaction_ok, ch=m.data.channel_id, ts=ts) 414 self.reaction_remove(icon=self.conf.reaction_ng, ch=m.data.channel_id, ts=ts) 415 m.status.reset()
26class SvcFunctions(FunctionsInterface): 27 """slack専用関数""" 28 29 def __init__(self, api: "AdapterAPI", conf: "SvcConfig"): 30 super().__init__() 31 32 try: 33 from slack_sdk.errors import SlackApiError 34 35 self.slack_api_error = SlackApiError 36 except ModuleNotFoundError as err: 37 raise ModuleNotFoundError(err.msg) from None 38 39 self.api = api 40 """API操作オブジェクト""" 41 self.conf = conf 42 """個別設定""" 43 44 def get_messages(self, words: Union[str, list[str]]) -> list["MessageParserProtocol"]: 45 """ 46 slackログからメッセージを検索して返す 47 48 Args: 49 words (Union[str, list[str]]): 検索するワード 50 51 Returns: 52 list[MessageParserProtocol]: 検索した結果 53 54 """ 55 g.adapter = cast("ServiceAdapter", g.adapter) 56 if isinstance(words, list): 57 words = " ".join(words) 58 59 # 検索クエリ 60 after = ExtDt(days=-self.conf.search_after, hours=g.cfg.setting.time_adjust).format(Format.YMD, Delimiter.HYPHEN) 61 channel = " ".join([f"in:{x}" for x in self.conf.search_channel]) 62 query = f"{words} {channel} after:{after}" 63 logging.info("query=%s, check_db=%s", query, g.cfg.setting.database_file) 64 65 # データ取得 66 response = self.api.webclient.search_messages(query=query, sort="timestamp", sort_dir="asc", count=100) 67 matches = response["messages"]["matches"] # 1ページ目 68 for p in range(2, response["messages"]["paging"]["pages"] + 1): 69 response = self.api.webclient.search_messages(query=query, sort="timestamp", sort_dir="asc", count=100, page=p) 70 matches += response["messages"]["matches"] # 2ページ目以降 71 72 # 必要なデータだけ辞書に格納 73 data: list["MessageParserProtocol"] = [] 74 for x in matches: 75 if isinstance(x, dict): 76 work_m = cast("MessageParserProtocol", g.adapter.parser()) 77 work_m.parser(x) 78 data.append(work_m) 79 80 return data 81 82 def get_message_details(self, matches: list["MessageParserProtocol"]) -> list["MessageParserProtocol"]: 83 """ 84 メッセージ詳細情報取得 85 86 Args: 87 matches (list[MessageParserProtocol]): 対象データ 88 89 Returns: 90 list[MessageParserProtocol]: 詳細情報追加データ 91 92 """ 93 new_matches: list["MessageParserProtocol"] = [] 94 95 # 詳細情報取得 96 for key in matches: 97 conversations = self.api.appclient.conversations_replies(channel=key.data.channel_id, ts=key.data.event_ts) 98 if msg := conversations.get("messages"): 99 res = cast(dict[str, Any], msg[0]) 100 else: 101 continue 102 103 if res: 104 # 各種時間取得 105 key.data.event_ts = str(res.get("ts", "0")) # イベント発生時間 106 key.data.thread_ts = str(res.get("thread_ts", "0")) # スレッドの先頭 107 key.data.edited_ts = str(cast(dict[str, Any], res.get("edited", {})).get("ts", "0")) # 編集時間 108 # リアクション取得 109 key.data.reaction_ok, key.data.reaction_ng = self.get_reactions_list(res) 110 111 new_matches.append(key) 112 113 return new_matches 114 115 def get_conversations(self, m: "MessageParserProtocol") -> dict[str, Any]: 116 """ 117 スレッド情報の取得 118 119 Args: 120 m (MessageParserProtocol): メッセージデータ 121 122 Returns: 123 dict[str, Any]: API response 124 125 """ 126 try: 127 res = self.api.appclient.conversations_replies(channel=m.data.channel_id, ts=m.data.event_ts) 128 logging.trace(res.validate()) # type: ignore 129 return cast(dict[str, Any], res) 130 except self.slack_api_error as err: 131 logging.error("slack_api_error: %s", err) 132 return {} 133 134 def get_reactions_list(self, msg: dict[str, Any]) -> tuple[list[str], list[str]]: 135 """ 136 botが付けたリアクションを取得 137 138 Args: 139 msg (dict[str, Any]): メッセージ内容 140 141 Returns: 142 tuple[list,list]: 143 - reaction_ok: okが付いているメッセージのタイムスタンプ 144 - reaction_ng: ngが付いているメッセージのタイムスタンプ 145 146 """ 147 reaction_ok: list[str] = [] 148 reaction_ng: list[str] = [] 149 150 if msg.get("reactions"): 151 for reactions in msg.get("reactions", {}): 152 if isinstance(reactions, dict) and self.conf.bot_id in reactions.get("users", []): 153 match reactions.get("name"): 154 case self.conf.reaction_ok: 155 reaction_ok.append(msg.get("ts", "")) 156 case self.conf.reaction_ng: 157 reaction_ng.append(msg.get("ts", "")) 158 159 return (reaction_ok, reaction_ng) 160 161 def get_channel_id(self) -> str: 162 """ 163 チャンネルIDを取得する 164 165 Returns: 166 str: チャンネルID 167 168 """ 169 channel_id = "" 170 171 try: 172 response = self.api.webclient.search_messages( 173 query=f"in:{self.conf.search_channel}", 174 count=1, 175 ) 176 messages: dict[str, Any] = response.get("messages", {}) 177 if messages.get("matches"): 178 channel = messages["matches"][0]["channel"] 179 if isinstance(self.conf.search_channel, str): 180 if channel["name"] in self.conf.search_channel: 181 channel_id = channel["id"] 182 else: 183 channel_id = channel["id"] 184 except self.slack_api_error as err: 185 logging.error("slack_api_error: %s", err) 186 187 return channel_id 188 189 def get_dm_channel_id(self, user_id: str) -> str: 190 """ 191 DMのチャンネルIDを取得する 192 193 Args: 194 user_id (str): DMの相手 195 196 Returns: 197 str: チャンネルID 198 199 """ 200 channel_id = "" 201 202 try: 203 response = self.api.appclient.conversations_open(users=[user_id]) 204 channel_id = response["channel"]["id"] 205 except self.slack_api_error as err: 206 logging.error("slack_api_error: %s", err) 207 208 return channel_id 209 210 def reaction_status(self, ch: str, ts: str) -> dict[str, list[str]]: 211 """ 212 botが付けたリアクションの種類を返す 213 214 Args: 215 ch (str): チャンネルID 216 ts (str): メッセージのタイムスタンプ 217 218 Returns: 219 dict[str, list[str]]: リアクション 220 - str: "oK" or "ng" 221 - list[str]: タイムスタンプ 222 223 """ 224 icon: dict[str, list[str]] = { 225 "ok": [], 226 "ng": [], 227 } 228 229 try: # 削除済みメッセージはエラーになるので潰す 230 res = self.api.appclient.reactions_get(channel=ch, timestamp=ts) 231 logging.trace(res.validate()) # type: ignore 232 except self.slack_api_error: 233 return icon 234 235 if reactions := cast(dict[str, Any], res["message"]).get("reactions"): 236 for reaction in cast(list[dict[str, Any]], reactions): 237 if reaction.get("name") == self.conf.reaction_ok and self.conf.bot_id in reaction["users"]: 238 icon["ok"].append(str(res["message"]["ts"])) 239 if reaction.get("name") == self.conf.reaction_ng and self.conf.bot_id in reaction["users"]: 240 icon["ng"].append(str(res["message"]["ts"])) 241 242 logging.debug("ch=%s, ts=%s, icon=%s", ch, ts, icon) 243 return icon 244 245 def reaction_append(self, icon: str, ch: str, ts: str) -> None: 246 """ 247 リアクション追加 248 249 Args: 250 icon (str): リアクション文字 251 ch (str): チャンネルID 252 ts (str): メッセージのタイムスタンプ 253 254 """ 255 if not all([icon, ch, ts]): 256 logging.warning("deficiency: ts=%s, ch=%s, icon=%s", ts, ch, icon) 257 return 258 259 try: 260 res: SlackResponse = self.api.appclient.reactions_add( 261 channel=str(ch), 262 name=icon, 263 timestamp=str(ts), 264 ) 265 logging.debug("ts=%s, ch=%s, icon=%s, %s", ts, ch, icon, res.validate()) # type: ignore[no-untyped-call] 266 except self.slack_api_error as err: 267 match cast(dict[str, Any], err.response).get("error"): 268 case "already_reacted": 269 pass 270 case _: 271 logging.error("slack_api_error: %s", err) 272 logging.error("ts=%s, ch=%s, icon=%s", ts, ch, icon) 273 274 def reaction_remove(self, icon: str, ch: str, ts: str) -> None: 275 """ 276 リアクション削除 277 278 Args: 279 icon (str): リアクション文字 280 ch (str): チャンネルID 281 ts (str): メッセージのタイムスタンプ 282 283 """ 284 if not all([icon, ch, ts]): 285 logging.warning("deficiency: ts=%s, ch=%s, icon=%s", ts, ch, icon) 286 return 287 288 try: 289 res = self.api.appclient.reactions_remove( 290 channel=ch, 291 name=icon, 292 timestamp=ts, 293 ) 294 logging.debug("ch=%s, ts=%s, icon=%s, %s", ch, ts, icon, res.validate()) # type: ignore[no-untyped-call] 295 except self.slack_api_error as err: 296 match cast(dict[str, Any], err.response).get("error"): 297 case "no_reaction": 298 pass 299 case "message_not_found": 300 pass 301 case "channel_not_found": 302 pass 303 case _: 304 logging.error("slack_api_error'(%s): %s", self.slack_api_error, err) 305 logging.error("ch=%s, ts=%s, icon=%s", ch, ts, icon) 306 307 def pickup_score(self) -> list["MessageParserProtocol"]: 308 """ 309 過去ログからスコア記録を検索して返す 310 311 Returns: 312 list[MessageParserProtocol]: 検索した結果 313 314 """ 315 # ゲーム結果の抽出 316 score_matches: list["MessageParserProtocol"] = [] 317 for keyword in g.cfg.rule.keyword_mapping.keys(): 318 for match in self.get_messages(keyword): 319 if validator.check_score(match): 320 if match.ignore_user: # 除外ユーザからのポストは破棄 321 logging.info("skip ignore user: %s", match.data.user_id) 322 continue 323 if match.data.text != match.data.text.replace(keyword, ""): # 検索結果にキーワードが含まれているか 324 score_matches.append(match) 325 logging.debug("found: keyword=%s, %s", keyword, match.data) 326 327 # イベント詳細取得 328 if score_matches: 329 return self.get_message_details(score_matches) 330 return score_matches 331 332 def pickup_remarks(self) -> list["MessageParserProtocol"]: 333 """ 334 slackログからメモを検索して返す 335 336 Returns: 337 list[MessageParserProtocol]: 検索した結果 338 339 """ 340 remarks_matches: list["MessageParserProtocol"] = [] 341 342 # メモの抽出 343 for match in self.get_messages(g.cfg.rule.remarks_words): 344 if match.ignore_user: # 除外ユーザからのポストは破棄 345 logging.info("skip ignore user: %s", match.data.user_id) 346 continue 347 if match.keyword in g.cfg.rule.remarks_words: 348 remarks_matches.append(match) 349 350 # イベント詳細取得 351 if remarks_matches: 352 return self.get_message_details(remarks_matches) 353 return remarks_matches 354 355 def post_processing(self, m: "MessageParserProtocol") -> None: 356 """ 357 後処理 358 359 Args: 360 m (MessageParserProtocol): メッセージデータ 361 362 """ 363 364 # リアクション文字取得 365 def _resolve_reaction(name: str, fallback: str) -> Any: 366 section_name = f"slack_{m.data.channel_id}" 367 if channel_config := g.params.channel_config: 368 if value := lookup.get_config_value( 369 config_file=channel_config, 370 section=section_name, 371 name=name, 372 val_type=str, 373 ): 374 return value 375 376 if value := lookup.get_config_value( 377 config_file=g.cfg.config_file, 378 section=section_name, 379 name=name, 380 val_type=str, 381 ): 382 return value 383 384 return lookup.get_config_value( 385 config_file=g.cfg.config_file, 386 section="slack", 387 name=name, 388 val_type=str, 389 fallback=fallback, 390 ) 391 392 self.conf.reaction_ok = str(_resolve_reaction("reaction_ok", self.conf.reaction_ok)) 393 self.conf.reaction_ng = str(_resolve_reaction("reaction_ng", self.conf.reaction_ng)) 394 395 # リアクション処理 396 match m.status.action: 397 case ActionStatus.NOTHING: 398 return 399 case ActionStatus.CHANGE: 400 for ts in m.status.target_ts: 401 reaction_data = self.reaction_status(ch=m.data.channel_id, ts=ts) 402 if m.status.reaction: # NGを外してOKを付ける 403 if not reaction_data.get("ok"): 404 self.reaction_append(icon=self.conf.reaction_ok, ch=m.data.channel_id, ts=ts) 405 if reaction_data.get("ng"): 406 self.reaction_remove(icon=self.conf.reaction_ng, ch=m.data.channel_id, ts=ts) 407 else: # OKを外してNGを付ける 408 if reaction_data.get("ok"): 409 self.reaction_remove(icon=self.conf.reaction_ok, ch=m.data.channel_id, ts=ts) 410 if not reaction_data.get("ng"): 411 self.reaction_append(icon=self.conf.reaction_ng, ch=m.data.channel_id, ts=ts) 412 case ActionStatus.DELETE: 413 for ts in m.status.target_ts: 414 self.reaction_remove(icon=self.conf.reaction_ok, ch=m.data.channel_id, ts=ts) 415 self.reaction_remove(icon=self.conf.reaction_ng, ch=m.data.channel_id, ts=ts) 416 m.status.reset()
slack専用関数
SvcFunctions( api: integrations.slack.api.AdapterAPI, conf: integrations.slack.config.SvcConfig)
29 def __init__(self, api: "AdapterAPI", conf: "SvcConfig"): 30 super().__init__() 31 32 try: 33 from slack_sdk.errors import SlackApiError 34 35 self.slack_api_error = SlackApiError 36 except ModuleNotFoundError as err: 37 raise ModuleNotFoundError(err.msg) from None 38 39 self.api = api 40 """API操作オブジェクト""" 41 self.conf = conf 42 """個別設定"""
def
get_messages( self, words: str | list[str]) -> list[integrations.protocols.MessageParserProtocol]:
44 def get_messages(self, words: Union[str, list[str]]) -> list["MessageParserProtocol"]: 45 """ 46 slackログからメッセージを検索して返す 47 48 Args: 49 words (Union[str, list[str]]): 検索するワード 50 51 Returns: 52 list[MessageParserProtocol]: 検索した結果 53 54 """ 55 g.adapter = cast("ServiceAdapter", g.adapter) 56 if isinstance(words, list): 57 words = " ".join(words) 58 59 # 検索クエリ 60 after = ExtDt(days=-self.conf.search_after, hours=g.cfg.setting.time_adjust).format(Format.YMD, Delimiter.HYPHEN) 61 channel = " ".join([f"in:{x}" for x in self.conf.search_channel]) 62 query = f"{words} {channel} after:{after}" 63 logging.info("query=%s, check_db=%s", query, g.cfg.setting.database_file) 64 65 # データ取得 66 response = self.api.webclient.search_messages(query=query, sort="timestamp", sort_dir="asc", count=100) 67 matches = response["messages"]["matches"] # 1ページ目 68 for p in range(2, response["messages"]["paging"]["pages"] + 1): 69 response = self.api.webclient.search_messages(query=query, sort="timestamp", sort_dir="asc", count=100, page=p) 70 matches += response["messages"]["matches"] # 2ページ目以降 71 72 # 必要なデータだけ辞書に格納 73 data: list["MessageParserProtocol"] = [] 74 for x in matches: 75 if isinstance(x, dict): 76 work_m = cast("MessageParserProtocol", g.adapter.parser()) 77 work_m.parser(x) 78 data.append(work_m) 79 80 return data
slackログからメッセージを検索して返す
Arguments:
- words (Union[str, list[str]]): 検索するワード
Returns:
list[MessageParserProtocol]: 検索した結果
def
get_message_details( self, matches: list[integrations.protocols.MessageParserProtocol]) -> list[integrations.protocols.MessageParserProtocol]:
82 def get_message_details(self, matches: list["MessageParserProtocol"]) -> list["MessageParserProtocol"]: 83 """ 84 メッセージ詳細情報取得 85 86 Args: 87 matches (list[MessageParserProtocol]): 対象データ 88 89 Returns: 90 list[MessageParserProtocol]: 詳細情報追加データ 91 92 """ 93 new_matches: list["MessageParserProtocol"] = [] 94 95 # 詳細情報取得 96 for key in matches: 97 conversations = self.api.appclient.conversations_replies(channel=key.data.channel_id, ts=key.data.event_ts) 98 if msg := conversations.get("messages"): 99 res = cast(dict[str, Any], msg[0]) 100 else: 101 continue 102 103 if res: 104 # 各種時間取得 105 key.data.event_ts = str(res.get("ts", "0")) # イベント発生時間 106 key.data.thread_ts = str(res.get("thread_ts", "0")) # スレッドの先頭 107 key.data.edited_ts = str(cast(dict[str, Any], res.get("edited", {})).get("ts", "0")) # 編集時間 108 # リアクション取得 109 key.data.reaction_ok, key.data.reaction_ng = self.get_reactions_list(res) 110 111 new_matches.append(key) 112 113 return new_matches
メッセージ詳細情報取得
Arguments:
- matches (list[MessageParserProtocol]): 対象データ
Returns:
list[MessageParserProtocol]: 詳細情報追加データ
def
get_conversations( self, m: integrations.protocols.MessageParserProtocol) -> dict[str, typing.Any]:
115 def get_conversations(self, m: "MessageParserProtocol") -> dict[str, Any]: 116 """ 117 スレッド情報の取得 118 119 Args: 120 m (MessageParserProtocol): メッセージデータ 121 122 Returns: 123 dict[str, Any]: API response 124 125 """ 126 try: 127 res = self.api.appclient.conversations_replies(channel=m.data.channel_id, ts=m.data.event_ts) 128 logging.trace(res.validate()) # type: ignore 129 return cast(dict[str, Any], res) 130 except self.slack_api_error as err: 131 logging.error("slack_api_error: %s", err) 132 return {}
スレッド情報の取得
Arguments:
- m (MessageParserProtocol): メッセージデータ
Returns:
dict[str, Any]: API response
def
get_reactions_list(self, msg: dict[str, typing.Any]) -> tuple[list[str], list[str]]:
134 def get_reactions_list(self, msg: dict[str, Any]) -> tuple[list[str], list[str]]: 135 """ 136 botが付けたリアクションを取得 137 138 Args: 139 msg (dict[str, Any]): メッセージ内容 140 141 Returns: 142 tuple[list,list]: 143 - reaction_ok: okが付いているメッセージのタイムスタンプ 144 - reaction_ng: ngが付いているメッセージのタイムスタンプ 145 146 """ 147 reaction_ok: list[str] = [] 148 reaction_ng: list[str] = [] 149 150 if msg.get("reactions"): 151 for reactions in msg.get("reactions", {}): 152 if isinstance(reactions, dict) and self.conf.bot_id in reactions.get("users", []): 153 match reactions.get("name"): 154 case self.conf.reaction_ok: 155 reaction_ok.append(msg.get("ts", "")) 156 case self.conf.reaction_ng: 157 reaction_ng.append(msg.get("ts", "")) 158 159 return (reaction_ok, reaction_ng)
botが付けたリアクションを取得
Arguments:
- msg (dict[str, Any]): メッセージ内容
Returns:
tuple[list,list]:
- reaction_ok: okが付いているメッセージのタイムスタンプ
- reaction_ng: ngが付いているメッセージのタイムスタンプ
def
get_channel_id(self) -> str:
161 def get_channel_id(self) -> str: 162 """ 163 チャンネルIDを取得する 164 165 Returns: 166 str: チャンネルID 167 168 """ 169 channel_id = "" 170 171 try: 172 response = self.api.webclient.search_messages( 173 query=f"in:{self.conf.search_channel}", 174 count=1, 175 ) 176 messages: dict[str, Any] = response.get("messages", {}) 177 if messages.get("matches"): 178 channel = messages["matches"][0]["channel"] 179 if isinstance(self.conf.search_channel, str): 180 if channel["name"] in self.conf.search_channel: 181 channel_id = channel["id"] 182 else: 183 channel_id = channel["id"] 184 except self.slack_api_error as err: 185 logging.error("slack_api_error: %s", err) 186 187 return channel_id
チャンネルIDを取得する
Returns:
str: チャンネルID
def
get_dm_channel_id(self, user_id: str) -> str:
189 def get_dm_channel_id(self, user_id: str) -> str: 190 """ 191 DMのチャンネルIDを取得する 192 193 Args: 194 user_id (str): DMの相手 195 196 Returns: 197 str: チャンネルID 198 199 """ 200 channel_id = "" 201 202 try: 203 response = self.api.appclient.conversations_open(users=[user_id]) 204 channel_id = response["channel"]["id"] 205 except self.slack_api_error as err: 206 logging.error("slack_api_error: %s", err) 207 208 return channel_id
DMのチャンネルIDを取得する
Arguments:
- user_id (str): DMの相手
Returns:
str: チャンネルID
def
reaction_status(self, ch: str, ts: str) -> dict[str, list[str]]:
210 def reaction_status(self, ch: str, ts: str) -> dict[str, list[str]]: 211 """ 212 botが付けたリアクションの種類を返す 213 214 Args: 215 ch (str): チャンネルID 216 ts (str): メッセージのタイムスタンプ 217 218 Returns: 219 dict[str, list[str]]: リアクション 220 - str: "oK" or "ng" 221 - list[str]: タイムスタンプ 222 223 """ 224 icon: dict[str, list[str]] = { 225 "ok": [], 226 "ng": [], 227 } 228 229 try: # 削除済みメッセージはエラーになるので潰す 230 res = self.api.appclient.reactions_get(channel=ch, timestamp=ts) 231 logging.trace(res.validate()) # type: ignore 232 except self.slack_api_error: 233 return icon 234 235 if reactions := cast(dict[str, Any], res["message"]).get("reactions"): 236 for reaction in cast(list[dict[str, Any]], reactions): 237 if reaction.get("name") == self.conf.reaction_ok and self.conf.bot_id in reaction["users"]: 238 icon["ok"].append(str(res["message"]["ts"])) 239 if reaction.get("name") == self.conf.reaction_ng and self.conf.bot_id in reaction["users"]: 240 icon["ng"].append(str(res["message"]["ts"])) 241 242 logging.debug("ch=%s, ts=%s, icon=%s", ch, ts, icon) 243 return icon
botが付けたリアクションの種類を返す
Arguments:
- ch (str): チャンネルID
- ts (str): メッセージのタイムスタンプ
Returns:
dict[str, list[str]]: リアクション
- str: "oK" or "ng"
- list[str]: タイムスタンプ
def
reaction_append(self, icon: str, ch: str, ts: str) -> None:
245 def reaction_append(self, icon: str, ch: str, ts: str) -> None: 246 """ 247 リアクション追加 248 249 Args: 250 icon (str): リアクション文字 251 ch (str): チャンネルID 252 ts (str): メッセージのタイムスタンプ 253 254 """ 255 if not all([icon, ch, ts]): 256 logging.warning("deficiency: ts=%s, ch=%s, icon=%s", ts, ch, icon) 257 return 258 259 try: 260 res: SlackResponse = self.api.appclient.reactions_add( 261 channel=str(ch), 262 name=icon, 263 timestamp=str(ts), 264 ) 265 logging.debug("ts=%s, ch=%s, icon=%s, %s", ts, ch, icon, res.validate()) # type: ignore[no-untyped-call] 266 except self.slack_api_error as err: 267 match cast(dict[str, Any], err.response).get("error"): 268 case "already_reacted": 269 pass 270 case _: 271 logging.error("slack_api_error: %s", err) 272 logging.error("ts=%s, ch=%s, icon=%s", ts, ch, icon)
リアクション追加
Arguments:
- icon (str): リアクション文字
- ch (str): チャンネルID
- ts (str): メッセージのタイムスタンプ
def
reaction_remove(self, icon: str, ch: str, ts: str) -> None:
274 def reaction_remove(self, icon: str, ch: str, ts: str) -> None: 275 """ 276 リアクション削除 277 278 Args: 279 icon (str): リアクション文字 280 ch (str): チャンネルID 281 ts (str): メッセージのタイムスタンプ 282 283 """ 284 if not all([icon, ch, ts]): 285 logging.warning("deficiency: ts=%s, ch=%s, icon=%s", ts, ch, icon) 286 return 287 288 try: 289 res = self.api.appclient.reactions_remove( 290 channel=ch, 291 name=icon, 292 timestamp=ts, 293 ) 294 logging.debug("ch=%s, ts=%s, icon=%s, %s", ch, ts, icon, res.validate()) # type: ignore[no-untyped-call] 295 except self.slack_api_error as err: 296 match cast(dict[str, Any], err.response).get("error"): 297 case "no_reaction": 298 pass 299 case "message_not_found": 300 pass 301 case "channel_not_found": 302 pass 303 case _: 304 logging.error("slack_api_error'(%s): %s", self.slack_api_error, err) 305 logging.error("ch=%s, ts=%s, icon=%s", ch, ts, icon)
リアクション削除
Arguments:
- icon (str): リアクション文字
- ch (str): チャンネルID
- ts (str): メッセージのタイムスタンプ
307 def pickup_score(self) -> list["MessageParserProtocol"]: 308 """ 309 過去ログからスコア記録を検索して返す 310 311 Returns: 312 list[MessageParserProtocol]: 検索した結果 313 314 """ 315 # ゲーム結果の抽出 316 score_matches: list["MessageParserProtocol"] = [] 317 for keyword in g.cfg.rule.keyword_mapping.keys(): 318 for match in self.get_messages(keyword): 319 if validator.check_score(match): 320 if match.ignore_user: # 除外ユーザからのポストは破棄 321 logging.info("skip ignore user: %s", match.data.user_id) 322 continue 323 if match.data.text != match.data.text.replace(keyword, ""): # 検索結果にキーワードが含まれているか 324 score_matches.append(match) 325 logging.debug("found: keyword=%s, %s", keyword, match.data) 326 327 # イベント詳細取得 328 if score_matches: 329 return self.get_message_details(score_matches) 330 return score_matches
過去ログからスコア記録を検索して返す
Returns:
list[MessageParserProtocol]: 検索した結果
332 def pickup_remarks(self) -> list["MessageParserProtocol"]: 333 """ 334 slackログからメモを検索して返す 335 336 Returns: 337 list[MessageParserProtocol]: 検索した結果 338 339 """ 340 remarks_matches: list["MessageParserProtocol"] = [] 341 342 # メモの抽出 343 for match in self.get_messages(g.cfg.rule.remarks_words): 344 if match.ignore_user: # 除外ユーザからのポストは破棄 345 logging.info("skip ignore user: %s", match.data.user_id) 346 continue 347 if match.keyword in g.cfg.rule.remarks_words: 348 remarks_matches.append(match) 349 350 # イベント詳細取得 351 if remarks_matches: 352 return self.get_message_details(remarks_matches) 353 return remarks_matches
slackログからメモを検索して返す
Returns:
list[MessageParserProtocol]: 検索した結果
355 def post_processing(self, m: "MessageParserProtocol") -> None: 356 """ 357 後処理 358 359 Args: 360 m (MessageParserProtocol): メッセージデータ 361 362 """ 363 364 # リアクション文字取得 365 def _resolve_reaction(name: str, fallback: str) -> Any: 366 section_name = f"slack_{m.data.channel_id}" 367 if channel_config := g.params.channel_config: 368 if value := lookup.get_config_value( 369 config_file=channel_config, 370 section=section_name, 371 name=name, 372 val_type=str, 373 ): 374 return value 375 376 if value := lookup.get_config_value( 377 config_file=g.cfg.config_file, 378 section=section_name, 379 name=name, 380 val_type=str, 381 ): 382 return value 383 384 return lookup.get_config_value( 385 config_file=g.cfg.config_file, 386 section="slack", 387 name=name, 388 val_type=str, 389 fallback=fallback, 390 ) 391 392 self.conf.reaction_ok = str(_resolve_reaction("reaction_ok", self.conf.reaction_ok)) 393 self.conf.reaction_ng = str(_resolve_reaction("reaction_ng", self.conf.reaction_ng)) 394 395 # リアクション処理 396 match m.status.action: 397 case ActionStatus.NOTHING: 398 return 399 case ActionStatus.CHANGE: 400 for ts in m.status.target_ts: 401 reaction_data = self.reaction_status(ch=m.data.channel_id, ts=ts) 402 if m.status.reaction: # NGを外してOKを付ける 403 if not reaction_data.get("ok"): 404 self.reaction_append(icon=self.conf.reaction_ok, ch=m.data.channel_id, ts=ts) 405 if reaction_data.get("ng"): 406 self.reaction_remove(icon=self.conf.reaction_ng, ch=m.data.channel_id, ts=ts) 407 else: # OKを外してNGを付ける 408 if reaction_data.get("ok"): 409 self.reaction_remove(icon=self.conf.reaction_ok, ch=m.data.channel_id, ts=ts) 410 if not reaction_data.get("ng"): 411 self.reaction_append(icon=self.conf.reaction_ng, ch=m.data.channel_id, ts=ts) 412 case ActionStatus.DELETE: 413 for ts in m.status.target_ts: 414 self.reaction_remove(icon=self.conf.reaction_ok, ch=m.data.channel_id, ts=ts) 415 self.reaction_remove(icon=self.conf.reaction_ng, ch=m.data.channel_id, ts=ts) 416 m.status.reset()
後処理
Arguments:
- m (MessageParserProtocol): メッセージデータ