cls.parser

cls/parser.py

  1"""
  2cls/parser.py
  3"""
  4
  5import logging
  6import re
  7from dataclasses import dataclass
  8from typing import Any
  9
 10import pandas as pd
 11from slack_sdk import WebClient
 12
 13import libs.global_value as g
 14from cls.timekit import ExtendedDatetime as ExtDt
 15from cls.types import CommandSpec
 16from libs.data import lookup
 17from libs.utils import textutil
 18
 19
 20@dataclass
 21class ParsedCommand:
 22    """コマンド解析結果"""
 23    flags: dict[str, Any]
 24    arguments: list[str]
 25    unknown: list[str]
 26    search_range: list["ExtDt"]
 27
 28
 29CommandsDict = dict[str, CommandSpec]
 30COMMANDS: CommandsDict = {
 31    "guest": {
 32        "match": [r"^ゲストナシ$", r"^ゲストアリ$", r"^ゲスト無効$"],
 33        "action": lambda w: {
 34            "ゲストナシ": {"guest_skip": False, "guest_skip2": False, "unregistered_replace": True},
 35            "ゲストアリ": {"guest_skip": True, "guest_skip2": True, "unregistered_replace": True},
 36            "ゲスト無効": {"unregistered_replace": False},
 37        }[w[0] if isinstance(w, tuple) else w],
 38    },
 39    "anonymous": {
 40        "match": [r"^匿名$", r"^anonymous$"],
 41        "action": lambda w: {"anonymous": True},
 42    },
 43
 44    "individual": {
 45        "match": [r"^個人$", "^個人成績$"],
 46        "action": lambda w: {"individual": True},
 47    },
 48    "team": {
 49        "match": [r"^チーム$", "^チーム成績$", "^team$"],
 50        "action": lambda w: {"individual": False},
 51    },
 52
 53    "all_player": {
 54        "match": [r"^全員$", r"^all$"],
 55        "action": lambda w: {"all_player": True},
 56    },
 57
 58    "a": {
 59        "match": [r"^(チーム同卓アリ|コンビアリ|同士討チ)$"],
 60        "action": lambda w: {"friendly_fire": True},
 61    },
 62    "b": {
 63        "match": [r"^(チーム同卓ナシ|コンビナシ)$"],
 64        "action": lambda w: {"friendly_fire": False},
 65    },
 66    # --- 動作変更フラグ
 67    "score_comparisons": {  # 比較
 68        "match": [r"^比較$", r"^点差$", r"^差分$"],
 69        "action": lambda w: {"score_comparisons": True},
 70    },
 71    "order": {  # 順位出力
 72        "match": [r"^順位$"],
 73        "action": lambda w: {"order": True},
 74    },
 75    "results": {  # 戦績
 76        "match": [r"^戦績$"],
 77        "action": lambda w: {"game_results": True},
 78    },
 79    "versus": {  # 対戦結果
 80        "match": [r"^対戦結果$", r"^対戦$"],
 81        "action": lambda w: {"versus_matrix": True},
 82    },
 83    "statistics": {  # 統計
 84        "match": [r"^統計$"],
 85        "action": lambda w: {"statistics": True},
 86    },
 87    "rating": {  # レーティング
 88        "match": [r"^レート$", r"^レーティング$", r"^rate$", r"^ratings?$"],
 89        "action": lambda w: {"rating": True},
 90    },
 91    "verbose": {  # 詳細
 92        "match": [r"^詳細$", r"^verbose$"],
 93        "action": lambda w: {"verbose": True},
 94    },
 95    # --- 集計条件
 96    "ranked": {
 97        "match": [r"^(トップ|上位|top)(\d*)$"],
 98        "action": lambda w: {"ranked": w},
 99    },
100    "stipulated": {
101        "match": [r"^(規定数|規定打数)(\d*)$"],
102        "action": lambda w: {"stipulated": w}
103    },
104    "interval": {
105        "match": [r"^(期間|区間|区切リ?|interval)(\d*)$"],
106        "action": lambda w: {"interval": w}
107    },
108    # --- 集約 / 検索条件
109    "daily": {
110        "match": [r"^daily$", r"^日次$", r"^デイリー$"],
111        "action": lambda w: {"collection": "daily"},
112    },
113    "monthly": {
114        "match": [r"^monthly$", r"^月次$", r"^マンスリー$"],
115        "action": lambda w: {"collection": "monthly"},
116    },
117    "yearly": {
118        "match": [r"^yearly$", r"^年次$", r"^イヤーリー$"],
119        "action": lambda w: {"collection": "yearly"},
120    },
121    "collection": {
122        "match": [r"^全体$"],
123        "action": lambda w: {"collection": "all"}
124    },
125    "comment": {
126        "match": [r"^(コメント|comment)(.*)$"],
127        "action": lambda w: {"search_word": w},
128        "type": "sql",
129    },
130    "grouping": {
131        "match": [r"^(集約)(\d*)$"],
132        "action": lambda w: {"group_length": w}
133    },
134    "rule_version": {
135        "match": [r"^(ルール|rule)(.*)$"],
136        "action": lambda w: {"rule_version": w},
137        "type": "str",
138    },
139    "most_recent": {
140        "match": [r"^(直近)(\d*)$"],
141        "action": lambda w: {"target_count": w}
142    },
143    # --- 出力オプション
144    "format": {
145        "match": [r"^(csv|text|txt)$"],
146        "action": lambda w: {"format": w},
147        "type": "str",
148    },
149    "filename": {
150        "match": [r"^(filename:|ファイル名)(.*)$"],
151        "action": lambda w: {"filename": w},
152        "type": "filename",
153    },
154}
155
156
157class CommandParser:
158    """引数解析クラス"""
159
160    def __init__(self):
161        self.day_format = re.compile(r"^([0-9]{8}|[0-9/.-]{8,10})$")
162        """日付文字列判定用正規表現
163        - *yyyymmdd*
164        - *yyyy/mm/dd*, *yyyy/m/d*
165        - *yyyy-mm-dd*, *yyyy-m-d*
166        - *yyyy.mm.dd*, *yyyy.m.d*
167        """
168
169    @classmethod
170    def is_valid_command(cls, word: str) -> bool:
171        """引数がコマンド名と一致するか判定する
172
173        Args:
174            word (str): チェック文字列
175
176        Returns:
177            bool: 真偽
178        """
179
180        for cmd in COMMANDS.values():
181            for pattern in cmd["match"]:
182                m = re.match(pattern, word)
183                if m:
184                    return True
185                m = re.match(pattern, textutil.str_conv(word.lower(), "h2k"))
186                if m:
187                    return True
188
189        return False
190
191    def analysis_argument(self, argument: list[str]) -> ParsedCommand:
192        """コマンドライン引数を解析する
193
194        Args:
195            argument (list[str]): 引数
196
197        Returns:
198            ParsedCommand: 結果
199        """
200
201        ret: dict = {}
202        unknown: list = []
203        args: list = []
204        search_range: list = []
205
206        for keyword in argument:
207            check_word = textutil.str_conv(keyword.lower(), "h2k")
208            check_word = check_word.replace("無シ", "ナシ").replace("有リ", "アリ")
209
210            if re.match(r"^([0-9]{8}|[0-9/.-]{8,10})$", check_word):
211                try_day = pd.to_datetime(check_word, errors="coerce").to_pydatetime()
212                if not pd.isna(try_day):
213                    search_range.append(ExtDt(try_day))
214                    search_range.append(ExtDt(try_day) + {"hour": 23, "minute": 59, "second": 59, "microsecond": 999999})
215                continue
216
217            if check_word in ExtDt.valid_keywords():
218                search_range.append(check_word)
219                continue
220
221            for cmd in COMMANDS.values():
222                for pattern in cmd["match"]:
223                    m = re.match(pattern, keyword)
224                    if m:
225                        ret.update(self._parse_match(cmd, m))
226                        break
227                    m = re.match(pattern, check_word)
228                    if m:
229                        ret.update(self._parse_match(cmd, m))
230                        break
231                else:
232                    continue
233                break
234            else:
235                unknown.append(keyword)
236
237        return ParsedCommand(flags=ret, arguments=args, unknown=unknown, search_range=search_range)
238
239    def _parse_match(self, cmd: CommandSpec, m: re.Match) -> dict:
240        """コマンド名に一致したときの処理
241
242        Args:
243            cmd (CommandSpec): コマンドマップ
244            m (re.Match): Matchオブジェクト
245
246        Returns:
247            dict: 更新用辞書
248        """
249        ret: dict = {}
250
251        match len(m.groups()):
252            case 0:  # 完全一致: ^command$
253                ret.update(cmd["action"](m.group()))
254            case 1:  # 選択: ^(command1|command2|...)$
255                ret.update(cmd["action"](m.groups()[0]))
256            case 2:  # 引数あり: ^(command)(\d*)$
257                tmp = cmd["action"](m.groups())
258                if isinstance(tmp, dict):
259                    key = next(iter(tmp.keys()))
260                    val = str(tmp[key][1])
261                    if "" != val:
262                        match cmd.get("type"):
263                            case "str":
264                                ret.update({key: val})
265                            case "sql":
266                                ret.update({key: f"%{val}%"})
267                            case "filename":
268                                if re.search(r"^[\w\-\.]+$", val):
269                                    ret.update({key: val})
270                            case "int":
271                                ret.update({key: int(val)})
272                            case _:
273                                ret.update({key: int(val) if val.isdigit() else val})
274
275        return ret
276
277
278class MessageParser:
279    """メッセージ解析クラス"""
280    client: WebClient = WebClient()
281    """slack WebClient オブジェクト"""
282
283    def __init__(self, body: dict | None = None):
284        self.channel_id: str | None = str()
285        """ポストされたチャンネルのID"""
286        self.channel_type: str | None = str()
287        """チャンネルタイプ
288        - *channel*: 通常チャンネル
289        - *group*: プライベートチャンネル
290        - *im*: ダイレクトメッセージ
291        - *search_messages*: 検索API
292        """
293        self.user_id: str = str()
294        """ポストしたユーザのID"""
295        self.text: str | None = str()
296        """ポストされた文字列"""
297        self.event_ts: str = str()  # テキストのまま処理する
298        """タイムスタンプ"""
299        self.thread_ts: str = str()  # テキストのまま処理する
300        """スレッドになっている場合のスレッド元のタイムスタンプ"""
301        self.status: str = str()  # event subtype
302        """イベントステータス
303        - *message_append*: 新規ポスト
304        - *message_changed*: 編集
305        - *message_deleted*: 削除
306        """
307        self.keyword: str = str()
308        self.argument: list = []
309        self.updatable: bool = bool()
310        self.in_thread: bool = bool()
311
312        if isinstance(body, dict):
313            self.parser(body)
314
315    def parser(self, _body: dict):
316        """postされたメッセージをパースする
317
318        Args:
319            _body (dict): postされたデータ
320        """
321
322        logging.trace(_body)  # type: ignore
323
324        # 初期値
325        self.text = ""
326        self.channel_id = ""
327        self.user_id = ""
328        self.thread_ts = "0"
329        self.keyword = ""
330        self.argument = []
331
332        if _body.get("command") == g.cfg.setting.slash_command:  # スラッシュコマンド
333            _event = _body
334            if not self.channel_id:
335                if _body.get("channel_name") == "directmessage":
336                    self.channel_id = _body.get("channel_id", None)
337                else:
338                    self.channel_id = lookup.api.get_dm_channel_id(_body.get("user_id", ""))
339
340        if _body.get("container"):  # Homeタブ
341            self.user_id = _body["user"].get("id")
342            self.channel_id = lookup.api.get_dm_channel_id(self.user_id)
343            self.text = "dummy"
344
345        _event = self.get_event_attribute(_body)
346        self.user_id = _event.get("user", self.user_id)
347        self.event_ts = _event.get("ts", self.event_ts)
348        self.thread_ts = _event.get("thread_ts", self.thread_ts)
349        self.channel_type = self.get_channel_type(_body)
350
351        # スレッド内のポストか判定
352        if float(self.thread_ts):
353            self.in_thread = self.event_ts != self.thread_ts
354        else:
355            self.in_thread = False
356
357        if "text" in _event:
358            self.text = _event.get("text")
359            if self.text:  # 空文字以外はキーワードと引数に分割
360                self.keyword = self.text.split()[0]
361                self.argument = self.text.split()[1:]
362        else:  # text属性が見つからないときはログに出力
363            if not _event.get("text") and not _body.get("type") == "block_actions":
364                logging.error("text not found: %s", _body)
365
366        self.check_updatable()
367        logging.info("channel_id=%s, channel_type=%s", self.channel_id, self.channel_type)
368
369    def get_event_attribute(self, _body: dict) -> dict:
370        """レスポンスからevent属性を探索して返す
371
372        Args:
373            _body (dict): レスポンス内容
374
375        Returns:
376            dict: event属性
377        """
378
379        _event: dict = {}
380
381        if _body.get("command") == g.cfg.setting.slash_command:
382            _event = _body
383
384        if _body.get("event"):
385            if not self.channel_id:
386                if _body.get("channel_name") != "directmessage":
387                    self.channel_id = _body["event"].get("channel")
388                else:
389                    self.channel_id = lookup.api.get_dm_channel_id(_body.get("user_id", ""))
390
391            match _body["event"].get("subtype"):
392                case "message_changed":
393                    self.status = "message_changed"
394                    _event = _body["event"]["message"]
395                case "message_deleted":
396                    self.status = "message_deleted"
397                    _event = _body["event"]["previous_message"]
398                case "file_share":
399                    self.status = "message_append"
400                    _event = _body["event"]
401                case None:
402                    self.status = "message_append"
403                    _event = _body["event"]
404                case _:
405                    self.status = "message_append"
406                    _event = _body["event"]
407                    logging.info("unknown subtype: %s", _body)
408
409        return _event
410
411    def get_channel_type(self, _body: dict) -> str | None:
412        """レスポンスからchannel_typeを探索して返す
413
414        Args:
415            _body (dict): レスポンス内容
416
417        Returns:
418            str | None: channel_type
419        """
420
421        _channel_type: str | None = None
422
423        if _body.get("command") == g.cfg.setting.slash_command:
424            if _body.get("channel_name") == "directmessage":
425                _channel_type = "im"
426            else:
427                _channel_type = "channel"
428        else:
429            if _body.get("event"):
430                _channel_type = _body["event"].get("channel_type")
431
432        return _channel_type
433
434    def check_updatable(self):
435        """DB更新可能チャンネルのポストかチェックする"""
436        self.updatable = False
437
438        if g.cfg.db.channel_limitations:
439            if self.channel_id in g.cfg.db.channel_limitations:
440                self.updatable = True
441        else:  # リストが空なら全チャンネルが対象
442            match self.channel_type:
443                case "channel":  # public channel
444                    self.updatable = True
445                case "group":  # private channel
446                    self.updatable = True
447                case "im":  # direct message
448                    self.updatable = False
449                case "search_messages":
450                    self.updatable = True
451                case _:
452                    self.updatable = True
@dataclass
class ParsedCommand:
21@dataclass
22class ParsedCommand:
23    """コマンド解析結果"""
24    flags: dict[str, Any]
25    arguments: list[str]
26    unknown: list[str]
27    search_range: list["ExtDt"]

