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 """メッセージパーサクラス"""
アダプタインターフェース
@dataclass
class
IntegrationsConfig42@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')
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 {}
個別関数インターフェース
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インターフェース
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()
初期化
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: タイムスタンプ