integrations.base.interface

integrations/base/interface.py

  1"""
  2integrations/base/interface.py
  3"""
  4
  5import re
  6from abc import ABC, abstractmethod
  7from configparser import ConfigParser
  8from dataclasses import dataclass, field, fields
  9from typing import (TYPE_CHECKING, Any, Generic, Literal, Optional, Type,
 10                    TypeVar, Union)
 11
 12from integrations.protocols import MsgData, PostData, StatusData
 13from libs.types import MessageTypeDict, StyleOptions
 14
 15if TYPE_CHECKING:
 16    from integrations.protocols import MessageParserProtocol
 17    from libs.types import MessageType
 18
 19ConfigT = TypeVar("ConfigT", bound="IntegrationsConfig")
 20ApiT = TypeVar("ApiT", bound="APIInterface")
 21FunctionsT = TypeVar("FunctionsT", bound="FunctionsInterface")
 22ParserT = TypeVar("ParserT", bound="MessageParserInterface")
 23
 24
 25class AdapterInterface(ABC, Generic[ConfigT, ApiT, FunctionsT, ParserT]):
 26    """アダプタインターフェース"""
 27
 28    interface_type: str
 29    """サービス識別子"""
 30
 31    conf: ConfigT
 32    """個別設定データクラス"""
 33    api: ApiT
 34    """インターフェース操作APIインスタンス"""
 35    functions: FunctionsT
 36    """サービス専用関数インスタンス"""
 37    parser: Type[ParserT]
 38    """メッセージパーサクラス"""
 39
 40
 41@dataclass
 42class IntegrationsConfig(ABC):
 43    """個別設定値"""
 44
 45    # ディスパッチテーブル用
 46    _command_dispatcher: dict = field(default_factory=dict)
 47    _keyword_dispatcher: dict = field(default_factory=dict)
 48
 49    config_file: Optional[ConfigParser] = field(default=None)
 50    """設定ファイル"""
 51
 52    # 共通設定
 53    slash_command: str = field(default="")
 54    """スラッシュコマンド名"""
 55
 56    badge_degree: bool = field(default=False)
 57    """プレイしたゲーム数に対して表示される称号
 58    - *True*: 表示する
 59    - *False*: 表示しない
 60    """
 61    badge_status: bool = field(default=False)
 62    """勝率に対して付く調子バッジ
 63    - *True*: 表示する
 64    - *False*: 表示しない
 65    """
 66    badge_grade: bool = field(default=False)
 67    """段位表示
 68    - *True*: 表示する
 69    - *False*: 表示しない
 70    """
 71
 72    plotting_backend: Literal["matplotlib", "plotly"] = field(default="matplotlib")
 73    """グラフ描写ライブラリ"""
 74
 75    @property
 76    def command_dispatcher(self) -> dict:
 77        """コマンドディスパッチテーブルを辞書で取得
 78
 79        Returns:
 80            dict: コマンドディスパッチテーブル
 81        """
 82
 83        return self._command_dispatcher
 84
 85    @property
 86    def keyword_dispatcher(self) -> dict:
 87        """キーワードディスパッチテーブルを辞書で取得
 88
 89        Returns:
 90            dict: キーワードディスパッチテーブル
 91        """
 92
 93        return self._keyword_dispatcher
 94
 95    def read_file(self, selected_service: str):
 96        """設定値取り込み
 97
 98        Args:
 99            selected_service (str): セクション
100
101        Raises:
102            TypeError: 無効な型が指定されている場合
103        """
104
105        if self.config_file is None:
106            raise TypeError("Configuration file not specified.")
107
108        value: Union[int, float, bool, str, list]
109        if self.config_file.has_section(selected_service):
110            for f in fields(self):
111                if f.name.startswith("_"):
112                    continue
113                if self.config_file.has_option(selected_service, f.name):
114                    if f.type is int:
115                        value = self.config_file.getint(selected_service, f.name)
116                    elif f.type is float:
117                        value = self.config_file.getfloat(selected_service, f.name)
118                    elif f.type is bool:
119                        value = self.config_file.getboolean(selected_service, f.name)
120                    elif f.type is str:
121                        value = self.config_file.get(selected_service, f.name)
122                    elif f.type is list:
123                        value = [x.strip() for x in self.config_file.get(selected_service, f.name).split(",")]
124                    else:
125                        raise TypeError(f"Unsupported type: {f.type}")
126                    setattr(self, f.name, value)
127
128
129class FunctionsInterface(ABC):
130    """個別関数インターフェース"""
131
132    @abstractmethod
133    def post_processing(self, m: "MessageParserProtocol"):
134        """後処理
135
136        Args:
137            m (MessageParserProtocol): メッセージデータ
138        """
139
140    @abstractmethod
141    def get_conversations(self, m: "MessageParserProtocol") -> dict:
142        """スレッド情報の取得
143
144        Args:
145            m (MessageParserProtocol): メッセージデータ
146
147        Returns:
148            dict: API response
149        """
150        return {}
151
152
153class APIInterface(ABC):
154    """アダプタAPIインターフェース"""
155
156    @abstractmethod
157    def post(self, m: "MessageParserProtocol"):
158        """メッセージを出力する
159
160        Args:
161            m (MessageParserProtocol): メッセージデータ
162        """
163
164
165class MessageParserDataMixin:
166    """メッセージ解析共通処理"""
167
168    data: "MsgData"
169    post: "PostData"
170    status: "StatusData"
171
172    def reset(self) -> None:
173        """初期化"""
174
175        self.data.reset()
176        self.post.reset()
177        self.status.reset()
178
179    def set_data(
180        self,
181        title: str,
182        data: "MessageType",
183        options: StyleOptions,
184    ):
185        """メッセージデータをセットshow_index
186
187        Args:
188            title (str): データ識別子
189            data (MessageType): 内容
190            options (StyleOptions): 表示オプション
191        """
192
193        msg = MessageTypeDict(
194            data=data,
195            options=options,
196        )
197        self.post.message.append({title: msg})
198
199    def get_score(self, keyword: str) -> dict:
200        """textからスコアを抽出する
201
202        Args:
203            keyword (str): 成績報告キーワード
204
205        Returns:
206            dict: 結果
207        """
208
209        text = self.data.text
210        ret: dict = {}
211
212        # 記号を置換
213        replace_chr = [
214            (chr(0xff0b), "+"),  # 全角プラス符号
215            (chr(0x2212), "-"),  # 全角マイナス符号
216            (chr(0xff08), "("),  # 全角丸括弧
217            (chr(0xff09), ")"),  # 全角丸括弧
218            (chr(0x2017), "_"),  # DOUBLE LOW LINE(半角)
219        ]
220        for z, h in replace_chr:
221            text = text.replace(z, h)
222
223        text = "".join(text.split())  # 改行削除
224
225        # パターンマッチング
226        pattern1 = re.compile(
227            rf"^({keyword})" + r"([^0-9()+-]+)([0-9+-]+)" * 4 + r"$"
228        )
229        pattern2 = re.compile(
230            r"^" + r"([^0-9()+-]+)([0-9+-]+)" * 4 + rf"({keyword})$"
231        )
232        pattern3 = re.compile(
233            rf"^({keyword})\((.+?)\)" + r"([^0-9()+-]+)([0-9+-]+)" * 4 + r"$"
234        )
235        pattern4 = re.compile(
236            r"^" + r"([^0-9()+-]+)([0-9+-]+)" * 4 + rf"({keyword})\((.+?)\)$"
237        )
238
239        # 情報取り出し
240        position: dict[str, int] = {}
241        match text:
242            case text if pattern1.findall(text):
243                msg = pattern1.findall(text)[0]
244                position = {
245                    "p1_name": 1, "p1_str": 2,
246                    "p2_name": 3, "p2_str": 4,
247                    "p3_name": 5, "p3_str": 6,
248                    "p4_name": 7, "p4_str": 8,
249                }
250                comment = None
251            case text if pattern2.findall(text):
252                msg = pattern2.findall(text)[0]
253                position = {
254                    "p1_name": 0, "p1_str": 1,
255                    "p2_name": 2, "p2_str": 3,
256                    "p3_name": 4, "p3_str": 5,
257                    "p4_name": 6, "p4_str": 7,
258                }
259                comment = None
260            case text if pattern3.findall(text):
261                msg = pattern3.findall(text)[0]
262                position = {
263                    "p1_name": 2, "p1_str": 3,
264                    "p2_name": 4, "p2_str": 5,
265                    "p3_name": 6, "p3_str": 7,
266                    "p4_name": 8, "p4_str": 9,
267                }
268                comment = str(msg[1])
269            case text if pattern4.findall(text):
270                msg = pattern4.findall(text)[0]
271                position = {
272                    "p1_name": 0, "p1_str": 1,
273                    "p2_name": 2, "p2_str": 3,
274                    "p3_name": 4, "p3_str": 5,
275                    "p4_name": 6, "p4_str": 7,
276                }
277                comment = str(msg[9])
278            case _:
279                return ret
280
281        for k, p in position.items():
282            ret.update({k: str(msg[p])})
283
284        ret.update(comment=comment)
285        ret.update(source=self.status.source)
286        ret.update(ts=self.data.event_ts)
287
288        return ret
289
290    def get_remarks(self, keyword: str) -> list:
291        """textからメモを抽出する
292
293        Args:
294            keyword (str): メモ記録キーワード
295
296        Returns:
297            list: 結果
298        """
299
300        ret: list = []
301        if re.match(rf"^{keyword}", self.data.text):  # キーワードが先頭に存在するかチェック
302            text = self.data.text.replace(keyword, "").strip().split()
303            for name, matter in zip(text[0::2], text[1::2]):
304                ret.append([name, matter])
305
306        return ret
307
308
309class MessageParserInterface(ABC):
310    """メッセージ解析インターフェース"""
311
312    data: "MsgData"
313    post: "PostData"
314    status: "StatusData"
315
316    @abstractmethod
317    def parser(self, body: Any):
318        """メッセージ解析
319
320        Args:
321            body (Any): 解析データ
322        """
323
324    @property
325    @abstractmethod
326    def in_thread(self) -> bool:
327        """元メッセージへのリプライとなっているか
328
329        Returns:
330            bool: 真偽値
331            - *True* : リプライの形(リプライ/スレッドなど)
332            - *False* : 通常メッセージ
333        """
334
335    @property
336    @abstractmethod
337    def is_bot(self) -> bool:
338        """botのポストかチェック
339
340        Returns:
341            bool: 真偽値
342            - *True* : botのポスト
343            - *False* : ユーザのポスト
344        """
345
346    @property
347    @abstractmethod
348    def check_updatable(self) -> bool:
349        """DB操作の許可チェック
350
351        Returns:
352            bool: 真偽値
353            - *True* : 許可
354            - *False* : 禁止
355        """
356
357    @property
358    @abstractmethod
359    def ignore_user(self) -> bool:
360        """ignore_useridに存在するユーザかチェック
361
362        Returns:
363            bool: 真偽値
364            - *True* : 存在する(操作禁止ユーザ)
365            - *False* : 存在しない
366        """
367
368    @property
369    def is_command(self) -> bool:
370        """コマンドで実行されているかチェック
371
372        Returns:
373            bool: 真偽値
374            - *True* : コマンド実行
375            - *False* : 非コマンド(キーワード呼び出し)
376        """
377
378        return self.status.command_flg
379
380    @property
381    def keyword(self) -> str:
382        """コマンドとして認識している文字列を返す
383
384        Returns:
385            str: コマンド名
386        """
387
388        if (ret := self.data.text.split()):
389            return ret[0]
390        return self.data.text
391
392    @property
393    def argument(self) -> list:
394        """コマンド引数として認識している文字列をリストで返す
395
396        Returns:
397            list: 引数リスト
398        """
399
400        if (ret := self.data.text.split()):
401            return ret[1:]
402        return ret
403
404    @property
405    def reply_ts(self) -> str:
406        """リプライ先のタイムスタンプを取得する
407
408        Returns:
409            str: タイムスタンプ
410        """
411
412        ret_ts: str = "0"
413
414        # tsが指定されていれば最優先
415        if self.post.ts != "undetermined":
416            return self.post.ts
417
418        # スレッドに返すか
419        if self.post.thread and self.in_thread:  # スレッド内
420            if self.data.thread_ts != "undetermined":
421                ret_ts = self.data.thread_ts
422        elif self.post.thread and not self.in_thread:  # スレッド外
423            ret_ts = self.data.event_ts
424
425        return ret_ts
class AdapterInterface(abc.ABC, typing.Generic[~ConfigT, ~ApiT, ~FunctionsT, ~ParserT]):
26class AdapterInterface(ABC, Generic[ConfigT, ApiT, FunctionsT, ParserT]):
27    """アダプタインターフェース"""
28
29    interface_type: str
30    """サービス識別子"""
31
32    conf: ConfigT
33    """個別設定データクラス"""
34    api: ApiT
35    """インターフェース操作APIインスタンス"""
36    functions: FunctionsT
37    """サービス専用関数インスタンス"""
38    parser: Type[ParserT]
39    """メッセージパーサクラス"""

