libs.domain.modify

libs/domain/modify.py

  1"""
  2libs/domain/modify.py
  3"""
  4
  5import logging
  6import os
  7import re
  8import shutil
  9import sqlite3
 10from contextlib import closing
 11from typing import TYPE_CHECKING, Any, cast
 12
 13import libs.global_value as g
 14from libs.functions import lookup, message
 15from libs.types import ActionStatus, MessageStatus, StyleOptions
 16from libs.utils import dbutil, formatter
 17from libs.utils.timekit import ExtendedDatetime as ExtDt
 18
 19if TYPE_CHECKING:
 20    from integrations.protocols import MessageParserProtocol
 21    from libs.domain.score import GameResult
 22    from libs.types import RemarkDict
 23
 24
 25def db_insert(detection: "GameResult", m: "MessageParserProtocol") -> int:
 26    """
 27    スコアデータをDBに追加する
 28
 29    Args:
 30        detection (GameResult): スコアデータ
 31        m (MessageParserProtocol): メッセージデータ
 32
 33    Returns:
 34        int: DB更新レコード数
 35
 36    """
 37    changes: int = 0
 38
 39    if m.check_updatable:
 40        with closing(dbutil.connection(g.cfg.setting.database_file)) as cur:
 41            try:
 42                cur.execute(
 43                    dbutil.query("RESULT_INSERT"),
 44                    {
 45                        "playtime": ExtDt(float(detection.ts)).format(ExtDt.FMT.SQL),
 46                        "rpoint_sum": detection.rpoint_sum,
 47                        **detection.to_dict(),
 48                    },
 49                )
 50                changes = cur.total_changes
 51                cur.commit()
 52            except sqlite3.IntegrityError as err:
 53                logging.error("IntegrityError: %s", err)
 54        logging.info("%s", detection.to_text("logging"))
 55        _score_check(detection, m)
 56    else:
 57        logging.warning("A post to a restricted channel was detected.")
 58        m.set_message(message.random_reply(m, "restricted_channel"), StyleOptions(key_title=False))
 59        m.post.ts = m.data.event_ts
 60
 61    g.adapter.functions.post_processing(m)
 62    return changes
 63
 64
 65def db_update(detection: "GameResult", m: "MessageParserProtocol") -> int:
 66    """
 67    スコアデータを変更する
 68
 69    Args:
 70        detection (GameResult): スコアデータ
 71        m (MessageParserProtocol): メッセージデータ
 72
 73    Returns:
 74        int: DB更新レコード数
 75
 76    """
 77    detection.calc()
 78    changes: int = int(0)
 79
 80    if m.check_updatable:
 81        with closing(dbutil.connection(g.cfg.setting.database_file)) as cur:
 82            cur.execute(
 83                dbutil.query("RESULT_UPDATE"),
 84                {
 85                    "playtime": ExtDt(float(detection.ts)).format(ExtDt.FMT.SQL),
 86                    "rpoint_sum": detection.rpoint_sum,
 87                    **detection.to_dict(),
 88                },
 89            )
 90            changes = cur.total_changes
 91            cur.commit()
 92        logging.info("%s", detection.to_text("logging"))
 93        _score_check(detection, m)
 94    else:
 95        logging.warning("A post to a restricted channel was detected.")
 96        m.set_message(message.random_reply(m, "restricted_channel"), StyleOptions(key_title=False))
 97        m.post.ts = m.data.event_ts
 98
 99    g.adapter.functions.post_processing(m)