コマンド解析結果

ParsedCommand( flags: dict[str, typing.Any], arguments: list[str], unknown: list[str], search_range: list[cls.timekit.ExtendedDatetime])
flags: dict[str, typing.Any]
arguments: list[str]
unknown: list[str]
search_range: list[cls.timekit.ExtendedDatetime]
CommandsDict = dict[str, cls.types.CommandSpec]
COMMANDS: dict[str, cls.types.CommandSpec] = {'guest': {'match': ['^ゲストナシ$', '^ゲストアリ$', '^ゲスト無効$'], 'action': <function <lambda>>}, 'anonymous': {'match': ['^匿名$', '^anonymous$'], 'action': <function <lambda>>}, 'individual': {'match': ['^個人$', '^個人成績$'], 'action': <function <lambda>>}, 'team': {'match': ['^チーム$', '^チーム成績$', '^team$'], 'action': <function <lambda>>}, 'all_player': {'match': ['^全員$', '^all$'], 'action': <function <lambda>>}, 'a': {'match': ['^(チーム同卓アリ|コンビアリ|同士討チ)$'], 'action': <function <lambda>>}, 'b': {'match': ['^(チーム同卓ナシ|コンビナシ)$'], 'action': <function <lambda>>}, 'score_comparisons': {'match': ['^比較$', '^点差$', '^差分$'], 'action': <function <lambda>>}, 'order': {'match': ['^順位$'], 'action': <function <lambda>>}, 'results': {'match': ['^戦績$'], 'action': <function <lambda>>}, 'versus': {'match': ['^対戦結果$', '^対戦$'], 'action': <function <lambda>>}, 'statistics': {'match': ['^統計$'], 'action': <function <lambda>>}, 'rating': {'match': ['^レート$', '^レーティング$', '^rate$', '^ratings?$'], 'action': <function <lambda>>}, 'verbose': {'match': ['^詳細$', '^verbose$'], 'action': <function <lambda>>}, 'ranked': {'match': ['^(トップ|上位|top)(\\d*)$'], 'action': <function <lambda>>}, 'stipulated': {'match': ['^(規定数|規定打数)(\\d*)$'], 'action': <function <lambda>>}, 'interval': {'match': ['^(期間|区間|区切リ?|interval)(\\d*)$'], 'action': <function <lambda>>}, 'daily': {'match': ['^daily$', '^日次$', '^デイリー$'], 'action': <function <lambda>>}, 'monthly': {'match': ['^monthly$', '^月次$', '^マンスリー$'], 'action': <function <lambda>>}, 'yearly': {'match': ['^yearly$', '^年次$', '^イヤーリー$'], 'action': <function <lambda>>}, 'collection': {'match': ['^全体$'], 'action': <function <lambda>>}, 'comment': {'match': ['^(コメント|comment)(.*)$'], 'action': <function <lambda>>, 'type': 'sql'}, 'grouping': {'match': ['^(集約)(\\d*)$'], 'action': <function <lambda>>}, 'rule_version': {'match': ['^(ルール|rule)(.*)$'], 'action': <function <lambda>>, 'type': 'str'}, 'most_recent': {'match': ['^(直近)(\\d*)$'], 'action': <function <lambda>>}, 'format': {'match': ['^(csv|text|txt)$'], 'action': <function <lambda>>, 'type': 'str'}, 'filename': {'match': ['^(filename:|ファイル名)(.*)$'], 'action': <function <lambda>>, 'type': 'filename'}}
class CommandParser:
158class CommandParser:
159    """引数解析クラス"""
160
161    def __init__(self):
162        self.day_format = re.compile(r"^([0-9]{8}|[0-9/.-]{8,10})$")
163        """日付文字列判定用正規表現
164        - *yyyymmdd*
165        - *yyyy/mm/dd*, *yyyy/m/d*
166        - *yyyy-mm-dd*, *yyyy-m-d*
167        - *yyyy.mm.dd*, *yyyy.m.d*
168        """
169
170    @classmethod
171    def is_valid_command(cls, word: str) -> bool:
172        """引数がコマンド名と一致するか判定する
173
174        Args:
175            word (str): チェック文字列
176
177        Returns:
178            bool: 真偽
179        """
180
181        for cmd in COMMANDS.values():
182            for pattern in cmd["match"]:
183                m = re.match(pattern, word)
184                if m:
185                    return True
186                m = re.match(pattern, textutil.str_conv(word.lower(), "h2k"))
187                if m:
188                    return True
189
190        return False
191
192    def analysis_argument(self, argument: list[str]) -> ParsedCommand:
193        """コマンドライン引数を解析する
194
195        Args:
196            argument (list[str]): 引数
197
198        Returns:
199            ParsedCommand: 結果
200        """
201
202        ret: dict = {}
203        unknown: list = []
204        args: list = []
205        search_range: list = []
206
207        for keyword in argument:
208            check_word = textutil.str_conv(keyword.lower(), "h2k")
209            check_word = check_word.replace("無シ", "ナシ").replace("有リ", "アリ")
210
211            if re.match(r"^([0-9]{8}|[0-9/.-]{8,10})$", check_word):
212                try_day = pd.to_datetime(check_word, errors="coerce").to_pydatetime()
213                if not pd.isna(try_day):
214                    search_range.append(ExtDt(try_day))
215                    search_range.append(ExtDt(try_day) + {"hour": 23, "minute": 59, "second": 59, "microsecond": 999999})
216                continue
217
218            if check_word in ExtDt.valid_keywords():
219                search_range.append(check_word)
220                continue
221
222            for cmd in COMMANDS.values():
223                for pattern in cmd["match"]:
224                    m = re.match(pattern, keyword)
225                    if m:
226                        ret.update(self._parse_match(cmd, m))
227                        break
228                    m = re.match(pattern, check_word)
229                    if m:
230                        ret.update(self._parse_match(cmd, m))
231                        break
232                else:
233                    continue
234                break
235            else:
236                unknown.append(keyword)
237
238        return ParsedCommand(flags=ret, arguments=args, unknown=unknown, search_range=search_range)
239
240    def _parse_match(self, cmd: CommandSpec, m: re.Match) -> dict:
241        """コマンド名に一致したときの処理
242
243        Args:
244            cmd (CommandSpec): コマンドマップ
245            m (re.Match): Matchオブジェクト
246
247        Returns:
248            dict: 更新用辞書
249        """
250        ret: dict = {}
251
252        match len(m.groups()):
253            case 0:  # 完全一致: ^command$
254                ret.update(cmd["action"](m.group()))
255            case 1:  # 選択: ^(command1|command2|...)$
256                ret.update(cmd["action"](m.groups()[0]))
257            case 2:  # 引数あり: ^(command)(\d*)$
258                tmp = cmd["action"](m.groups())
259                if isinstance(tmp, dict):
260                    key = next(iter(tmp.keys()))
261                    val = str(tmp[key][1])
262                    if "" != val:
263                        match cmd.get("type"):
264                            case "str":
265                                ret.update({key: val})
266                            case "sql":
267                                ret.update({key: f"%{val}%"})
268                            case "filename":
269                                if re.search(r"^[\w\-\.]+$", val):
270                                    ret.update({key: val})
271                            case "int":
272                                ret.update({key: int(val)})
273                            case _:
274                                ret.update({key: int(val) if val.isdigit() else val})
275
276        return ret

