Skip to content

Commit d1b3f27

Browse files
authored
Merge pull request #232 from LeslieLeung/feature/quote0
2 parents ce007c0 + 3ebc8ee commit d1b3f27

9 files changed

Lines changed: 703 additions & 535 deletions

File tree

.env.example

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,10 @@ channel_12_DINGTALK_SAFE_WORDS=<YOUR_DINGTALK_SAFE_WORDS>
7272

7373
# channel_13(apprise)
7474
channel_13_TYPE=apprise
75-
channel_13_APPRISE_URLS=<YOUR_APPRISE_URLS>
75+
channel_13_APPRISE_URLS=<YOUR_APPRISE_URLS>
76+
77+
# channel_14(quote0)
78+
channel_14_TYPE=quote0
79+
channel_14_QUOTE0_DEVICE_ID=<YOUR_QUOTE0_DEVICE_ID>
80+
channel_14_QUOTE0_API_KEY=<YOUR_QUOTE0_API_KEY>
81+
channel_14_QUOTE0_BASE_URL=https://dot.mindreset.tech

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Heimdallr 是一个非常轻量的通知网关,可以聚合各种推送渠道
3737
- [钉钉自定义机器人](https://open.dingtalk.com/document/robots/custom-robot-access)
3838
- [Apprise](https://github.com/caronc/apprise)
3939
- [PushMe](https://push.i-i.me/)
40+
- [Quote0/MindReset](https://dot.mindreset.tech/docs/server/text_api)
4041

4142
> 如果有需要的通知方式,请提交 [issue](https://github.com/LeslieLeung/heimdallr/issues/new?assignees=LeslieLeung&labels=enhancement&template=feature_request.md&title=)
4243

docs/Config.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ channel_4_WECOM_WEBHOOK_KEY=<YOUR_WECOM_WEBHOOK_KEY>
8888
| ntfy | ntfy |
8989
| 飞书/Lark Webhook | lark_webhook |
9090
| 钉钉自定义机器人 | dingtalk_webhook |
91+
| Quote0/MindReset | quote0 |
9192

9293

9394
#### 渠道配置后缀
@@ -126,7 +127,10 @@ channel_4_WECOM_WEBHOOK_KEY=<YOUR_WECOM_WEBHOOK_KEY>
126127
| `DINGTALK_SECRET` | 钉钉 | 钉钉的 secret,通过加签方式保护机器人。与关键词只能二选一,推荐使用签名,见 [这里](https://open.dingtalk.com/document/orgapp/customize-robot-security-settings)|
127128
| `APPRISE_URL` | Apprise | Apprise 的协议 URL,见 [这里](https://github.com/caronc/apprise#supported-notifications) |
128129
| `PUSHME_URL` | PushMe | PushMe 的服务端地址 |
129-
| `PUSHME_PUSH_KEY` | PushMe |
130+
| `PUSHME_PUSH_KEY` | PushMe | PushMe 的推送密钥 |
131+
| `QUOTE0_DEVICE_ID` | Quote0 | Quote0 (MindReset) 的设备ID,见 [Quote0 API](https://dot.mindreset.tech/docs/server/text_api) |
132+
| `QUOTE0_API_KEY` | Quote0 | Quote0 (MindReset) 的API密钥 |
133+
| `QUOTE0_BASE_URL` | Quote0 | Quote0 (MindReset) 的服务端地址,默认 https://dot.mindreset.tech
130134

131135

132136
## 腾讯云 Serverless 环境变量设置

heimdallr/api/welcome.py

Lines changed: 172 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,46 +12,198 @@ async def welcome():
1212
<head>
1313
<title>Welcome to Heimdallr!</title>
1414
<style>
15-
body {
16-
font-family: Arial, sans-serif;
17-
background-color: #f0f0f0;
15+
* {
1816
margin: 0;
1917
padding: 0;
18+
box-sizing: border-box;
19+
}
20+
21+
body {
22+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
23+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
24+
min-height: 100vh;
2025
display: flex;
2126
flex-direction: column;
2227
justify-content: center;
2328
align-items: center;
24-
height: 100vh;
29+
color: #333;
30+
position: relative;
31+
overflow-x: hidden;
2532
}
26-
.welcome-message {
33+
34+
.welcome-container {
35+
background: rgba(255, 255, 255, 0.95);
36+
backdrop-filter: blur(10px);
37+
border-radius: 20px;
38+
padding: 60px 80px;
39+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
2740
text-align: center;
28-
font-size: 2em;
29-
color: #333;
41+
max-width: 700px;
42+
margin: 20px;
43+
animation: fadeInUp 0.8s ease-out;
44+
}
45+
46+
@keyframes fadeInUp {
47+
from {
48+
opacity: 0;
49+
transform: translateY(30px);
50+
}
51+
to {
52+
opacity: 1;
53+
transform: translateY(0);
54+
}
55+
}
56+
57+
.welcome-message h1 {
58+
font-size: 2.5em;
59+
margin-bottom: 30px;
60+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
61+
-webkit-background-clip: text;
62+
-webkit-text-fill-color: transparent;
63+
background-clip: text;
64+
font-weight: 700;
65+
}
66+
67+
.welcome-message p {
68+
font-size: 1.2em;
69+
margin-bottom: 20px;
70+
color: #555;
71+
line-height: 1.6;
72+
}
73+
74+
.welcome-message a {
75+
color: #667eea;
76+
text-decoration: none;
77+
font-weight: 600;
78+
transition: all 0.3s ease;
79+
position: relative;
80+
}
81+
82+
.welcome-message a:hover {
83+
color: #764ba2;
84+
transform: translateY(-2px);
85+
}
86+
87+
.welcome-message a::after {
88+
content: '';
89+
position: absolute;
90+
width: 0;
91+
height: 2px;
92+
bottom: -2px;
93+
left: 0;
94+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
95+
transition: width 0.3s ease;
96+
}
97+
98+
.welcome-message a:hover::after {
99+
width: 100%;
30100
}
101+
31102
.small-text {
32-
color: #666;
33-
font-size: 0.8em;
103+
font-size: 1em;
104+
color: #777;
105+
margin-top: 30px;
34106
}
107+
108+
.feature-grid {
109+
display: flex;
110+
justify-content: space-between;
111+
gap: 20px;
112+
margin: 30px 0;
113+
width: 100%;
114+
}
115+
116+
.feature-item {
117+
background: rgba(102, 126, 234, 0.1);
118+
border-radius: 10px;
119+
padding: 20px;
120+
transition: transform 0.3s ease;
121+
flex: 1;
122+
min-width: 0;
123+
}
124+
125+
.feature-item:hover {
126+
transform: translateY(-5px);
127+
}
128+
129+
.feature-icon {
130+
font-size: 2em;
131+
margin-bottom: 10px;
132+
}
133+
35134
footer {
36-
position: fixed;
37-
left: 0;
135+
position: absolute;
38136
bottom: 0;
39-
width: 100%;
40-
background-color: #666; /* Changed the color here */
41-
color: white;
137+
left: 0;
138+
right: 0;
139+
background: rgba(0, 0, 0, 0.1);
140+
backdrop-filter: blur(10px);
141+
padding: 20px 0;
42142
text-align: center;
43-
padding: 10px 0;
143+
color: white;
144+
font-size: 1.1em;
145+
}
146+
147+
footer a {
148+
color: #fff;
149+
text-decoration: none;
150+
font-weight: 600;
151+
transition: all 0.3s ease;
152+
}
153+
154+
footer a:hover {
155+
color: #ffeb3b;
156+
text-shadow: 0 0 10px rgba(255, 235, 59, 0.5);
157+
}
158+
159+
@media (max-width: 600px) {
160+
.welcome-container {
161+
padding: 40px 30px;
162+
margin: 15px;
163+
}
164+
165+
.welcome-message h1 {
166+
font-size: 2em;
167+
}
168+
169+
.welcome-message p {
170+
font-size: 1.1em;
171+
}
172+
173+
.feature-grid {
174+
flex-direction: column;
175+
gap: 15px;
176+
}
44177
}
45178
</style>
46179
</head>
47180
<body>
48-
<div class="welcome-message">
49-
<h1>🎉Welcome to Heimdallr!🎉</h1>
50-
<p>部署成功!查看<a href="/docs">接口文档</a></p>
51-
<p class="small-text">不知道怎么用?查看<a href="https://github.com/LeslieLeung/heimdallr#%E7%A4%BA%E4%BE%8B%E5%BA%94%E7%94%A8" target="_blank">示例应用</a></p>
181+
<div class="welcome-container">
182+
<div class="welcome-message">
183+
<h1>🎉 Welcome to Heimdallr! 🎉</h1>
184+
<p>部署成功!您的通知服务已经就绪</p>
185+
186+
<div class="feature-grid">
187+
<div class="feature-item">
188+
<div class="feature-icon">📱</div>
189+
<p>多平台通知</p>
190+
</div>
191+
<div class="feature-item">
192+
<div class="feature-icon">⚡</div>
193+
<p>快速部署</p>
194+
</div>
195+
<div class="feature-item">
196+
<div class="feature-icon">🔧</div>
197+
<p>易于集成</p>
198+
</div>
199+
</div>
200+
201+
<p><a href="/docs">查看接口文档</a> 开始使用</p>
202+
<p class="small-text">不知道怎么用?查看<a href="https://github.com/LeslieLeung/heimdallr#%E7%A4%BA%E4%BE%8B%E5%BA%94%E7%94%A8" target="_blank">示例应用</a></p>
203+
</div>
52204
</div>
53205
<footer>
54-
<p>如果觉得本项目不错,不妨给个Star!<a href="https://github.com/leslieleung/heimdallr" target="_blank">GitHub</a></p>
206+
<p>如果觉得本项目不错,不妨给个Star! <a href="https://github.com/leslieleung/heimdallr" target="_blank">GitHub</a></p>
55207
</footer>
56208
</body>
57209
</html>

heimdallr/channel/factory.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from heimdallr.channel.pushdeer import PushDeer, PushDeerMessage
1313
from heimdallr.channel.pushme import Pushme, PushmeMessage
1414
from heimdallr.channel.pushover import Pushover, PushoverMessage
15+
from heimdallr.channel.quote0 import Quote0, Quote0Message
1516
from heimdallr.channel.telegram import Telegram, TelegramMessage
1617
from heimdallr.channel.wecom import (
1718
WecomApp,
@@ -40,6 +41,7 @@
4041
CHANNEL_DINGTALK_WEBHOOK = "dingtalk_webhook"
4142
CHANNEL_APPRISE = "apprise"
4243
CHANNEL_PUSHME = "pushme"
44+
CHANNEL_QUOTE0 = "quote0"
4345

4446

4547
def _get_channel_type_by_name(name: str) -> str:
@@ -85,6 +87,8 @@ def build_channel(name: str) -> Channel:
8587
return Apprise(name, channel_type)
8688
elif channel_type == CHANNEL_PUSHME:
8789
return Pushme(name, channel_type)
90+
elif channel_type == CHANNEL_QUOTE0:
91+
return Quote0(name, channel_type)
8892
else:
8993
raise ParamException(f"Channel {name} type {channel_type} not supported.")
9094

@@ -123,5 +127,7 @@ def build_message(name: str, title: str, body: str, **kwargs) -> Message:
123127
return AppriseMessage(title, body, **kwargs)
124128
elif channel_type == CHANNEL_PUSHME:
125129
return PushmeMessage(title, body, **kwargs)
130+
elif channel_type == CHANNEL_QUOTE0:
131+
return Quote0Message(title, body, **kwargs)
126132
else:
127133
raise ParamException(f"Channel type {channel_type} not supported.")

heimdallr/channel/quote0.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import logging
2+
from typing import Tuple
3+
4+
import requests
5+
6+
from heimdallr.channel.base import Channel, Message
7+
from heimdallr.config.config import get_config_str
8+
from heimdallr.config.definition import (
9+
SUFFIX_QUOTE0_API_KEY,
10+
SUFFIX_QUOTE0_BASE_URL,
11+
SUFFIX_QUOTE0_DEVICE_ID,
12+
)
13+
from heimdallr.exception import ParamException
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
class Quote0Message(Message):
19+
def __init__(
20+
self,
21+
title: str,
22+
body: str,
23+
**kwargs,
24+
):
25+
super().__init__(title, body)
26+
27+
def render_message(self) -> dict:
28+
"""
29+
Render the message to the format that MindReset API expects.
30+
According to the API docs: https://dot.mindreset.tech/docs/server/text_api
31+
The API expects: POST /api/open/text
32+
Header: "Authorization: Bearer <API_KEY>"
33+
Body: {"deviceId":"<序列号>","title":"<标题>","message":"<内容>"}
34+
"""
35+
if self.body == "":
36+
raise ParamException("Message body cannot be empty.")
37+
38+
payload = {"deviceId": "", "title": self.title, "message": self.body, "signature": "Heimdallr"}
39+
return payload
40+
41+
42+
class Quote0(Channel):
43+
def __init__(self, name: str, type: str) -> None:
44+
super().__init__(name, type)
45+
self.base_url: str = "https://dot.mindreset.tech"
46+
self.device_id: str = ""
47+
self.api_key: str = ""
48+
self._build_channel()
49+
50+
def _build_channel(self) -> None:
51+
"""
52+
Get the configuration for Quote0 channel.
53+
"""
54+
self.base_url = get_config_str(self.get_name(), SUFFIX_QUOTE0_BASE_URL, self.base_url)
55+
self.device_id = get_config_str(self.get_name(), SUFFIX_QUOTE0_DEVICE_ID, "")
56+
self.api_key = get_config_str(self.get_name(), SUFFIX_QUOTE0_API_KEY, "")
57+
58+
if self.device_id == "":
59+
raise ParamException("Quote0 device ID cannot be empty.")
60+
if self.api_key == "":
61+
raise ParamException("Quote0 API key cannot be empty.")
62+
63+
def send(self, message: Message) -> Tuple[bool, str]:
64+
"""
65+
Send a message to MindReset server.
66+
According to API docs: POST /api/open/text
67+
"""
68+
if not isinstance(message, Quote0Message):
69+
message = Quote0Message(message.title, message.body)
70+
71+
url = f"{self.base_url}/api/open/text"
72+
payload = message.render_message()
73+
payload["deviceId"] = self.device_id
74+
75+
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
76+
77+
try:
78+
rs = requests.post(url, json=payload, headers=headers)
79+
logger.debug(f"Quote0 response: {rs.text}")
80+
81+
# Check if the request was successful
82+
if rs.status_code == 200:
83+
return True, "Message sent successfully"
84+
else:
85+
error_msg = f"HTTP {rs.status_code}: {rs.text}"
86+
logger.error(f"Quote0 error: {error_msg}")
87+
return False, error_msg
88+
89+
except requests.exceptions.RequestException as e:
90+
error_msg = f"Request failed: {str(e)}"
91+
logger.error(f"Quote0 request error: {error_msg}")
92+
return False, error_msg

heimdallr/config/definition.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,7 @@
5454
# pushme
5555
SUFFIX_PUSHME_URL = "PUSHME_URL"
5656
SUFFIX_PUSHME_PUSH_KEY = "PUSHME_PUSH_KEY"
57+
# quote0 (mindreset)
58+
SUFFIX_QUOTE0_DEVICE_ID = "QUOTE0_DEVICE_ID"
59+
SUFFIX_QUOTE0_API_KEY = "QUOTE0_API_KEY"
60+
SUFFIX_QUOTE0_BASE_URL = "QUOTE0_BASE_URL"

0 commit comments

Comments
 (0)