100    return changes
101
102
103def db_delete(m: "MessageParserProtocol") -> None:
104    """
105    スコアデータを削除する
106
107    Args:
108        m (MessageParserProtocol): メッセージデータ
109
110    """
111    if m.check_updatable:
112        with closing(dbutil.connection(g.cfg.setting.database_file)) as cur:
113            # ゲーム結果の削除
114            cur.execute(dbutil.query("RESULT_DELETE"), (m.data.event_ts,))
115            if delete_result := cur.execute("select changes();").fetchone()[0]:
116                m.status.target_ts.append(m.data.event_ts)
117                logging.info("result: ts=%s, count=%s", m.data.event_ts, delete_result)
118            # メモの削除
119            if remark_list := cur.execute("select event_ts from remarks where thread_ts=?", (m.data.event_ts,)).fetchall():
120                cur.execute(dbutil.query("REMARKS_DELETE_ALL"), (m.data.event_ts,))
121                if delete_remark := cur.execute("select changes();").fetchone()[0]:
122                    m.status.target_ts.extend([str(x.get("event_ts")) for x in list(map(dict, remark_list))])
123                    logging.info("remark: ts=%s, count=%s", m.data.event_ts, delete_remark)
124            cur.commit()
125        m.status.action = ActionStatus.DELETE
126        g.adapter.functions.post_processing(m)
127    else:
128        logging.warning("A post to a restricted channel was detected.")
129
130
131def db_backup() -> str:
132    """
133    データベースのバックアップ
134
135    Returns:
136        str: 動作結果メッセージ
137
138    """
139    if not g.cfg.setting.backup_dir:  # バックアップ設定がされていない場合は何もしない
140        return ""
141
142    fname = os.path.splitext(g.cfg.setting.database_file)[0]
143    fext = os.path.splitext(g.cfg.setting.database_file)[1]
144    bktime = ExtDt().format(ExtDt.FMT.EXT)
145    bkfname = os.path.join(g.cfg.setting.backup_dir, os.path.basename(f"{fname}_{bktime}{fext}"))
146
147    if not os.path.isdir(g.cfg.setting.backup_dir):  # バックアップディレクトリ作成
148        try:
149            os.mkdir(g.cfg.setting.backup_dir)
150        except OSError as err:
151            logging.warning(err, exc_info=True)
152            logging.warning("Database backup directory creation failed !!!")
153            return "\nバックアップ用ディレクトリ作成の作成に失敗しました。"
154
155    # バックアップディレクトリにコピー
156    try:
157        shutil.copyfile(g.cfg.setting.database_file, bkfname)
158        logging.info("database backup: %s", bkfname)
159        return "\nデータベースをバックアップしました。"
160    except OSError as err:
161        logging.warning(err, exc_info=True)
162        logging.warning("Database backup failed !!!")
163        return "\nデータベースのバックアップに失敗しました。"
164
165
166def remarks_append(m: "MessageParserProtocol", remarks: list["RemarkDict"]) -> None:
167    """
168    メモをDBに記録する
169
170    Args:
171        m (MessageParserProtocol): メッセージデータ
172        remarks (list[RemarkDict]): メモに残す内容
173
174    """
175    if m.check_updatable:
176        with closing(dbutil.connection(g.cfg.setting.database_file)) as cur:
177            for para in remarks:
178                # 親スレッドの情報
179                row = cur.execute("select * from result where ts=:thread_ts", para).fetchone()
180                if row:
181                    if para["name"] in [v for k, v in dict(row).items() if str(k).endswith("_name")]:
182                        cur.execute(dbutil.query("REMARKS_INSERT"), para)
183                        m.status.target_ts.append(para["event_ts"])
184                        if not m.data.channel_id:
185                            m.data.channel_id = para["source"].replace(f"{g.adapter.interface_type}_", "")
186                        logging.info("insert: %s", para)
187            cur.commit()
188            count = cur.execute("select changes();").fetchone()[0]
189
190        # 後処理
191        if count:
192            m.status.action = ActionStatus.CHANGE
193            m.status.reaction = True
194        else:
195            m.status.action = ActionStatus.NOTHING
196
197        g.adapter.functions.post_processing(m)
198
199
200def remarks_delete(m: "MessageParserProtocol") -> None:
201    """
202    DBからメモを削除する
203
204    Args:
205        m (MessageParserProtocol): メッセージデータ
206
207    """
208    if m.check_updatable:
209        with closing(dbutil.connection(g.cfg.setting.database_file)) as cur:
210            cur.execute(dbutil.query("REMARKS_DELETE_ONE"), (m.data.event_ts,))
211            cur.commit()
212            if count := cur.execute("select changes();").fetchone()[0]:
213                m.status.target_ts.append(m.data.event_ts)
214                logging.info("ts=%s, count=%s", m.data.event_ts, count)
215        # 後処理
216        m.status.action = ActionStatus.DELETE
217        g.adapter.functions.post_processing(m)
218
219
220def remarks_delete_compar(m: "MessageParserProtocol", para: "RemarkDict") -> None:
221    """
222    DBからメモを削除する(突合)
223
224    Args:
225        m (MessageParserProtocol): メッセージデータ
226        para (RemarkDict): パラメータ
227
228    """
229    with closing(dbutil.connection(g.cfg.setting.database_file)) as cur:
230        cur.execute(dbutil.query("REMARKS_DELETE_COMPAR"), para)
231        cur.commit()
232
233        left = cur.execute("select count() from remarks where event_ts=:event_ts;", para).fetchone()[0]
234        logging.info("ts=%s, count=%s", m.data.event_ts, left)
235
236    # 後処理
237    m.status.action = ActionStatus.DELETE
238    m.status.target_ts.append(para["event_ts"])
239    m.data.channel_id = para["source"].replace(f"{g.adapter.interface_type}_", "")
240    if left > 0:  # メモデータが残っているなら
241        m.status.action = ActionStatus.CHANGE
242    g.adapter.functions.post_processing(m)
243
244
245def check_remarks(m: "MessageParserProtocol") -> None:
246    """
247    メモの内容を拾ってDBに格納する
248
249    Args:
250        m (MessageParserProtocol): メッセージデータ
251
252    """
253    game_result = lookup.exsist_record(m.data.thread_ts)
254    if game_result.has_valid_data():  # ゲーム結果のスレッドになっているか
255        remarks: list["RemarkDict"] = []
256        for name, matter in zip(m.argument[0::2], m.argument[1::2]):
257            remark: "RemarkDict" = {
258                "thread_ts": m.data.thread_ts,
259                "event_ts": m.data.event_ts,
260                "name": formatter.name_replace(name, not_replace=True),
261                "matter": matter,
262                "source": m.status.source,
263            }
264            if remark["name"] in game_result.to_list() and remark not in remarks:
265                remarks.append(remark)
266
267        match m.data.status:
268            case MessageStatus.APPEND:
269                remarks_append(m, remarks)
270            case MessageStatus.CHANGED:
271                remarks_delete(m)
272                remarks_append(m, remarks)
273            case MessageStatus.DELETED:
274                remarks_delete(m)
275            case _:
276                pass
277
278
279def reprocessing_remarks(m: "MessageParserProtocol") -> None:
280    """
281    スレッドの内容を再処理
282
283    Args:
284        m (MessageParserProtocol): メッセージデータ
285
286    """
287    res = g.adapter.functions.get_conversations(m)
288    msg = res.get("messages")
289
290    if msg:
291        reply_count = int(cast(dict[str, Any], msg[0]).get("reply_count", 0))
292        m.data.thread_ts = str(cast(dict[str, Any], msg[0]).get("ts"))
293
294        for x in range(1, reply_count + 1):
295            m.data.text = str(cast(dict[str, str], msg[x]).get("text", ""))
296            if m.data.text:
297                m.data.event_ts = str(cast(dict[str, Any], msg[x]).get("ts"))
298                logging.debug(
299                    "(%s/%s) thread_ts=%s, event_ts=%s, %s",
300                    x,
301                    reply_count,
302                    m.data.thread_ts,
303                    m.data.event_ts,
304                    m.data.text,
305                )
306                if re.match(rf"^{g.cfg.setting.remarks_word}", m.keyword):
307                    check_remarks(m)
308
309
310def _score_check(detection: "GameResult", m: "MessageParserProtocol") -> None:
311    """
312    スコアデータ格納状態を記録する
313
314    Args:
315        detection (GameResult): スコアデータ
316        m (MessageParserProtocol): メッセージデータ
317
318    """
319    # 結果
320    m.status.action = ActionStatus.CHANGE
321    m.status.target_ts.append(m.data.event_ts)
322    m.status.reaction = True
323    m.status.message = detection.to_text("detail")
324    m.post.message.clear()
325
326    # 素点合計チェック
327    if detection.deposit:
328        m.status.reaction = False
329        m.status.rpoint_sum = detection.rpoint_sum
330        m.set_message(message.random_reply(m, "invalid_score"), StyleOptions(key_title=False))
331        m.post.ts = m.data.event_ts
332
333    # プレイヤー名重複チェック
334    if len(set(detection.to_list())) != detection.mode:
335        m.status.reaction = False
336        m.set_message(message.random_reply(m, "same_player"), StyleOptions(key_title=False))
337        m.post.ts = m.data.event_ts
def db_insert( detection: libs.domain.score.GameResult, m: integrations.protocols.MessageParserProtocol) -> int:
26def db_insert(detection: "GameResult", m: "MessageParserProtocol") -> int:
27    """
28    スコアデータをDBに追加する
29
30    Args:
31        detection (GameResult): スコアデータ
32        m (MessageParserProtocol): メッセージデータ
33
34    Returns:
35        int: DB更新レコード数
36
37    """
38    changes: int = 0
39
40    if m.check_updatable:
41        with closing(dbutil.connection(g.cfg.setting.database_file)) as cur:
42            try:
43                cur.execute(
44                    dbutil.query("RESULT_INSERT"),
45                    {
46                        "playtime": ExtDt(float(detection.ts)).format(ExtDt.FMT.SQL),
47                        "rpoint_sum": detection.rpoint_sum,
48                        **detection.to_dict(),
49                    },
50                )
51                changes = cur.total_changes
52                cur.commit()
53            except sqlite3.IntegrityError as err:
54                logging.error("IntegrityError: %s", err)
55        logging.info("%s", detection.to_text("logging"))
56        _score_check(detection, m)
57    else:
58        logging.warning("A post to a restricted channel was detected.")
59        m.set_message(message.random_reply(m, "restricted_channel"), StyleOptions(key_title=False))
60        m.post.ts = m.data.event_ts
61
62    g.adapter.functions.post_processing(m)
63    return changes