アダプタインターフェース

interface_type: str

サービス識別子

conf: ~ConfigT

個別設定データクラス

api: ~ApiT

インターフェース操作APIインスタンス

functions: ~FunctionsT

サービス専用関数インスタンス

parser: Type[~ParserT]

メッセージパーサクラス

@dataclass
class IntegrationsConfig(abc.ABC):
 42@dataclass
 43class IntegrationsConfig(ABC):
 44    """個別設定値"""
 45
 46    # ディスパッチテーブル用
 47    _command_dispatcher: dict = field(default_factory=dict)
 48    _keyword_dispatcher: dict = field(default_factory=dict)
 49
 50    config_file: Optional[ConfigParser] = field(default=None)
 51    """設定ファイル"""
 52
 53    # 共通設定
 54    slash_command: str = field(default="")
 55    """スラッシュコマンド名"""
 56
 57    badge_degree: bool = field(default=False)
 58    """プレイしたゲーム数に対して表示される称号
 59    - *True*: 表示する
 60    - *False*: 表示しない
 61    """
 62    badge_status: bool = field(default=False)
 63    """勝率に対して付く調子バッジ
 64    - *True*: 表示する
 65    - *False*: 表示しない
 66    """
 67    badge_grade: bool = field(default=False)
 68    """段位表示
 69    - *True*: 表示する
 70    - *False*: 表示しない
 71    """
 72
 73    plotting_backend: Literal["matplotlib", "plotly"] = field(default="matplotlib")
 74    """グラフ描写ライブラリ"""
 75
 76    @property
 77    def command_dispatcher(self) -> dict:
 78        """コマンドディスパッチテーブルを辞書で取得
 79
 80        Returns:
 81            dict: コマンドディスパッチテーブル
 82        """
 83
 84        return self._command_dispatcher
 85
 86    @property
 87    def keyword_dispatcher(self) -> dict:
 88        """キーワードディスパッチテーブルを辞書で取得
 89
 90        Returns:
 91            dict: キーワードディスパッチテーブル
 92        """
 93
 94        return self._keyword_dispatcher
 95
 96    def read_file(self, selected_service: str):
 97        """設定値取り込み
 98
 99        Args:
100            selected_service (str): セクション
101
102        Raises:
103            TypeError: 無効な型が指定されている場合
104        """
105
106        if self.config_file is None:
107            raise TypeError("Configuration file not specified.")
108
109        value: Union[int, float, bool, str, list]
110        if self.config_file.has_section(selected_service):
111            for f in fields(self):
112                if f.name.startswith("_"):
113                    continue
114                if self.config_file.has_option(selected_service, f.name):
115                    if f.type is int:
116                        value = self.config_file.getint(selected_service, f.name)
117                    elif f.type is float:
118                        value = self.config_file.getfloat(selected_service, f.name)
119                    elif f.type is bool:
120                        value = self.config_file.getboolean(selected_service, f.name)
121                    elif f.type is str:
122                        value = self.config_file.get(selected_service, f.name)
123                    elif f.type is list:
124                        value = [x.strip() for x in self.config_file.get(selected_service, f.name).split(",")]
125                    else:
126                        raise TypeError(f"Unsupported type: {f.type}")
127                    setattr(self, f.name, value)