引数解析クラス

day_format

日付文字列判定用正規表現

  • yyyymmdd
  • yyyy/mm/dd, yyyy/m/d
  • yyyy-mm-dd, yyyy-m-d
  • yyyy.mm.dd, yyyy.m.d
@classmethod
def is_valid_command(cls, word: str) -> bool:
170    @classmethod
171    def is_valid_command(cls, word: str) -> bool:
172        """引数がコマンド名と一致するか判定する
173
174        Args:
175            word (str): チェック文字列
176
177        Returns:
178            bool: 真偽
179        """
180
181        for cmd in COMMANDS.values():
182            for pattern in cmd["match"]:
183                m = re.match(pattern, word)
184                if m:
185                    return True
186                m = re.match(pattern, textutil.str_conv(word.lower(), "h2k"))
187                if m:
188                    return True
189
190        return False

引数がコマンド名と一致するか判定する

Arguments:
  • word (str): チェック文字列
Returns:

bool: 真偽

def analysis_argument(self, argument: list[str]) -> ParsedCommand:
192    def analysis_argument(self, argument: list[str]) -> ParsedCommand:
193        """コマンドライン引数を解析する
194
195        Args:
196            argument (list[str]): 引数
197
198        Returns:
199            ParsedCommand: 結果
200        """
201
202        ret: dict = {}
203        unknown: list = []
204        args: list = []
205        search_range: list = []
206
207        for keyword in argument:
208            check_word = textutil.str_conv(keyword.lower(), "h2k")
209            check_word = check_word.replace("無シ", "ナシ").replace("有リ", "アリ")
210
211            if re.match(r"^([0-9]{8}|[0-9/.-]{8,10})$", check_word):
212                try_day = pd.to_datetime(check_word, errors="coerce").to_pydatetime()
213                if not pd.isna(try_day):
214                    search_range.append(ExtDt(try_day))
215                    search_range.append(ExtDt(try_day) + {"hour": 23, "minute": 59, "second": 59, "microsecond": 999999})
216                continue
217
218            if check_word in ExtDt.valid_keywords():
219                search_range.append(check_word)
220                continue
221
222            for cmd in COMMANDS.values():
223                for pattern in cmd["match"]:
224                    m = re.match(pattern, keyword)
225                    if m:
226                        ret.update(self._parse_match(cmd, m))
227                        break
228                    m = re.match(pattern, check_word)
229                    if m:
230                        ret.update(self._parse_match(cmd, m))
231                        break
232                else:
233                    continue
234                break
235            else:
236                unknown.append(keyword)
237
238        return ParsedCommand(flags=ret, arguments=args, unknown=unknown, search_range=search_range)