スコアデータをDBに追加する

Arguments:
  • detection (GameResult): スコアデータ
  • m (MessageParserProtocol): メッセージデータ
Returns:

int: DB更新レコード数

def db_update( detection: libs.domain.score.GameResult, m: integrations.protocols.MessageParserProtocol) -> int:
 66def db_update(detection: "GameResult", m: "MessageParserProtocol") -> int:
 67    """
 68    スコアデータを変更する
 69
 70    Args:
 71        detection (GameResult): スコアデータ
 72        m (MessageParserProtocol): メッセージデータ
 73
 74    Returns:
 75        int: DB更新レコード数
 76
 77    """
 78    detection.calc()
 79    changes: int = int(0)
 80
 81    if m.check_updatable:
 82        with closing(dbutil.connection(g.cfg.setting.database_file)) as cur:
 83            cur.execute(
 84                dbutil.query("RESULT_UPDATE"),
 85                {
 86                    "playtime": ExtDt(float(detection.ts)).format(ExtDt.FMT.SQL),
 87                    "rpoint_sum": detection.rpoint_sum,
 88                    **detection.to_dict(),
 89                },
 90            )
 91            changes = cur.total_changes
 92            cur.commit()
 93        logging.info("%s", detection.to_text("logging"))
 94        _score_check(detection, m)
 95    else:
 96        logging.warning("A post to a restricted channel was detected.")
 97        m.set_message(message.random_reply(m, "restricted_channel"), StyleOptions(key_title=False))
 98        m.post.ts = m.data.event_ts
 99