個別設定値

IntegrationsConfig( _command_dispatcher: dict = <factory>, _keyword_dispatcher: dict = <factory>, config_file: Optional[configparser.ConfigParser] = None, slash_command: str = '', badge_degree: bool = False, badge_status: bool = False, badge_grade: bool = False, plotting_backend: Literal['matplotlib', 'plotly'] = 'matplotlib')
config_file: Optional[configparser.ConfigParser] = None

設定ファイル

slash_command: str = ''

スラッシュコマンド名

badge_degree: bool = False

プレイしたゲーム数に対して表示される称号

  • True: 表示する
  • False: 表示しない
badge_status: bool = False

勝率に対して付く調子バッジ

  • True: 表示する
  • False: 表示しない
badge_grade: bool = False

段位表示

  • True: 表示する
  • False: 表示しない
plotting_backend: Literal['matplotlib', 'plotly'] = 'matplotlib'

グラフ描写ライブラリ

command_dispatcher: dict
76    @property
77    def command_dispatcher(self) -> dict:
78        """コマンドディスパッチテーブルを辞書で取得
79
80        Returns:
81            dict: コマンドディスパッチテーブル
82        """
83
84        return self._command_dispatcher

コマンドディスパッチテーブルを辞書で取得

Returns:

dict: コマンドディスパッチテーブル

