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