100    g.adapter.functions.post_processing(m)
101    return changes

スコアデータを変更する

Arguments:
  • detection (GameResult): スコアデータ
  • m (MessageParserProtocol): メッセージデータ
Returns:

int: DB更新レコード数

def db_delete(m: integrations.protocols.MessageParserProtocol) -> None:
104def db_delete(m: "MessageParserProtocol") -> None:
105    """
106    スコアデータを削除する
107
108    Args:
109        m (MessageParserProtocol): メッセージデータ
110
111    """
112    if m.check_updatable:
113        with closing(dbutil.connection(g.cfg.setting.database_file)) as cur:
114            # ゲーム結果の削除
115            cur.execute(dbutil.query("RESULT_DELETE"), (m.data.event_ts,))
116            if delete_result := cur.execute("select changes();").fetchone()[0]:
117                m.status.target_ts.append(m.data.event_ts)
118                logging.info("result: ts=%s, count=%s", m.data.event_ts, delete_result)
119            # メモの削除
120            if remark_list := cur.execute("select event_ts from remarks where thread_ts=?", (m.data.event_ts,)).fetchall():
121                cur.execute(dbutil.query("REMARKS_DELETE_ALL"), (m.data.event_ts,))
122                if delete_remark := cur.execute("select changes();").fetchone()[0]:
123                    m.status.target_ts.extend([str(x.get("event_ts")) for x in list(map(dict, remark_list))])
124                    logging.info("remark: ts=%s, count=%s", m.data.event_ts, delete_remark)
125            cur.commit()
126        m.status.action = ActionStatus.DELETE
127        g.adapter.functions.post_processing(m)
128    else:
129        logging.warning("A post to a restricted channel was detected.")