keyword_dispatcher: dict
86    @property
87    def keyword_dispatcher(self) -> dict:
88        """キーワードディスパッチテーブルを辞書で取得
89
90        Returns:
91            dict: キーワードディスパッチテーブル
92        """
93
94        return self._keyword_dispatcher

キーワードディスパッチテーブルを辞書で取得

Returns:

dict: キーワードディスパッチテーブル

def read_file(self, selected_service: str):
 96    def read_file(self, selected_service: str):
 97        """設定値取り込み
 98
 99        Args:
100            selected_service (str): セクション
101
102        Raises:
103            TypeError: 無効な型が指定されている場合
104        """
105
106        if self.config_file is None:
107            raise TypeError("Configuration file not specified.")
108
109        value: Union[int, float, bool, str, list]
110        if self.config_file.has_section(selected_service):
111            for f in fields(self):
112                if f.name.startswith("_"):
113                    continue
114                if self.config_file.has_option(selected_service, f.name):
115                    if f.type is int:
116                        value = self.config_file.getint(selected_service, f.name)
117                    elif f.type is float:
118                        value = self.config_file.getfloat(selected_service, f.name)
119                    elif f.type is bool:
120                        value = self.config_file.getboolean(selected_service, f.name)
121                    elif f.type is str:
122                        value = self.config_file.get(selected_service, f.name)
123                    elif f.type is list:
124                        value = [x.strip() for x in self.config_file.get(selected_service, f.name).split(",")]
125                    else:
126                        raise TypeError(f"Unsupported type: {f.type}")
127                    setattr(self, f.name, value)