コマンドライン引数を解析する

Arguments:
  • argument (list[str]): 引数
Returns:

ParsedCommand: 結果

class MessageParser:
279class MessageParser:
280    """メッセージ解析クラス"""
281    client: WebClient = WebClient()
282    """slack WebClient オブジェクト"""
283
284    def __init__(self, body: dict | None = None):
285        self.channel_id: str | None = str()
286        """ポストされたチャンネルのID"""
287        self.channel_type: str | None = str()
288        """チャンネルタイプ
289        - *channel*: 通常チャンネル
290        - *group*: プライベートチャンネル
291        - *im*: ダイレクトメッセージ
292        - *search_messages*: 検索API
293        """
294        self.user_id: str = str()
295        """ポストしたユーザのID"""
296        self.text: str | None = str()
297        """ポストされた文字列"""
298        self.event_ts: str = str()  # テキストのまま処理する
299        """タイムスタンプ"""
300        self.thread_ts: str = str()  # テキストのまま処理する
301        """スレッドになっている場合のスレッド元のタイムスタンプ"""
302        self.status: str = str()  # event subtype
303        """イベントステータス
304        - *message_append*: 新規ポスト
305        - *message_changed*: 編集
306        - *message_deleted*: 削除
307        """
308        self.keyword: str = str()
309        self.argument: list = []
310        self.updatable: bool = bool()
311        self.in_thread: bool = bool()
312
313        if isinstance(body, dict):
314            self.parser(body)
315
316    def parser(self, _body: dict):
317        """postされたメッセージをパースする
318
319        Args:
320            _body (dict): postされたデータ
321        """
322
323        logging.trace(_body)  # type: ignore
324
325        # 初期値
326        self.text = ""
327        self.channel_id = ""
328        self.user_id = ""
329        self.thread_ts = "0"
330        self.keyword = ""
331        self.argument = []
332
333        if _body.get("command") == g.cfg.setting.slash_command:  # スラッシュコマンド
334            _event = _body
335            if not self.channel_id:
336                if _body.get("channel_name") == "directmessage":
337                    self.channel_id = _body.get("channel_id", None)
338                else:
339                    self.channel_id = lookup.api.get_dm_channel_id(_body.get("user_id", ""))
340
341        if _body.get("container"):  # Homeタブ
342            self.user_id = _body["user"].get("id")
343            self.channel_id = lookup.api.get_dm_channel_id(self.user_id)
344            self.text = "dummy"
345
346        _event = self.get_event_attribute(_body)
347        self.user_id = _event.get("user", self.user_id)
348        self.event_ts = _event.get("ts", self.event_ts)
349        self.thread_ts = _event.get("thread_ts", self.thread_ts)
350        self.channel_type = self.get_channel_type(_body)
351
352        # スレッド内のポストか判定
353        if float(self.thread_ts):
354            self.in_thread = self.event_ts != self.thread_ts
355        else:
356            self.in_thread = False
357
358        if "text" in _event:
359            self.text = _event.get("text")
360            if self.text:  # 空文字以外はキーワードと引数に分割
361                self.keyword = self.text.split()[0]
362                self.argument = self.text.split()[1:]
363        else:  # text属性が見つからないときはログに出力
364            if not _event.get("text") and not _body.get("type") == "block_actions":
365                logging.error("text not found: %s", _body)
366
367        self.check_updatable()
368        logging.info("channel_id=%s, channel_type=%s", self.channel_id, self.channel_type)
369
370    def get_event_attribute(self, _body: dict) -> dict:
371        """レスポンスからevent属性を探索して返す
372
373        Args:
374            _body (dict): レスポンス内容
375
376        Returns:
377            dict: event属性
378        """
379
380        _event: dict = {}
381
382        if _body.get("command") == g.cfg.setting.slash_command:
383            _event = _body
384
385        if _body.get("event"):
386            if not self.channel_id:
387                if _body.get("channel_name") != "directmessage":
388                    self.channel_id = _body["event"].get("channel")
389                else:
390                    self.channel_id = lookup.api.get_dm_channel_id(_body.get("user_id", ""))
391
392            match _body["event"].get("subtype"):
393                case "message_changed":
394                    self.status = "message_changed"
395                    _event = _body["event"]["message"]
396                case "message_deleted":
397                    self.status = "message_deleted"
398                    _event = _body["event"]["previous_message"]
399                case "file_share":
400                    self.status = "message_append"
401                    _event = _body["event"]
402                case None:
403                    self.status = "message_append"
404                    _event = _body["event"]
405                case _:
406                    self.status = "message_append"
407                    _event = _body["event"]
408                    logging.info("unknown subtype: %s", _body)
409
410        return _event
411
412    def get_channel_type(self, _body: dict) -> str | None:
413        """レスポンスからchannel_typeを探索して返す
414
415        Args:
416            _body (dict): レスポンス内容
417
418        Returns:
419            str | None: channel_type
420        """
421
422        _channel_type: str | None = None
423
424        if _body.get("command") == g.cfg.setting.slash_command:
425            if _body.get("channel_name") == "directmessage":
426                _channel_type = "im"
427            else:
428                _channel_type = "channel"
429        else:
430            if _body.get("event"):
431                _channel_type = _body["event"].get("channel_type")
432
433        return _channel_type
434
435    def check_updatable(self):
436        """DB更新可能チャンネルのポストかチェックする"""
437        self.updatable = False
438
439        if g.cfg.db.channel_limitations:
440            if self.channel_id in g.cfg.db.channel_limitations:
441                self.updatable = True
442        else:  # リストが空なら全チャンネルが対象
443            match self.channel_type:
444                case "channel":  # public channel
445                    self.updatable = True
446                case "group":  # private channel
447                    self.updatable = True
448                case "im":  # direct message
449                    self.updatable = False
450                case "search_messages":
451                    self.updatable = True
452                case _:
453                    self.updatable = True

