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
def main(m: integrations.protocols.MessageParserProtocol) -> None:
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): メッセージデータ
def check_omission(results: libs.domain.datamodels.ComparisonResults) -> None:
 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): 結果格納データクラス
def check_remarks(results: libs.domain.datamodels.ComparisonResults) -> None:
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): 結果格納データクラス
def check_total_score(results: libs.domain.datamodels.ComparisonResults) -> None:
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): 結果格納データクラス
def check_pending(m: integrations.protocols.MessageParserProtocol) -> bool:
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: チェック開始