設定値取り込み

Arguments:
  • selected_service (str): セクション
Raises:
  • TypeError: 無効な型が指定されている場合
class FunctionsInterface(abc.ABC):
130class FunctionsInterface(ABC):
131    """個別関数インターフェース"""
132
133    @abstractmethod
134    def post_processing(self, m: "MessageParserProtocol"):
135        """後処理
136
137        Args:
138            m (MessageParserProtocol): メッセージデータ
139        """
140
141    @abstractmethod
142    def get_conversations(self, m: "MessageParserProtocol") -> dict:
143        """スレッド情報の取得
144
145        Args:
146            m (MessageParserProtocol): メッセージデータ
147
148        Returns:
149            dict: API response
150        """
151        return {}

個別関数インターフェース

@abstractmethod
def post_processing(self, m: integrations.protocols.MessageParserProtocol):
133    @abstractmethod
134    def post_processing(self, m: "MessageParserProtocol"):
135        """後処理
136
137        Args:
138            m (MessageParserProtocol): メッセージデータ
139        """

後処理

Arguments:
  • m (MessageParserProtocol): メッセージデータ
@abstractmethod
def get_conversations(self, m: integrations.protocols.MessageParserProtocol) -> dict:
141    @abstractmethod
142    def get_conversations(self, m: "MessageParserProtocol") -> dict:
143        """スレッド情報の取得
144
145        Args:
146            m (MessageParserProtocol): メッセージデータ
147
148        Returns:
149            dict: API response
150        """
151        return {}

スレッド情報の取得

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

dict: API response

class APIInterface(abc.ABC):
154class APIInterface(ABC):
155    """アダプタAPIインターフェース"""
156
157    @abstractmethod
158    def post(self, m: "MessageParserProtocol"):
159        """メッセージを出力する
160
161        Args:
162            m (MessageParserProtocol): メッセージデータ
163        """

アダプタAPIインターフェース

@abstractmethod
def post(self, m: integrations.protocols.MessageParserProtocol):
157    @abstractmethod
158    def post(self, m: "MessageParserProtocol"):
159        """メッセージを出力する
160
161        Args:
162            m (MessageParserProtocol): メッセージデータ
163        """

メッセージを出力する

Arguments:
  • m (MessageParserProtocol): メッセージデータ