メッセージ解析クラス

MessageParser(body: dict | None = None)
284    def __init__(self, body: dict | None = None):
285        self.channel_id: str | None = str()
286        """ポストされたチャンネルのID"""
287        self.channel_type: str | None = str()
288        """チャンネルタイプ
289        - *channel*: 通常チャンネル
290        - *group*: プライベートチャンネル
291        - *im*: ダイレクトメッセージ
292        - *search_messages*: 検索API
293        """
294        self.user_id: str = str()
295        """ポストしたユーザのID"""
296        self.text: str | None = str()
297        """ポストされた文字列"""
298        self.event_ts: str = str()  # テキストのまま処理する
299        """タイムスタンプ"""
300        self.thread_ts: str = str()  # テキストのまま処理する
301        """スレッドになっている場合のスレッド元のタイムスタンプ"""
302        self.status: str = str()  # event subtype
303        """イベントステータス
304        - *message_append*: 新規ポスト
305        - *message_changed*: 編集
306        - *message_deleted*: 削除
307        """
308        self.keyword: str = str()
309        self.argument: list = []
310        self.updatable: bool = bool()
311        self.in_thread: bool = bool()
312
313        if isinstance(body, dict):
314            self.parser(body)
client: slack_sdk.web.client.WebClient = <slack_sdk.web.client.WebClient object>