スコアデータを削除する

Arguments:
  • m (MessageParserProtocol): メッセージデータ
def db_backup() -> str:
132def db_backup() -> str:
133    """
134    データベースのバックアップ
135
136    Returns:
137        str: 動作結果メッセージ
138
139    """
140    if not g.cfg.setting.backup_dir:  # バックアップ設定がされていない場合は何もしない
141        return ""
142
143    fname = os.path.splitext(g.cfg.setting.database_file)[0]
144    fext = os.path.splitext(g.cfg.setting.database_file)[1]
145    bktime = ExtDt().format(ExtDt.FMT.EXT)
146    bkfname = os.path.join(g.cfg.setting.backup_dir, os.path.basename(f"{fname}_{bktime}{fext}"))
147
148    if not os.path.isdir(g.cfg.setting.backup_dir):  # バックアップディレクトリ作成
149        try:
150            os.mkdir(g.cfg.setting.backup_dir)
151        except OSError as err:
152            logging.warning(err, exc_info=True)
153            logging.warning("Database backup directory creation failed !!!")
154            return "\nバックアップ用ディレクトリ作成の作成に失敗しました。"
155
156    # バックアップディレクトリにコピー
157    try:
158        shutil.copyfile(g.cfg.setting.database_file, bkfname)
159        logging.info("database backup: %s", bkfname)
160        return "\nデータベースをバックアップしました。"
161    except OSError as err:
162        logging.warning(err, exc_info=True)
163        logging.warning("Database backup failed !!!")
164        return "\nデータベースのバックアップに失敗しました。"

データベースのバックアップ

Returns:

str: 動作結果メッセージ

def remarks_append( m: integrations.protocols.MessageParserProtocol, remarks: list[libs.types.RemarkDict]) -> None:
167def remarks_append(m: "MessageParserProtocol", remarks: list["RemarkDict"]) -> None:
168    """
169    メモをDBに記録する
170
171    Args:
172        m (MessageParserProtocol): メッセージデータ
173        remarks (list[RemarkDict]): メモに残す内容
174
175    """
176    if m.check_updatable:
177        with closing(dbutil.connection(g.cfg.setting.database_file)) as cur:
178            for para in remarks:
179                # 親スレッドの情報
180                row = cur.execute("select * from result where ts=:thread_ts", para).fetchone()
181                if row:
182                    if para["name"] in [v for k, v in dict(row).items() if str(k).endswith("_name")]:
183                        cur.execute(dbutil.query("REMARKS_INSERT"), para)
184                        m.status.target_ts.append(para["event_ts"])
185                        if not m.data.channel_id:
186                            m.data.channel_id = para["source"].replace(f"{g.adapter.interface_type}_", "")
187                        logging.info("insert: %s", para)
188            cur.commit()
189            count = cur.execute("select changes();").fetchone()[0]
190
191        # 後処理
192        if count:
193            m.status.action = ActionStatus.CHANGE
194            m.status.reaction = True
195        else:
196            m.status.action = ActionStatus.NOTHING
197
198        g.adapter.functions.post_processing(m)

メモをDBに記録する

Arguments:
  • m (MessageParserProtocol): メッセージデータ
  • remarks (list[RemarkDict]): メモに残す内容
def remarks_delete(m: integrations.protocols.MessageParserProtocol) -> None:
201def remarks_delete(m: "MessageParserProtocol") -> None:
202    """
203    DBからメモを削除する
204
205    Args:
206        m (MessageParserProtocol): メッセージデータ
207
208    """
209    if m.check_updatable:
210        with closing(dbutil.connection(g.cfg.setting.database_file)) as cur:
211            cur.execute(dbutil.query("REMARKS_DELETE_ONE"), (m.data.event_ts,))
212            cur.commit()
213            if count := cur.execute("select changes();").fetchone()[0]:
214                m.status.target_ts.append(m.data.event_ts)
215                logging.info("ts=%s, count=%s", m.data.event_ts, count)
216        # 後処理
217        m.status.action = ActionStatus.DELETE
218        g.adapter.functions.post_processing(m)