class MessageParserDataMixin:
166class MessageParserDataMixin:
167    """メッセージ解析共通処理"""
168
169    data: "MsgData"
170    post: "PostData"
171    status: "StatusData"
172
173    def reset(self) -> None:
174        """初期化"""
175
176        self.data.reset()
177        self.post.reset()
178        self.status.reset()
179
180    def set_data(
181        self,
182        title: str,
183        data: "MessageType",
184        options: StyleOptions,
185    ):
186        """メッセージデータをセットshow_index
187
188        Args:
189            title (str): データ識別子
190            data (MessageType): 内容
191            options (StyleOptions): 表示オプション
192        """
193
194        msg = MessageTypeDict(
195            data=data,
196            options=options,
197        )
198        self.post.message.append({title: msg})
199
200    def get_score(self, keyword: str) -> dict:
201        """textからスコアを抽出する
202
203        Args:
204            keyword (str): 成績報告キーワード
205
206        Returns:
207            dict: 結果
208        """
209
210        text = self.data.text
211        ret: dict = {}
212
213        # 記号を置換
214        replace_chr = [
215            (chr(0xff0b), "+"),  # 全角プラス符号
216            (chr(0x2212), "-"),  # 全角マイナス符号
217            (chr(0xff08), "("),  # 全角丸括弧
218            (chr(0xff09), ")"),  # 全角丸括弧
219            (chr(0x2017), "_"),  # DOUBLE LOW LINE(半角)
220        ]
221        for z, h in replace_chr:
222            text = text.replace(z, h)
223
224        text = "".join(text.split())  # 改行削除
225
226        # パターンマッチング
227        pattern1 = re.compile(
228            rf"^({keyword})" + r"([^0-9()+-]+)([0-9+-]+)" * 4 + r"$"
229        )
230        pattern2 = re.compile(
231            r"^" + r"([^0-9()+-]+)([0-9+-]+)" * 4 + rf"({keyword})$"
232        )
233        pattern3 = re.compile(
234            rf"^({keyword})\((.+?)\)" + r"([^0-9()+-]+)([0-9+-]+)" * 4 + r"$"
235        )
236        pattern4 = re.compile(
237            r"^" + r"([^0-9()+-]+)([0-9+-]+)" * 4 + rf"({keyword})\((.+?)\)$"
238        )
239
240        # 情報取り出し
241        position: dict[str, int] = {}
242        match text:
243            case text if pattern1.findall(text):
244                msg = pattern1.findall(text)[0]
245                position = {
246                    "p1_name": 1, "p1_str": 2,
247                    "p2_name": 3, "p2_str": 4,
248                    "p3_name": 5, "p3_str": 6,
249                    "p4_name": 7, "p4_str": 8,
250                }
251                comment = None
252            case text if pattern2.findall(text):
253                msg = pattern2.findall(text)[0]
254                position = {
255                    "p1_name": 0, "p1_str": 1,
256                    "p2_name": 2, "p2_str": 3,
257                    "p3_name": 4, "p3_str": 5,
258                    "p4_name": 6, "p4_str": 7,
259                }
260                comment = None
261            case text if pattern3.findall(text):
262                msg = pattern3.findall(text)[0]
263                position = {
264                    "p1_name": 2, "p1_str": 3,
265                    "p2_name": 4, "p2_str": 5,
266                    "p3_name": 6, "p3_str": 7,
267                    "p4_name": 8, "p4_str": 9,
268                }
269                comment = str(msg[1])
270            case text if pattern4.findall(text):
271                msg = pattern4.findall(text)[0]
272                position = {
273                    "p1_name": 0, "p1_str": 1,
274                    "p2_name": 2, "p2_str": 3,
275                    "p3_name": 4, "p3_str": 5,
276                    "p4_name": 6, "p4_str": 7,
277                }
278                comment = str(msg[9])
279            case _:
280                return ret
281
282        for k, p in position.items():
283            ret.update({k: str(msg[p])})
284
285        ret.update(comment=comment)
286        ret.update(source=self.status.source)
287        ret.update(ts=self.data.event_ts)
288
289        return ret
290
291    def get_remarks(self, keyword: str) -> list:
292        """textからメモを抽出する
293
294        Args:
295            keyword (str): メモ記録キーワード
296
297        Returns:
298            list: 結果
299        """
300
301        ret: list = []
302        if re.match(rf"^{keyword}", self.data.text):  # キーワードが先頭に存在するかチェック
303            text = self.data.text.replace(keyword, "").strip().split()
304            for name, matter in zip(text[0::2], text[1::2]):
305                ret.append([name, matter])
306
307        return ret

メッセージ解析共通処理

def reset(self) -> None:
173    def reset(self) -> None:
174        """初期化"""
175
176        self.data.reset()
177        self.post.reset()
178        self.status.reset()

初期化

def set_data( self, title: str, data: 'MessageType', options: libs.types.StyleOptions):
180    def set_data(
181        self,
182        title: str,
183        data: "MessageType",
184        options: StyleOptions,
185    ):
186        """メッセージデータをセットshow_index
187
188        Args:
189            title (str): データ識別子
190            data (MessageType): 内容
191            options (StyleOptions): 表示オプション
192        """
193
194        msg = MessageTypeDict(
195            data=data,
196            options=options,
197        )
198        self.post.message.append({title: msg})

メッセージデータをセットshow_index

Arguments:
  • title (str): データ識別子
  • data (MessageType): 内容
  • options (StyleOptions): 表示オプション
