Skip to content

Commit e787046

Browse files
authored
Merge pull request #64 from ClericPy/dev
1.8.0
2 parents 339a904 + b4d94b0 commit e787046

File tree

10 files changed

+383
-300
lines changed

10 files changed

+383
-300
lines changed

watchdogs/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
from .config import Config
44
from .main import init_app
55

6-
__version__ = '1.7.2'
6+
__version__ = '1.8.0'
77
__all__ = ['Config', 'init_app']
88
logging.getLogger('watchdogs').addHandler(logging.NullHandler())

watchdogs/app.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ async def rss(request: Request,
433433
logger.error(f'latest_result is list: {latest_result}')
434434
link: str = latest_result.get('url') or task['origin_url']
435435
description: str = latest_result.get('text') or ''
436-
title: str = f'{task["name"]}#{description[:80]}'
436+
title: str = f'{task["name"]}#{latest_result.get("title", description[:80])}'
437437
item: dict = {
438438
'title': title,
439439
'link': link,
@@ -479,9 +479,10 @@ async def lite(request: Request,
479479
now = datetime.now()
480480
for task in tasks:
481481
result = loads(task['latest_result'] or '{}')
482-
# for cache...
482+
# set / get cache from task
483483
task['url'] = task.get('url') or result.get('url') or task['origin_url']
484-
task['text'] = task.get('text') or result.get('text') or ''
484+
task['text'] = task.get('text') or result.get('title') or result.get(
485+
'text') or ''
485486
task['timeago'] = timeago(
486487
(now - task['last_change_time']).total_seconds(),
487488
1,
@@ -504,4 +505,7 @@ async def lite(request: Request,
504505
else:
505506
last_page_url = ''
506507
context['last_page_url'] = last_page_url
508+
quoted_tag = quote_plus(tag)
509+
rss_sign = Config.get_sign('/rss', f'tag={quoted_tag}')[1]
510+
context['rss_url'] = f'/rss?tag={quoted_tag}&sign={rss_sign}'
507511
return templates.TemplateResponse("lite.html", context=context)

watchdogs/callbacks.py

+117-51
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,65 @@
44
from traceback import format_exc
55
from typing import Dict, Type
66

7+
from torequests.utils import ttime
8+
79
from .utils import ensure_await_result
810

911

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+
1066
class Callback(ABC):
1167
"""
1268
Constraint: Callback object should has this attribute:
@@ -58,66 +114,76 @@ async def callback(self, task):
58114
if not key or not key.strip():
59115
continue
60116
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+
})
67122
self.logger.info(f'ServerChanCallback ({key}): {r.text}')
68123
oks.append((key, bool(r)))
69124
return f'{len(oks)} sended, {oks}'
70125

71126

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.
101130
131+
1. Create a group.
132+
2. Create a robot which contains word ":"
133+
3. Set the task.custom_info as: dingding:{access_token}
102134
103-
class CallbackHandler(CallbackHandlerBase):
135+
Doc: https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq/e9d991e2
136+
"""
137+
name = "dingding"
104138

105139
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'![cover]({cover})\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+
}
107169

108170
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 `:`'
123175
)
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}'

watchdogs/config.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,14 @@ async def auth_checker(request: Request, call_next):
7676
# try checking sign
7777
given_sign, valid_sign = Config.get_sign(path, query_string)
7878
if given_sign == valid_sign:
79+
# sign checking pass
7980
return await call_next(request)
81+
# try check cookie
8082
if not Config.watchdog_auth or Config.watchdog_auth == request.cookies.get(
8183
'watchdog_auth', ''):
82-
# no watchdog_auth or cookie is valid
84+
# valid cookie, or no watchdog_auth checker
8385
return await call_next(request)
86+
# not pass either checker, refused
8487
if query_has_sign:
8588
# request with sign will not redirect
8689
return JSONResponse(
@@ -90,6 +93,7 @@ async def auth_checker(request: Request, call_next):
9093
},
9194
)
9295
else:
96+
# bad cookie, reset the watchdog_auth cookie as null
9397
resp = RedirectResponse(
9498
f'/auth?redirect={quote_plus(request.scope["path"])}', 302)
9599
resp.set_cookie('watchdog_auth', '')
@@ -181,7 +185,7 @@ class Config:
181185
custom_tabs: List[Dict] = []
182186
COLLATION: str = None
183187
cookie_max_age = 86400 * 7
184-
default_page_size = 15
188+
default_page_size = 20
185189

186190
@classmethod
187191
def add_custom_tabs(cls, label, url, name=None, desc=None):

watchdogs/crawler.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ async def crawl(task: Task):
123123
result_list = None
124124
else:
125125
if len(crawl_result) == 1:
126-
# chain result for __request__ which fetch a new request
126+
# crawl_result schema: {rule_name: list_or_dict}
127127
formated_result = get_watchdog_result(
128128
item=crawl_result.popitem()[1])
129129
if formated_result == {'text': 'text not found'}:
@@ -138,7 +138,7 @@ async def crawl(task: Task):
138138
# use force crawl one web UI for more log
139139
logger.info(f'{task.name} Crawl success: {result_list}'[:150])
140140
else:
141-
error = 'Invalid crawl_result schema: {rule_name: [{"text": "xxx", "url": "xxx"}]}, but given %r' % crawl_result
141+
error = 'Invalid crawl_result against schema {rule_name: [{"text": "Required", "url": "Optional", "__key__": "Optional"}]}, given is %r' % crawl_result
142142
logger.error(f'{task.name}: {error}')
143143
result_list = [{"text": error}]
144144
return task, error, result_list
@@ -212,10 +212,14 @@ async def _crawl_once(task_name: Optional[str] = None, chunk_size: int = 20):
212212
# compare latest_result and new list
213213
# later first, just like the saved result_list sortings
214214
old_latest_result = loads(task.latest_result)
215+
# try to use the __key__
216+
old_latest_result_key = old_latest_result.get(
217+
'__key__', old_latest_result)
215218
# list of dict
216219
to_insert_result_list = []
217220
for result in result_list:
218-
if result == old_latest_result:
221+
result_key = result.get('__key__', result)
222+
if result_key == old_latest_result_key:
219223
break
220224
to_insert_result_list.append(result)
221225
if to_insert_result_list:

watchdogs/static/js/watchdogs.js

+13-4
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,8 @@ var Main = {
434434
},
435435
get_latest_result(latest_result, max_length = 80) {
436436
try {
437-
return JSON.parse(latest_result).text.slice(0, max_length)
437+
let item = JSON.parse(latest_result)
438+
return item.title || item.text.slice(0, max_length)
438439
} catch (error) {
439440
return latest_result
440441
}
@@ -455,7 +456,7 @@ var Main = {
455456
'</td><td><a target="_blank" ' +
456457
href +
457458
">" +
458-
this.escape_html(result.text) +
459+
this.escape_html(result.title || result.text) +
459460
"</a></td></tr>"
460461
})
461462
text += "</table>"
@@ -671,7 +672,7 @@ var Main = {
671672
dangerouslyUseHTMLString: true,
672673
closeOnClickModal: true,
673674
closeOnPressEscape: true,
674-
customClass: 'work_hours_doc',
675+
customClass: "work_hours_doc",
675676
})
676677
},
677678
check_error_task({ row, rowIndex }) {
@@ -763,7 +764,7 @@ var vue_app = Vue.extend(Main)
763764
var app = new vue_app({
764765
delimiters: ["${", "}"],
765766
}).$mount("#app")
766-
app.load_tasks()
767+
// app.load_tasks()
767768
// init app vars
768769
;(() => {
769770
// init_vars
@@ -773,4 +774,12 @@ app.load_tasks()
773774
app[name] = args[name]
774775
})
775776
node.parentNode.removeChild(node)
777+
// auto load
778+
var io = new IntersectionObserver((entries) => {
779+
if (entries[0].intersectionRatio <= 0) return
780+
if (app.has_more) {
781+
app.load_tasks()
782+
}
783+
})
784+
io.observe(document.getElementById("auto_load"))
776785
})()

0 commit comments

Comments
 (0)