DBからメモを削除する

Arguments:
  • m (MessageParserProtocol): メッセージデータ
def remarks_delete_compar( m: integrations.protocols.MessageParserProtocol, para: libs.types.RemarkDict) -> None:
221def remarks_delete_compar(m: "MessageParserProtocol", para: "RemarkDict") -> None:
222    """
223    DBからメモを削除する(突合)
224
225    Args:
226        m (MessageParserProtocol): メッセージデータ
227        para (RemarkDict): パラメータ
228
229    """
230    with closing(dbutil.connection(g.cfg.setting.database_file)) as cur:
231        cur.execute(dbutil.query("REMARKS_DELETE_COMPAR"), para)
232        cur.commit()
233
234        left = cur.execute("select count() from remarks where event_ts=:event_ts;", para).fetchone()[0]
235        logging.info("ts=%s, count=%s", m.data.event_ts, left)
236
237    # 後処理
238    m.status.action = ActionStatus.DELETE
239    m.status.target_ts.append(para["event_ts"])
240    m.data.channel_id = para["source"].replace(f"{g.adapter.interface_type}_", "")
241    if left > 0:  # メモデータが残っているなら
242        m.status.action = ActionStatus.CHANGE
243    g.adapter.functions.post_processing(m)

DBからメモを削除する(突合)

Arguments:
  • m (MessageParserProtocol): メッセージデータ
  • para (RemarkDict): パラメータ
def check_remarks(m: integrations.protocols.MessageParserProtocol) -> None:
246def check_remarks(m: "MessageParserProtocol") -> None:
247    """
248    メモの内容を拾ってDBに格納する
249
250    Args:
251        m (MessageParserProtocol): メッセージデータ
252
253    """
254    game_result = lookup.exsist_record(m.data.thread_ts)
255    if game_result.has_valid_data():  # ゲーム結果のスレッドになっているか
256        remarks: list["RemarkDict"] = []
257        for name, matter in zip(m.argument[0::2], m.argument[1::2]):
258            remark: "RemarkDict" = {
259                "thread_ts": m.data.thread_ts,
260                "event_ts": m.data.event_ts,
261                "name": formatter.name_replace(name, not_replace=True),
262                "matter": matter,
263                "source": m.status.source,
264            }
265            if remark["name"] in game_result.to_list() and remark not in remarks:
266                remarks.append(remark)
267
268        match m.data.status:
269            case MessageStatus.APPEND:
270                remarks_append(m, remarks)
271            case MessageStatus.CHANGED:
272                remarks_delete(m)
273                remarks_append(m, remarks)
274            case MessageStatus.DELETED:
275                remarks_delete(m)
276            case _:
277                pass

メモの内容を拾ってDBに格納する

Arguments:
  • m (MessageParserProtocol): メッセージデータ
def reprocessing_remarks(m: integrations.protocols.MessageParserProtocol) -> None:
280def reprocessing_remarks(m: "MessageParserProtocol") -> None:
281    """
282    スレッドの内容を再処理
283
284    Args:
285        m (MessageParserProtocol): メッセージデータ
286
287    """
288    res = g.adapter.functions.get_conversations(m)
289    msg = res.get("messages")
290
291    if msg:
292        reply_count = int(cast(dict[str, Any], msg[0]).get("reply_count", 0))
293        m.data.thread_ts = str(cast(dict[str, Any], msg[0]).get("ts"))
294
295        for x in range(1, reply_count + 1):
296            m.data.text = str(cast(dict[str, str], msg[x]).get("text", ""))
297            if m.data.text:
298                m.data.event_ts = str(cast(dict[str, Any], msg[x]).get("ts"))
299                logging.debug(
300                    "(%s/%s) thread_ts=%s, event_ts=%s, %s",
301                    x,
302                    reply_count,
303                    m.data.thread_ts,
304                    m.data.event_ts,
305                    m.data.text,
306                )
307                if re.match(rf"^{g.cfg.setting.remarks_word}", m.keyword):
308                    check_remarks(m)

スレッドの内容を再処理

Arguments:
  • m (MessageParserProtocol): メッセージデータ