def get_score(self, keyword: str) -> dict:
200    def get_score(self, keyword: str) -> dict:
201        """textからスコアを抽出する
202
203        Args:
204            keyword (str): 成績報告キーワード
205
206        Returns:
207            dict: 結果
208        """
209
210        text = self.data.text
211        ret: dict = {}
212
213        # 記号を置換
214        replace_chr = [
215            (chr(0xff0b), "+"),  # 全角プラス符号
216            (chr(0x2212), "-"),  # 全角マイナス符号
217            (chr(0xff08), "("),  # 全角丸括弧
218            (chr(0xff09), ")"),  # 全角丸括弧
219            (chr(0x2017), "_"),  # DOUBLE LOW LINE(半角)
220        ]
221        for z, h in replace_chr:
222            text = text.replace(z, h)
223
224        text = "".join(text.split())  # 改行削除
225
226        # パターンマッチング
227        pattern1 = re.compile(
228            rf"^({keyword})" + r"([^0-9()+-]+)([0-9+-]+)" * 4 + r"$"
229        )
230        pattern2 = re.compile(
231            r"^" + r"([^0-9()+-]+)([0-9+-]+)" * 4 + rf"({keyword})$"
232        )
233        pattern3 = re.compile(
234            rf"^({keyword})\((.+?)\)" + r"([^0-9()+-]+)([0-9+-]+)" * 4 + r"$"
235        )
236        pattern4 = re.compile(
237            r"^" + r"([^0-9()+-]+)([0-9+-]+)" * 4 + rf"({keyword})\((.+?)\)$"
238        )
239
240        # 情報取り出し
241        position: dict[str, int] = {}
242        match text:
243            case text if pattern1.findall(text):
244                msg = pattern1.findall(text)[0]
245                position = {
246                    "p1_name": 1, "p1_str": 2,
247                    "p2_name": 3, "p2_str": 4,
248                    "p3_name": 5, "p3_str": 6,
249                    "p4_name": 7, "p4_str": 8,
250                }
251                comment = None
252            case text if pattern2.findall(text):
253                msg = pattern2.findall(text)[0]
254                position = {
255                    "p1_name": 0, "p1_str": 1,
256                    "p2_name": 2, "p2_str": 3,
257                    "p3_name": 4, "p3_str": 5,
258                    "p4_name": 6, "p4_str": 7,
259                }
260                comment = None
261            case text if pattern3.findall(text):
262                msg = pattern3.findall(text)[0]
263                position = {
264                    "p1_name": 2, "p1_str": 3,
265                    "p2_name": 4, "p2_str": 5,
266                    "p3_name": 6, "p3_str": 7,
267                    "p4_name": 8, "p4_str": 9,
268                }
269                comment = str(msg[1])
270            case text if pattern4.findall(text):
271                msg = pattern4.findall(text)[0]
272                position = {
273                    "p1_name": 0, "p1_str": 1,
274                    "p2_name": 2, "p2_str": 3,
275                    "p3_name": 4, "p3_str": 5,
276                    "p4_name": 6, "p4_str": 7,
277                }
278                comment = str(msg[9])
279            case _:
280                return ret
281
282        for k, p in position.items():
283            ret.update({k: str(msg[p])})
284
285        ret.update(comment=comment)
286        ret.update(source=self.status.source)
287        ret.update(ts=self.data.event_ts)
288
289        return ret

textからスコアを抽出する

Arguments:
  • keyword (str): 成績報告キーワード
Returns:

dict: 結果

def get_remarks(self, keyword: str) -> list:
291    def get_remarks(self, keyword: str) -> list:
292        """textからメモを抽出する
293
294        Args:
295            keyword (str): メモ記録キーワード
296
297        Returns:
298            list: 結果
299        """
300
301        ret: list = []
302        if re.match(rf"^{keyword}", self.data.text):  # キーワードが先頭に存在するかチェック
303            text = self.data.text.replace(keyword, "").strip().split()
304            for name, matter in zip(text[0::2], text[1::2]):
305                ret.append([name, matter])
306
307        return ret

textからメモを抽出する

Arguments:
  • keyword (str): メモ記録キーワード
Returns:

list: 結果

class MessageParserInterface(abc.ABC):
310class MessageParserInterface(ABC):
311    """メッセージ解析インターフェース"""
312
313    data: "MsgData"
314    post: "PostData"
315    status: "StatusData"
316
317    @abstractmethod
318    def parser(self, body: Any):
319        """メッセージ解析
320
321        Args:
322            body (Any): 解析データ
323        """
324
325    @property
326    @abstractmethod
327    def in_thread(self) -> bool:
328        """元メッセージへのリプライとなっているか
329
330        Returns:
331            bool: 真偽値
332            - *True* : リプライの形(リプライ/スレッドなど)
333            - *False* : 通常メッセージ
334        """
335
336    @property
337    @abstractmethod
338    def is_bot(self) -> bool:
339        """botのポストかチェック
340
341        Returns:
342            bool: 真偽値
343            - *True* : botのポスト
344            - *False* : ユーザのポスト
345        """
346
347    @property
348    @abstractmethod
349    def check_updatable(self) -> bool:
350        """DB操作の許可チェック
351
352        Returns:
353            bool: 真偽値
354            - *True* : 許可
355            - *False* : 禁止
356        """
357
358    @property
359    @abstractmethod
360    def ignore_user(self) -> bool:
361        """ignore_useridに存在するユーザかチェック
362
363        Returns:
364            bool: 真偽値
365            - *True* : 存在する(操作禁止ユーザ)
366            - *False* : 存在しない
367        """
368
369    @property
370    def is_command(self) -> bool:
371        """コマンドで実行されているかチェック
372
373        Returns:
374            bool: 真偽値
375            - *True* : コマンド実行
376            - *False* : 非コマンド(キーワード呼び出し)
377        """
378
379        return self.status.command_flg
380
381    @property
382    def keyword(self) -> str:
383        """コマンドとして認識している文字列を返す
384
385        Returns:
386            str: コマンド名
387        """
388
389        if (ret := self.data.text.split()):
390            return ret[0]
391        return self.data.text
392
393    @property
394    def argument(self) -> list:
395        """コマンド引数として認識している文字列をリストで返す
396
397        Returns:
398            list: 引数リスト
399        """
400
401        if (ret := self.data.text.split()):
402            return ret[1:]
403        return ret
404
405    @property
406    def reply_ts(self) -> str:
407        """リプライ先のタイムスタンプを取得する
408
409        Returns:
410            str: タイムスタンプ
411        """
412
413        ret_ts: str = "0"
414
415        # tsが指定されていれば最優先
416        if self.post.ts != "undetermined":
417            return self.post.ts
418
419        # スレッドに返すか
420        if self.post.thread and self.in_thread:  # スレッド内
421            if self.data.thread_ts != "undetermined":
422                ret_ts = self.data.thread_ts
423        elif self.post.thread and not self.in_thread:  # スレッド外
424            ret_ts = self.data.event_ts
425
426        return ret_ts