slack WebClient オブジェクト

channel_id: str | None

ポストされたチャンネルのID

channel_type: str | None

チャンネルタイプ

  • channel: 通常チャンネル
  • group: プライベートチャンネル
  • im: ダイレクトメッセージ
  • search_messages: 検索API
user_id: str

ポストしたユーザのID

text: str | None

ポストされた文字列

event_ts: str

タイムスタンプ

thread_ts: str

スレッドになっている場合のスレッド元のタイムスタンプ

status: str

イベントステータス

  • message_append: 新規ポスト
  • message_changed: 編集
  • message_deleted: 削除
keyword: str
argument: list
updatable: bool
in_thread: bool
def parser(self, _body: dict):
316    def parser(self, _body: dict):
317        """postされたメッセージをパースする
318
319        Args:
320            _body (dict): postされたデータ
321        """
322
323        logging.trace(_body)  # type: ignore
324
325        # 初期値
326        self.text = ""
327        self.channel_id = ""
328        self.user_id = ""
329        self.thread_ts = "0"
330        self.keyword = ""
331        self.argument = []
332
333        if _body.get("command") == g.cfg.setting.slash_command:  # スラッシュコマンド
334            _event = _body
335            if not self.channel_id:
336                if _body.get("channel_name") == "directmessage":
337                    self.channel_id = _body.get("channel_id", None)
338                else:
339                    self.channel_id = lookup.api.get_dm_channel_id(_body.get("user_id", ""))
340
341        if _body.get("container"):  # Homeタブ
342            self.user_id = _body["user"].get("id")
343            self.channel_id = lookup.api.get_dm_channel_id(self.user_id)
344            self.text = "dummy"
345
346        _event = self.get_event_attribute(_body)
347        self.user_id = _event.get("user", self.user_id)
348        self.event_ts = _event.get("ts", self.event_ts)
349        self.thread_ts = _event.get("thread_ts", self.thread_ts)
350        self.channel_type = self.get_channel_type(_body)
351
352        # スレッド内のポストか判定
353        if float(self.thread_ts):
354            self.in_thread = self.event_ts != self.thread_ts
355        else:
356            self.in_thread = False
357
358        if "text" in _event:
359            self.text = _event.get("text")
360            if self.text:  # 空文字以外はキーワードと引数に分割
361                self.keyword = self.text.split()[0]
362                self.argument = self.text.split()[1:]
363        else:  # text属性が見つからないときはログに出力
364            if not _event.get("text") and not _body.get("type") == "block_actions":
365                logging.error("text not found: %s", _body)
366
367        self.check_updatable()
368        logging.info("channel_id=%s, channel_type=%s", self.channel_id, self.channel_type)

