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)
def main(m: integrations.protocols.MessageParserProtocol) -> None:
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]): 検索結果