メッセージ解析インターフェース

@abstractmethod
def parser(self, body: Any):
317    @abstractmethod
318    def parser(self, body: Any):
319        """メッセージ解析
320
321        Args:
322            body (Any): 解析データ
323        """

メッセージ解析

Arguments:
  • body (Any): 解析データ
in_thread: bool
325    @property
326    @abstractmethod
327    def in_thread(self) -> bool:
328        """元メッセージへのリプライとなっているか
329
330        Returns:
331            bool: 真偽値
332            - *True* : リプライの形(リプライ/スレッドなど)
333            - *False* : 通常メッセージ
334        """

元メッセージへのリプライとなっているか

Returns:

bool: 真偽値

  • True : リプライの形(リプライ/スレッドなど)
  • False : 通常メッセージ
is_bot: bool
336    @property
337    @abstractmethod
338    def is_bot(self) -> bool:
339        """botのポストかチェック
340
341        Returns:
342            bool: 真偽値
343            - *True* : botのポスト
344            - *False* : ユーザのポスト
345        """

botのポストかチェック

Returns:

bool: 真偽値

  • True : botのポスト
  • False : ユーザのポスト
check_updatable: bool
347    @property
348    @abstractmethod
349    def check_updatable(self) -> bool:
350        """DB操作の許可チェック
351
352        Returns:
353            bool: 真偽値
354            - *True* : 許可
355            - *False* : 禁止
356        """

DB操作の許可チェック

Returns:

bool: 真偽値

  • True : 許可
  • False : 禁止
ignore_user: bool
358    @property
359    @abstractmethod
360    def ignore_user(self) -> bool:
361        """ignore_useridに存在するユーザかチェック
362
363        Returns:
364            bool: 真偽値
365            - *True* : 存在する(操作禁止ユーザ)
366            - *False* : 存在しない
367        """

ignore_useridに存在するユーザかチェック

Returns:

bool: 真偽値

  • True : 存在する(操作禁止ユーザ)
  • False : 存在しない
is_command: bool
369    @property
370    def is_command(self) -> bool:
371        """コマンドで実行されているかチェック
372
373        Returns:
374            bool: 真偽値
375            - *True* : コマンド実行
376            - *False* : 非コマンド(キーワード呼び出し)
377        """
378
379        return self.status.command_flg

コマンドで実行されているかチェック

Returns:

bool: 真偽値

  • True : コマンド実行
  • False : 非コマンド(キーワード呼び出し)
keyword: str
381    @property
382    def keyword(self) -> str:
383        """コマンドとして認識している文字列を返す
384
385        Returns:
386            str: コマンド名
387        """
388
389        if (ret := self.data.text.split()):
390            return ret[0]
391        return self.data.text

コマンドとして認識している文字列を返す

Returns:

str: コマンド名

argument: list
393    @property
394    def argument(self) -> list:
395        """コマンド引数として認識している文字列をリストで返す
396
397        Returns:
398            list: 引数リスト
399        """
400
401        if (ret := self.data.text.split()):
402            return ret[1:]
403        return ret

コマンド引数として認識している文字列をリストで返す

Returns:

list: 引数リスト

reply_ts: str
405    @property
406    def reply_ts(self) -> str:
407        """リプライ先のタイムスタンプを取得する
408
409        Returns:
410            str: タイムスタンプ
411        """
412
413        ret_ts: str = "0"
414
415        # tsが指定されていれば最優先
416        if self.post.ts != "undetermined":
417            return self.post.ts
418
419        # スレッドに返すか
420        if self.post.thread and self.in_thread:  # スレッド内
421            if self.data.thread_ts != "undetermined":
422                ret_ts = self.data.thread_ts
423        elif self.post.thread and not self.in_thread:  # スレッド外
424            ret_ts = self.data.event_ts
425
426        return ret_ts

リプライ先のタイムスタンプを取得する

Returns:

str: タイムスタンプ