postされたメッセージをパースする

Arguments:
  • _body (dict): postされたデータ
def get_event_attribute(self, _body: dict) -> dict:
370    def get_event_attribute(self, _body: dict) -> dict:
371        """レスポンスからevent属性を探索して返す
372
373        Args:
374            _body (dict): レスポンス内容
375
376        Returns:
377            dict: event属性
378        """
379
380        _event: dict = {}
381
382        if _body.get("command") == g.cfg.setting.slash_command:
383            _event = _body
384
385        if _body.get("event"):
386            if not self.channel_id:
387                if _body.get("channel_name") != "directmessage":
388                    self.channel_id = _body["event"].get("channel")
389                else:
390                    self.channel_id = lookup.api.get_dm_channel_id(_body.get("user_id", ""))
391
392            match _body["event"].get("subtype"):
393                case "message_changed":
394                    self.status = "message_changed"
395                    _event = _body["event"]["message"]
396                case "message_deleted":
397                    self.status = "message_deleted"
398                    _event = _body["event"]["previous_message"]
399                case "file_share":
400                    self.status = "message_append"
401                    _event = _body["event"]
402                case None:
403                    self.status = "message_append"
404                    _event = _body["event"]
405                case _:
406                    self.status = "message_append"
407                    _event = _body["event"]
408                    logging.info("unknown subtype: %s", _body)
409
410        return _event

