|
4 | 4 | from traceback import format_exc
|
5 | 5 | from typing import Dict, Type
|
6 | 6 |
|
| 7 | +from torequests.utils import ttime |
| 8 | + |
7 | 9 | from .utils import ensure_await_result
|
8 | 10 |
|
9 | 11 |
|
| 12 | +class CallbackHandlerBase(ABC): |
| 13 | + logger = getLogger('watchdogs') |
| 14 | + |
| 15 | + def __init__(self): |
| 16 | + # lazy init object |
| 17 | + self.callbacks_dict: Dict[str, Type[Callback]] = {} |
| 18 | + for cls in Callback.__subclasses__(): |
| 19 | + try: |
| 20 | + assert cls.name is not None |
| 21 | + cls.doc = cls.doc or cls.__doc__ |
| 22 | + self.callbacks_dict[cls.name] = cls |
| 23 | + except Exception as err: |
| 24 | + self.logger.error(f'{cls} registers failed: {err!r}') |
| 25 | + self.workers = {cb.name: cb.doc for cb in self.callbacks_dict.values()} |
| 26 | + |
| 27 | + @abstractmethod |
| 28 | + async def callback(self, task): |
| 29 | + pass |
| 30 | + |
| 31 | + def get_callback(self, name): |
| 32 | + obj = self.callbacks_dict.get(name) |
| 33 | + if not obj: |
| 34 | + # not found callback |
| 35 | + return None |
| 36 | + if not isinstance(obj, Callback): |
| 37 | + # here for lazy init |
| 38 | + obj = obj() |
| 39 | + self.callbacks_dict[name] = obj |
| 40 | + return obj |
| 41 | + |
| 42 | + |
| 43 | +class CallbackHandler(CallbackHandlerBase): |
| 44 | + |
| 45 | + def __init__(self): |
| 46 | + super().__init__() |
| 47 | + |
| 48 | + async def callback(self, task): |
| 49 | + custom_info: str = task.custom_info.strip() |
| 50 | + name = custom_info.split(':', 1)[0] |
| 51 | + cb = self.get_callback(name) or self.get_callback('') |
| 52 | + if not cb: |
| 53 | + # not found callback, ignore |
| 54 | + return |
| 55 | + try: |
| 56 | + call_result = await ensure_await_result(cb.callback(task)) |
| 57 | + self.logger.info( |
| 58 | + f'{cb.name or "default"} callback({custom_info}) for task {task.name} {call_result}: ' |
| 59 | + ) |
| 60 | + except Exception: |
| 61 | + self.logger.error( |
| 62 | + f'{cb.name or "default"} callback({custom_info}) for task {task.name} error:\n{format_exc()}' |
| 63 | + ) |
| 64 | + |
| 65 | + |
10 | 66 | class Callback(ABC):
|
11 | 67 | """
|
12 | 68 | Constraint: Callback object should has this attribute:
|
@@ -58,66 +114,76 @@ async def callback(self, task):
|
58 | 114 | if not key or not key.strip():
|
59 | 115 | continue
|
60 | 116 | key = key.strip()
|
61 |
| - r = await self.req.post( |
62 |
| - f'https://sc.ftqq.com/{key}.send', |
63 |
| - data={ |
64 |
| - 'text': title, |
65 |
| - 'desp': body |
66 |
| - }) |
| 117 | + r = await self.req.post(f'https://sc.ftqq.com/{key}.send', |
| 118 | + data={ |
| 119 | + 'text': title, |
| 120 | + 'desp': body |
| 121 | + }) |
67 | 122 | self.logger.info(f'ServerChanCallback ({key}): {r.text}')
|
68 | 123 | oks.append((key, bool(r)))
|
69 | 124 | return f'{len(oks)} sended, {oks}'
|
70 | 125 |
|
71 | 126 |
|
72 |
| -class CallbackHandlerBase(ABC): |
73 |
| - logger = getLogger('watchdogs') |
74 |
| - |
75 |
| - def __init__(self): |
76 |
| - # lazy init object |
77 |
| - self.callbacks_dict: Dict[str, Type[Callback]] = {} |
78 |
| - for cls in Callback.__subclasses__(): |
79 |
| - try: |
80 |
| - assert cls.name is not None |
81 |
| - cls.doc = cls.doc or cls.__doc__ |
82 |
| - self.callbacks_dict[cls.name] = cls |
83 |
| - except Exception as err: |
84 |
| - self.logger.error(f'{cls} registers failed: {err!r}') |
85 |
| - self.workers = {cb.name: cb.doc for cb in self.callbacks_dict.values()} |
86 |
| - |
87 |
| - @abstractmethod |
88 |
| - async def callback(self, task): |
89 |
| - pass |
90 |
| - |
91 |
| - def get_callback(self, name): |
92 |
| - obj = self.callbacks_dict.get(name) |
93 |
| - if not obj: |
94 |
| - # not found callback |
95 |
| - return None |
96 |
| - if not isinstance(obj, Callback): |
97 |
| - # here for lazy init |
98 |
| - obj = obj() |
99 |
| - self.callbacks_dict[name] = obj |
100 |
| - return obj |
| 127 | +class DingTalkCallback(Callback): |
| 128 | + """ |
| 129 | +DingDing robot notify toolkit. Will auto check msg type as text / card. |
101 | 130 |
|
| 131 | + 1. Create a group. |
| 132 | + 2. Create a robot which contains word ":" |
| 133 | + 3. Set the task.custom_info as: dingding:{access_token} |
102 | 134 |
|
103 |
| -class CallbackHandler(CallbackHandlerBase): |
| 135 | + Doc: https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq/e9d991e2 |
| 136 | +""" |
| 137 | + name = "dingding" |
104 | 138 |
|
105 | 139 | def __init__(self):
|
106 |
| - super().__init__() |
| 140 | + from torequests.dummy import Requests |
| 141 | + self.req = Requests() |
| 142 | + |
| 143 | + def make_data(self, task): |
| 144 | + latest_result = loads(task.latest_result or '{}') |
| 145 | + title = latest_result.get('title') or '' |
| 146 | + url = latest_result.get('url') or task.origin_url |
| 147 | + text = latest_result.get('text') or '' |
| 148 | + cover = latest_result.get('cover') or '' |
| 149 | + if cover: |
| 150 | + text = f'\n{text}' |
| 151 | + if url or cover: |
| 152 | + # markdown |
| 153 | + title = f'# {task.name}: {title}\n> {ttime()}' |
| 154 | + return { |
| 155 | + "actionCard": { |
| 156 | + "title": title, |
| 157 | + "text": f'{title}\n\n{text}', |
| 158 | + "singleTitle": "Read More", |
| 159 | + "singleURL": url |
| 160 | + }, |
| 161 | + "msgtype": "actionCard" |
| 162 | + } |
| 163 | + return { |
| 164 | + "msgtype": "text", |
| 165 | + "text": { |
| 166 | + "content": f"{task.name}: {title}\n{text}" |
| 167 | + } |
| 168 | + } |
107 | 169 |
|
108 | 170 | async def callback(self, task):
|
109 |
| - custom_info: str = task.custom_info.strip() |
110 |
| - name = custom_info.split(':', 1)[0] |
111 |
| - cb = self.get_callback(name) or self.get_callback('') |
112 |
| - if not cb: |
113 |
| - # not found callback, ignore |
114 |
| - return |
115 |
| - try: |
116 |
| - call_result = await ensure_await_result(cb.callback(task)) |
117 |
| - self.logger.info( |
118 |
| - f'{cb.name or "default"} callback({custom_info}) for task {task.name} {call_result}: ' |
119 |
| - ) |
120 |
| - except Exception: |
121 |
| - self.logger.error( |
122 |
| - f'{cb.name or "default"} callback({custom_info}) for task {task.name} error:\n{format_exc()}' |
| 171 | + name, arg = task.custom_info.split(':', 1) |
| 172 | + if not arg: |
| 173 | + raise ValueError( |
| 174 | + f'{task.name}: custom_info `{task.custom_info}` missing args after `:`' |
123 | 175 | )
|
| 176 | + |
| 177 | + data = self.make_data(task) |
| 178 | + oks = [] |
| 179 | + for access_token in set(arg.strip().split()): |
| 180 | + if not access_token or not access_token.strip(): |
| 181 | + continue |
| 182 | + access_token = access_token.strip() |
| 183 | + r = await self.req.post( |
| 184 | + f'https://oapi.dingtalk.com/robot/send?access_token={access_token}', |
| 185 | + json=data) |
| 186 | + self.logger.info( |
| 187 | + f'{self.__class__.__name__} ({access_token}): {r.text}') |
| 188 | + oks.append((access_token, bool(r))) |
| 189 | + return f'{len(oks)} sended, {oks}' |
0 commit comments