-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathnotifier.py
More file actions
329 lines (269 loc) · 11.3 KB
/
Copy pathnotifier.py
File metadata and controls
329 lines (269 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
"""
Notifier system - supports multiple notification providers
Provides async interface for sending notifications
"""
import asyncio
import re
from abc import ABC, abstractmethod
from loguru import logger
from rich.console import Console
from rich.markdown import Markdown
class BaseNotifier(ABC):
"""Base class for all notification providers"""
def __init__(self, config: dict):
"""
Initialize the notifier.
Args:
config: Configuration dictionary for this notifier
"""
self.config = config
self.enabled = config.get("enabled", True)
@abstractmethod
async def send(self, markdown_content: str) -> None:
"""
Send a notification with markdown content.
Args:
markdown_content: The notification content in markdown format
"""
raise NotImplementedError
class ConsoleNotifier(BaseNotifier):
"""Console notifier that logs to terminal using rich"""
def __init__(self, config: dict):
super().__init__(config)
self.console = Console()
self.enable_rich = config.get("enable_rich_markdown_formatting", True)
logger.info("Console notifier initialized")
async def send(self, markdown_content: str) -> None:
"""
Send notification to console.
Args:
markdown_content: The notification content in markdown format
"""
if not self.enabled:
return
try:
if self.enable_rich:
md = Markdown(markdown_content)
self.console.print(md)
else:
print(markdown_content)
except Exception as e:
logger.error(f"Error sending console notification: {e}", exc_info=True)
class NtfyNotifier(BaseNotifier):
"""Ntfy notifier that sends to ntfy server"""
def __init__(self, config: dict):
super().__init__(config)
self.url = config.get("url", "https://ntfy.sh")
self.topic = config.get("topic", "reborn")
self.token = config.get("auth_token")
# Import here to avoid dependency if not using ntfy
from python_ntfy import NtfyClient
self.client = NtfyClient(topic=self.topic, server=self.url, auth=self.token)
logger.info(f"Ntfy notifier initialized: {self.url}/{self.topic}")
async def send(self, markdown_content: str) -> None:
"""
Send notification to ntfy server.
Args:
markdown_content: The notification content in markdown format
"""
if not self.enabled:
return
try:
# Send as plain text (ntfy will handle markdown rendering)
await asyncio.to_thread(
self.client.send, message=markdown_content, format_as_markdown=True
)
logger.debug(f"Ntfy notification sent: {markdown_content[:100]}...")
except Exception as e:
logger.error(f"Error sending ntfy notification: {e}", exc_info=True)
class LarkNotifier(BaseNotifier):
"""Lark notifier that sends messages to Lark (Feishu) via webhook"""
def __init__(self, config: dict):
super().__init__(config)
self.webhook_url = config.get("webhook_url")
if not self.webhook_url:
raise ValueError("webhook_url must be provided for LarkNotifier")
self.session = None
logger.info("Lark notifier initialized")
async def send(self, markdown_content: str) -> None:
"""
Send notification to Lark via webhook.
Args:
markdown_content: The notification content in markdown format
"""
if not self.enabled:
return
try:
# Check if session is initialized
if self.session is None:
# Import here to avoid dependency if not using LarkNotifier
import aiohttp
self.session = aiohttp.ClientSession()
# Try to get the title using regex
title_match = re.search(
r"^#\s+(.+)$", markdown_content.strip(), re.MULTILINE
)
title = title_match.group(1) if title_match else "Notification"
# Send the markdown content as a Lark message
payload = {
"msg_type": "interactive",
"card": {
"schema": "2.0",
"header": {
"title": {"tag": "plain_text", "content": title},
"template": "blue",
},
"body": {
"elements": [
{
"tag": "markdown",
"content": markdown_content,
}
],
},
},
}
async with self.session.post(self.webhook_url, json=payload) as response:
if response.status != 200:
text = await response.text()
raise Exception(
f"Failed to send Lark notification: {response.status}, {text}"
)
except Exception as e:
logger.error(f"Error sending Lark notification: {e}", exc_info=True)
async def close(self) -> None:
"""Close the aiohttp session"""
if self.session is not None:
await self.session.close()
self.session = None
class OneBotNotifier(BaseNotifier):
"""OneBot-11 notifier that sends messages to OneBot-11 servers via HTTP"""
def __init__(self, config: dict):
super().__init__(config)
self.url = config.get("url", "http://127.0.0.1:5700")
self.access_token = config.get("access_token", "")
self.to_group_ids = config.get("to_group_ids", [])
self.to_friend_ids = config.get("to_friend_ids", [])
if not self.to_group_ids and not self.to_friend_ids:
logger.warning(
"OneBotNotifier: No group or friend IDs configured, notifications will not be sent"
)
# Prepare headers once during initialization
self.headers = {}
if self.access_token:
self.headers["Authorization"] = f"Bearer {self.access_token}"
self.session = None
logger.info(
f"OneBot-11 notifier initialized: {self.url} "
f"(groups: {len(self.to_group_ids)}, friends: {len(self.to_friend_ids)})"
)
async def send(self, markdown_content: str) -> None:
"""
Send notification to OneBot-11 server.
Args:
markdown_content: The notification content in markdown format
"""
if not self.enabled:
return
if not self.to_group_ids and not self.to_friend_ids:
return
# Check if session is initialized
if self.session is None:
# Import here to avoid dependency if not using OneBotNotifier
import aiohttp
# Create session with timeout configuration
timeout = aiohttp.ClientTimeout(total=30)
self.session = aiohttp.ClientSession(timeout=timeout)
# Send to all configured groups
for group_id in self.to_group_ids:
payload = {"group_id": group_id, "message": markdown_content}
try:
async with self.session.post(
f"{self.url}/send_group_msg", json=payload, headers=self.headers
) as response:
if response.status != 200:
text = await response.text()
logger.error(
f"Failed to send OneBot group message to {group_id}: {response.status}, {text}"
)
else:
logger.debug(f"OneBot group notification sent to {group_id}")
except Exception as e:
logger.error(
f"Error sending OneBot group notification to {group_id}: {e}",
exc_info=True,
)
# Send to all configured friends
for friend_id in self.to_friend_ids:
payload = {"user_id": friend_id, "message": markdown_content}
try:
async with self.session.post(
f"{self.url}/send_private_msg", json=payload, headers=self.headers
) as response:
if response.status != 200:
text = await response.text()
logger.error(
f"Failed to send OneBot private message to {friend_id}: {response.status}, {text}"
)
else:
logger.debug(
f"OneBot private notification sent to {friend_id}"
)
except Exception as e:
logger.error(
f"Error sending OneBot private notification to {friend_id}: {e}",
exc_info=True,
)
async def close(self) -> None:
"""Close the aiohttp session"""
if self.session is not None:
await self.session.close()
self.session = None
class Notifier:
"""Main notifier class that manages multiple notification providers. This class handles the configuration file and initializes the appropriate notifier instances based on the provided settings."""
def __init__(self, config: dict):
"""
Initialize the notifier system.
Args:
config: Configuration dictionary with notifier settings
"""
self.notifiers: list[BaseNotifier] = []
# Initialize console notifier if configured
console_config = config.get("console", {})
if console_config.get("enabled", False):
self.notifiers.append(ConsoleNotifier(console_config))
# Initialize ntfy notifier if configured
ntfy_config = config.get("ntfy", {})
if ntfy_config.get("enabled", False):
self.notifiers.append(NtfyNotifier(ntfy_config))
# Initialize lark notifier if configured
lark_config = config.get("lark", {})
if lark_config.get("enabled", False):
self.notifiers.append(LarkNotifier(lark_config))
# Initialize onebot-11 notifier if configured
onebot_config = config.get("onebot-11", {})
if onebot_config.get("enabled", False):
self.notifiers.append(OneBotNotifier(onebot_config))
logger.info(
f"Notifier system initialized with {len(self.notifiers)} provider(s)"
)
async def send(self, markdown_content: str) -> None:
"""
Send notification to all configured providers.
Args:
markdown_content: The notification content in markdown format
"""
if not self.notifiers:
logger.warning("No notifiers configured, skipping notification")
return
# Send to all notifiers concurrently
await asyncio.gather(
*[notifier.send(markdown_content) for notifier in self.notifiers],
return_exceptions=True,
)
async def close(self):
"""Close/cleanup all notifiers"""
for notifier in self.notifiers:
if hasattr(notifier, "close"):
await notifier.close()
logger.info("Notifier system closed")