レスポンスからevent属性を探索して返す

Arguments:
  • _body (dict): レスポンス内容
Returns:

dict: event属性

def get_channel_type(self, _body: dict) -> str | None:
412    def get_channel_type(self, _body: dict) -> str | None:
413        """レスポンスからchannel_typeを探索して返す
414
415        Args:
416            _body (dict): レスポンス内容
417
418        Returns:
419            str | None: channel_type
420        """
421
422        _channel_type: str | None = None
423
424        if _body.get("command") == g.cfg.setting.slash_command:
425            if _body.get("channel_name") == "directmessage":
426                _channel_type = "im"
427            else:
428                _channel_type = "channel"
429        else:
430            if _body.get("event"):
431                _channel_type = _body["event"].get("channel_type")
432
433        return _channel_type

レスポンスからchannel_typeを探索して返す

Arguments:
  • _body (dict): レスポンス内容
Returns:

str | None: channel_type

def check_updatable(self):
435    def check_updatable(self):
436        """DB更新可能チャンネルのポストかチェックする"""
437        self.updatable = False
438
439        if g.cfg.db.channel_limitations:
440            if self.channel_id in g.cfg.db.channel_limitations:
441                self.updatable = True
442        else:  # リストが空なら全チャンネルが対象
443            match self.channel_type:
444                case "channel":  # public channel
445                    self.updatable = True
446                case "group":  # private channel
447                    self.updatable = True
448                case "im":  # direct message
449                    self.updatable = False
450                case "search_messages":
451                    self.updatable = True
452                case _:
453                    self.updatable = True

DB更新可能チャンネルのポストかチェックする