integrations.slack.events.comparison
integrations/slack/events/comparison.py
1""" 2integrations/slack/events/comparison.py 3""" 4 5import logging 6from typing import TYPE_CHECKING, cast 7 8import libs.global_value as g 9from libs.domain import modify 10from libs.domain.datamodels import ComparisonResults 11from libs.domain.score import GameResult 12from libs.functions import lookup, search 13from libs.types import ActionStatus, CommandType, StyleOptions 14from libs.utils import formatter, validator 15from libs.utils.timekit import ExtendedDatetime as ExtDt 16 17if TYPE_CHECKING: 18 from integrations.protocols import MessageParserProtocol 19 from integrations.slack.adapter import ServiceAdapter 20 from libs.types import RemarkDict 21 22 23def main(m: "MessageParserProtocol") -> None: 24 """ 25 突合処理 26 27 Args: 28 m (MessageParserProtocol): メッセージデータ 29 30 """ 31 g.adapter = cast("ServiceAdapter", g.adapter) 32 33 if g.cfg.main_parser.has_section(m.status.source): 34 g.adapter.conf.search_channel = lookup.get_config_value( 35 config_file=g.cfg.config_file, 36 section=m.status.source, 37 name="search_channel", 38 val_type=str, 39 fallback=g.adapter.conf.search_channel, 40 ) 41 g.adapter.conf.search_after = lookup.get_config_value( 42 config_file=g.cfg.config_file, 43 section=m.status.source, 44 name="search_after", 45 val_type=int, 46 fallback=g.adapter.conf.search_after, 47 ) 48 49 results = ComparisonResults(search_after=-g.adapter.conf.search_after) 50 51 check_omission(results) 52 check_remarks(results) 53 check_total_score(results) 54 55 m.set_message(results.output("headline"), StyleOptions(title="データ突合", key_title=True)) 56 if results.pending: 57 m.set_message(results.output("pending"), StyleOptions(title="保留", key_title=False)) 58 m.set_message(results.output("mismatch"), StyleOptions(title="不一致", key_title=False)) 59 m.set_message(results.output("missing"), StyleOptions(title="取りこぼし", key_title=False)) 60 m.set_message(results.output("delete"), StyleOptions(title="削除漏れ", key_title=False)) 61 m.set_message(results.output("remark_mod"), StyleOptions(title="メモ更新", key_title=False)) 62 m.set_message(results.output("remark_del"), StyleOptions(title="メモ削除", key_title=False)) 63 if results.invalid_score: 64 m.set_message(results.output("invalid_score"), StyleOptions(title="供託残り", key_title=False)) 65 66 m.post.thread = True 67 m.post.ts = m.data.event_ts 68 m.status.action = ActionStatus.NOTHING 69 m.status.message = results 70 71 72def check_omission(results: ComparisonResults) -> None: 73 """ 74 スコア突合 75 76 Args: 77 results (ComparisonResults): 結果格納データクラス 78 79 """ 80 g.adapter = cast("ServiceAdapter", g.adapter) 81 slack_score: list[GameResult] = [] 82 83 for work_m in set(g.adapter.functions.pickup_score()): 84 if work_m.keyword in g.keyword_dispatcher: # コマンドキーワードはスキップ 85 continue 86 if detection := validator.check_score(work_m): 87 score = GameResult(**detection) 88 for k, v in score.to_dict().items(): # 名前の正規化 89 if str(k).endswith("_name"): 90 score.set(**{k: formatter.name_replace(str(v), not_replace=True)}) 91 # 保留チェック 92 if check_pending(work_m): 93 results.pending.append(score) 94 else: 95 slack_score.append(score) 96 results.score_list.update({work_m.data.event_ts: work_m}) 97 98 if slack_score: 99 first_ts = float(min(x.ts for x in slack_score)) 100 else: 101 first_ts = float(results.after.format(ExtDt.FMT.TS)) 102 103 db_score = search.for_db_score(first_ts) 104 105 # SLACK -> DATABASE 106 ts_list = [x.ts for x in db_score] 107 for score in slack_score: 108 work_m = results.score_list[score.ts] 109 if score.ts in ts_list: 110 target = db_score[ts_list.index(score.ts)] 111 if score != target: # 不一致(更新) 112 results.mismatch.append({"before": target, "after": score}) 113 logging.info("mismatch: %s (%s)", score.ts, ExtDt(float(score.ts)).format(ExtDt.FMT.YMDHMS)) 114 logging.debug(" * slack: %s", score.to_text("detail")) 115 logging.debug(" * db: %s", target.to_text("detail")) 116 modify.db_update(score, work_m) 117 else: # 取りこぼし(追加) 118 results.missing.append(score) 119 logging.info("missing: %s (%s)", score.ts, ExtDt(float(score.ts)).format(ExtDt.FMT.YMDHMS)) 120 logging.debug(score.to_text("logging")) 121 modify.db_insert(score, work_m) 122 123 # DATABASE -> SLACK 124 ts_list = [x.ts for x in slack_score] 125 work_m = g.adapter.parser() 126 for score in db_score: 127 if score.ts not in ts_list: # 削除漏れ 128 work_m.data.event_ts = score.ts 129 if score.source: 130 work_m.data.channel_id = score.source.replace("slack_", "") 131 results.delete.append(score) 132 logging.info("delete (Only database): %s %s", ExtDt(float(score.ts)).format(ExtDt.FMT.YMDHMS), score.to_text("logging")) 133 work_m.status.command_type = CommandType.COMPARISON 134 modify.db_delete(work_m) 135 136 137def check_remarks(results: ComparisonResults) -> None: 138 """ 139 メモ突合 140 141 Args: 142 results (ComparisonResults): 結果格納データクラス 143 144 """ 145 g.adapter = cast("ServiceAdapter", g.adapter) 146 slack_remarks: list["RemarkDict"] = [] 147 score_list: dict[str, GameResult] = {} 148 149 for loop_m in results.score_list.values(): 150 if detection := validator.check_score(loop_m): 151 score = GameResult(**detection) 152 for k, v in score.to_dict().items(): # 名前の正規化 153 if str(k).endswith("_name"): 154 score.set(**{k: formatter.name_replace(str(v), not_replace=True)}) 155 score_list.update({loop_m.data.event_ts: score}) 156 157 for loop_m in g.adapter.functions.pickup_remarks(): 158 for name, matter in zip(loop_m.argument[0::2], loop_m.argument[1::2]): 159 # 対象外のメモはスキップ 160 if not float(loop_m.data.thread_ts): 161 continue # リプライになっていない 162 if loop_m.data.thread_ts not in score_list: 163 continue # ゲーム結果に紐付かない 164 pname = formatter.name_replace(str(name), not_replace=True) 165 if pname not in score_list[loop_m.data.thread_ts].to_list("name"): 166 continue # ゲーム結果に名前がない 167 if loop_m.data.thread_ts in [x.ts for x in results.pending]: 168 continue # 紐付くゲーム結果が保留中 169 slack_remarks.append( 170 { 171 "thread_ts": loop_m.data.thread_ts, 172 "event_ts": loop_m.data.event_ts, 173 "name": pname, 174 "matter": matter, 175 "source": loop_m.status.source, 176 } 177 ) 178 179 db_remarks = search.for_db_remarks(float(results.after.format(ExtDt.FMT.TS))) 180 181 # SLACK -> DATABASE 182 work_m = cast("MessageParserProtocol", g.adapter.parser()) 183 184 for remark in slack_remarks: 185 if remark in db_remarks: 186 continue # 変化なし 187 if remark["thread_ts"] in [x.ts for x in results.pending]: 188 continue # 紐付くゲーム結果が保留中 189 results.remark_mod.append(remark) 190 191 if results.remark_mod: 192 work_m.reset() 193 for remark in results.remark_mod: 194 work_m.data.event_ts = remark["event_ts"] 195 work_m.data.channel_id = remark["source"].replace("slack_", "") 196 work_m.status.command_type = CommandType.COMPARISON 197 modify.remarks_delete(work_m) 198 modify.remarks_append(work_m, results.remark_mod) 199 200 # DATABASE -> SLACK 201 work_remarks = [{k: str(v) for k, v in d.items() if k != "source"} for d in slack_remarks] # sourceを除外したリスト 202 for remark in db_remarks: 203 check_remark = {k: str(v) for k, v in remark.items() if k != "source"} 204 if check_remark not in work_remarks: # slackに記録なし 205 results.remark_del.append(remark) 206 modify.remarks_delete_compar(work_m, remark) 207 208 209def check_total_score(results: ComparisonResults) -> None: 210 """ 211 素点合計の再チェック 212 213 Args: 214 results (ComparisonResults): 結果格納データクラス 215 216 """ 217 for loop_m in results.score_list.values(): 218 if detection := validator.check_score(loop_m): 219 score = GameResult(**detection) 220 for k, v in score.to_dict().items(): # 名前の正規化 221 if str(k).endswith("_name"): 222 score.set(**{k: formatter.name_replace(str(v), not_replace=True)}) 223 if score.deposit: 224 results.invalid_score.append(score) 225 226 227def check_pending(m: "MessageParserProtocol") -> bool: 228 """ 229 保留チェック 230 231 Args: 232 m (MessageParserProtocol): メッセージデータ 233 234 Returns: 235 bool: 真偽 236 - *True*: 保留中 237 - *False*: チェック開始 238 239 """ 240 g.adapter = cast("ServiceAdapter", g.adapter) 241 242 now_ts = float(ExtDt().format(ExtDt.FMT.TS)) 243 244 if m.data.edited_ts == "undetermined": 245 check_ts = float(m.data.event_ts) + g.adapter.conf.search_wait 246 else: 247 check_ts = float(m.data.edited_ts) + g.adapter.conf.search_wait 248 249 if check_ts > now_ts: 250 return True 251 return False
24def main(m: "MessageParserProtocol") -> None: 25 """ 26 突合処理 27 28 Args: 29 m (MessageParserProtocol): メッセージデータ 30 31 """ 32 g.adapter = cast("ServiceAdapter", g.adapter) 33 34 if g.cfg.main_parser.has_section(m.status.source): 35 g.adapter.conf.search_channel = lookup.get_config_value( 36 config_file=g.cfg.config_file, 37 section=m.status.source, 38 name="search_channel", 39 val_type=str, 40 fallback=g.adapter.conf.search_channel, 41 ) 42 g.adapter.conf.search_after = lookup.get_config_value( 43 config_file=g.cfg.config_file, 44 section=m.status.source, 45 name="search_after", 46 val_type=int, 47 fallback=g.adapter.conf.search_after, 48 ) 49 50 results = ComparisonResults(search_after=-g.adapter.conf.search_after) 51 52 check_omission(results) 53 check_remarks(results) 54 check_total_score(results) 55 56 m.set_message(results.output("headline"), StyleOptions(title="データ突合", key_title=True)) 57 if results.pending: 58 m.set_message(results.output("pending"), StyleOptions(title="保留", key_title=False)) 59 m.set_message(results.output("mismatch"), StyleOptions(title="不一致", key_title=False)) 60 m.set_message(results.output("missing"), StyleOptions(title="取りこぼし", key_title=False)) 61 m.set_message(results.output("delete"), StyleOptions(title="削除漏れ", key_title=False)) 62 m.set_message(results.output("remark_mod"), StyleOptions(title="メモ更新", key_title=False)) 63 m.set_message(results.output("remark_del"), StyleOptions(title="メモ削除", key_title=False)) 64 if results.invalid_score: 65 m.set_message(results.output("invalid_score"), StyleOptions(title="供託残り", key_title=False)) 66 67 m.post.thread = True 68 m.post.ts = m.data.event_ts 69 m.status.action = ActionStatus.NOTHING 70 m.status.message = results
突合処理
Arguments:
- m (MessageParserProtocol): メッセージデータ
73def check_omission(results: ComparisonResults) -> None: 74 """ 75 スコア突合 76 77 Args: 78 results (ComparisonResults): 結果格納データクラス 79 80 """ 81 g.adapter = cast("ServiceAdapter", g.adapter) 82 slack_score: list[GameResult] = [] 83 84 for work_m in set(g.adapter.functions.pickup_score()): 85 if work_m.keyword in g.keyword_dispatcher: # コマンドキーワードはスキップ 86 continue 87 if detection := validator.check_score(work_m): 88 score = GameResult(**detection) 89 for k, v in score.to_dict().items(): # 名前の正規化 90 if str(k).endswith("_name"): 91 score.set(**{k: formatter.name_replace(str(v), not_replace=True)}) 92 # 保留チェック 93 if check_pending(work_m): 94 results.pending.append(score) 95 else: 96 slack_score.append(score) 97 results.score_list.update({work_m.data.event_ts: work_m}) 98 99 if slack_score: 100 first_ts = float(min(x.ts for x in slack_score)) 101 else: 102 first_ts = float(results.after.format(ExtDt.FMT.TS)) 103 104 db_score = search.for_db_score(first_ts) 105 106 # SLACK -> DATABASE 107 ts_list = [x.ts for x in db_score] 108 for score in slack_score: 109 work_m = results.score_list[score.ts] 110 if score.ts in ts_list: 111 target = db_score[ts_list.index(score.ts)] 112 if score != target: # 不一致(更新) 113 results.mismatch.append({"before": target, "after": score}) 114 logging.info("mismatch: %s (%s)", score.ts, ExtDt(float(score.ts)).format(ExtDt.FMT.YMDHMS)) 115 logging.debug(" * slack: %s", score.to_text("detail")) 116 logging.debug(" * db: %s", target.to_text("detail")) 117 modify.db_update(score, work_m) 118 else: # 取りこぼし(追加) 119 results.missing.append(score) 120 logging.info("missing: %s (%s)", score.ts, ExtDt(float(score.ts)).format(ExtDt.FMT.YMDHMS)) 121 logging.debug(score.to_text("logging")) 122 modify.db_insert(score, work_m) 123 124 # DATABASE -> SLACK 125 ts_list = [x.ts for x in slack_score] 126 work_m = g.adapter.parser() 127 for score in db_score: 128 if score.ts not in ts_list: # 削除漏れ 129 work_m.data.event_ts = score.ts 130 if score.source: 131 work_m.data.channel_id = score.source.replace("slack_", "") 132 results.delete.append(score) 133 logging.info("delete (Only database): %s %s", ExtDt(float(score.ts)).format(ExtDt.FMT.YMDHMS), score.to_text("logging")) 134 work_m.status.command_type = CommandType.COMPARISON 135 modify.db_delete(work_m)
スコア突合
Arguments:
- results (ComparisonResults): 結果格納データクラス
138def check_remarks(results: ComparisonResults) -> None: 139 """ 140 メモ突合 141 142 Args: 143 results (ComparisonResults): 結果格納データクラス 144 145 """ 146 g.adapter = cast("ServiceAdapter", g.adapter) 147 slack_remarks: list["RemarkDict"] = [] 148 score_list: dict[str, GameResult] = {} 149 150 for loop_m in results.score_list.values(): 151 if detection := validator.check_score(loop_m): 152 score = GameResult(**detection) 153 for k, v in score.to_dict().items(): # 名前の正規化 154 if str(k).endswith("_name"): 155 score.set(**{k: formatter.name_replace(str(v), not_replace=True)}) 156 score_list.update({loop_m.data.event_ts: score}) 157 158 for loop_m in g.adapter.functions.pickup_remarks(): 159 for name, matter in zip(loop_m.argument[0::2], loop_m.argument[1::2]): 160 # 対象外のメモはスキップ 161 if not float(loop_m.data.thread_ts): 162 continue # リプライになっていない 163 if loop_m.data.thread_ts not in score_list: 164 continue # ゲーム結果に紐付かない 165 pname = formatter.name_replace(str(name), not_replace=True) 166 if pname not in score_list[loop_m.data.thread_ts].to_list("name"): 167 continue # ゲーム結果に名前がない 168 if loop_m.data.thread_ts in [x.ts for x in results.pending]: 169 continue # 紐付くゲーム結果が保留中 170 slack_remarks.append( 171 { 172 "thread_ts": loop_m.data.thread_ts, 173 "event_ts": loop_m.data.event_ts, 174 "name": pname, 175 "matter": matter, 176 "source": loop_m.status.source, 177 } 178 ) 179 180 db_remarks = search.for_db_remarks(float(results.after.format(ExtDt.FMT.TS))) 181 182 # SLACK -> DATABASE 183 work_m = cast("MessageParserProtocol", g.adapter.parser()) 184 185 for remark in slack_remarks: 186 if remark in db_remarks: 187 continue # 変化なし 188 if remark["thread_ts"] in [x.ts for x in results.pending]: 189 continue # 紐付くゲーム結果が保留中 190 results.remark_mod.append(remark) 191 192 if results.remark_mod: 193 work_m.reset() 194 for remark in results.remark_mod: 195 work_m.data.event_ts = remark["event_ts"] 196 work_m.data.channel_id = remark["source"].replace("slack_", "") 197 work_m.status.command_type = CommandType.COMPARISON 198 modify.remarks_delete(work_m) 199 modify.remarks_append(work_m, results.remark_mod) 200 201 # DATABASE -> SLACK 202 work_remarks = [{k: str(v) for k, v in d.items() if k != "source"} for d in slack_remarks] # sourceを除外したリスト 203 for remark in db_remarks: 204 check_remark = {k: str(v) for k, v in remark.items() if k != "source"} 205 if check_remark not in work_remarks: # slackに記録なし 206 results.remark_del.append(remark) 207 modify.remarks_delete_compar(work_m, remark)
メモ突合
Arguments:
- results (ComparisonResults): 結果格納データクラス
210def check_total_score(results: ComparisonResults) -> None: 211 """ 212 素点合計の再チェック 213 214 Args: 215 results (ComparisonResults): 結果格納データクラス 216 217 """ 218 for loop_m in results.score_list.values(): 219 if detection := validator.check_score(loop_m): 220 score = GameResult(**detection) 221 for k, v in score.to_dict().items(): # 名前の正規化 222 if str(k).endswith("_name"): 223 score.set(**{k: formatter.name_replace(str(v), not_replace=True)}) 224 if score.deposit: 225 results.invalid_score.append(score)
素点合計の再チェック
Arguments:
- results (ComparisonResults): 結果格納データクラス
228def check_pending(m: "MessageParserProtocol") -> bool: 229 """ 230 保留チェック 231 232 Args: 233 m (MessageParserProtocol): メッセージデータ 234 235 Returns: 236 bool: 真偽 237 - *True*: 保留中 238 - *False*: チェック開始 239 240 """ 241 g.adapter = cast("ServiceAdapter", g.adapter) 242 243 now_ts = float(ExtDt().format(ExtDt.FMT.TS)) 244 245 if m.data.edited_ts == "undetermined": 246 check_ts = float(m.data.event_ts) + g.adapter.conf.search_wait 247 else: 248 check_ts = float(m.data.edited_ts) + g.adapter.conf.search_wait 249 250 if check_ts > now_ts: 251 return True 252 return False
保留チェック
Arguments:
- m (MessageParserProtocol): メッセージデータ
Returns:
bool: 真偽
- True: 保留中
- False: チェック開始