Skip to content

Commit 91faed0

Browse files
authored
Added QQ Push Support (#1366)
1 parent 62c762c commit 91faed0

File tree

5 files changed

+259
-6
lines changed

5 files changed

+259
-6
lines changed

KEYWORDS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Pushplus
7979
PushSafer
8080
Pushy
8181
PushDeer
82+
QQ Push
8283
Reddit
8384
Resend
8485
Revolt

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ The table below identifies the services this tool supports and some example serv
113113
| [PushSafer](https://github.com/caronc/apprise/wiki/Notify_pushsafer) | psafer:// or psafers:// | (TCP) 80 or 443 | psafer://privatekey<br />psafers://privatekey/DEVICE<br />psafer://privatekey/DEVICE1/DEVICE2/DEVICEN
114114
| [Pushy](https://github.com/caronc/apprise/wiki/Notify_pushy) | pushy:// | (TCP) 443 | pushy://apikey/DEVICE<br />pushy://apikey/DEVICE1/DEVICE2/DEVICEN<br />pushy://apikey/TOPIC<br />pushy://apikey/TOPIC1/TOPIC2/TOPICN
115115
| [PushDeer](https://github.com/caronc/apprise/wiki/Notify_pushdeer) | pushdeer:// or pushdeers:// | (TCP) 80 or 443 | pushdeer://pushKey<br />pushdeer://hostname/pushKey<br />pushdeer://hostname:port/pushKey
116+
| [QQ Push](https://github.com/caronc/apprise/wiki/Notify_qq) | qq:// | (TCP) 443 | qq://Token
116117
| [Reddit](https://github.com/caronc/apprise/wiki/Notify_reddit) | reddit:// | (TCP) 443 | reddit://user:password@app_id/app_secret/subreddit<br />reddit://user:password@app_id/app_secret/sub1/sub2/subN
117118
| [Resend](https://github.com/caronc/apprise/wiki/Notify_resend) | resend:// | (TCP) 443 | resend://APIToken:FromEmail/<br />resend://APIToken:FromEmail/ToEmail<br />resend://APIToken:FromEmail/ToEmail1/ToEmail2/ToEmailN/
118119
| [Revolt](https://github.com/caronc/apprise/wiki/Notify_Revolt) | revolt:// | (TCP) 443 | revolt://bottoken/ChannelID<br />revolt://bottoken/ChannelID1/ChannelID2/ChannelIDN |

apprise/plugins/qq.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# BSD 2-Clause License
4+
#
5+
# Apprise - Push Notification Library.
6+
# Copyright (c) 2025, Chris Caron <lead2gold@gmail.com>
7+
#
8+
# Redistribution and use in source and binary forms, with or without
9+
# modification, are permitted provided that the following conditions are met:
10+
#
11+
# 1. Redistributions of source code must retain the above copyright notice,
12+
# this list of conditions and the following disclaimer.
13+
#
14+
# 2. Redistributions in binary form must reproduce the above copyright notice,
15+
# this list of conditions and the following disclaimer in the documentation
16+
# and/or other materials provided with the distribution.
17+
#
18+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28+
# POSSIBILITY OF SUCH DAMAGE.
29+
30+
# Assumes QQ Push API provided by third-party bridge like message-pusher
31+
32+
import re
33+
import requests
34+
35+
from ..utils.parse import validate_regex
36+
from ..url import PrivacyMode
37+
from .base import NotifyBase
38+
from ..locale import gettext_lazy as _
39+
from ..common import NotifyType
40+
41+
42+
class NotifyQQ(NotifyBase):
43+
"""
44+
A wrapper for QQ Push Notifications
45+
"""
46+
47+
# The default descriptive name associated with the Notification
48+
service_name = _('QQ Push')
49+
50+
# The services URL
51+
service_url = 'https://github.com/songquanpeng/message-pusher'
52+
53+
# The default secure protocol
54+
secure_protocol = 'qq'
55+
56+
# A URL that takes you to the setup/help of the specific protocol
57+
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_qq'
58+
59+
# URL used to send notifications with
60+
notify_url = 'https://qmsg.zendee.cn/send/'
61+
62+
templates = (
63+
'{schema}://{token}',
64+
)
65+
66+
template_tokens = dict(NotifyBase.template_tokens, **{
67+
'token': {
68+
'name': _('User Token'),
69+
'type': 'string',
70+
'private': True,
71+
'required': True,
72+
'regex': (r'^[a-z0-9]{24,64}$', 'i'),
73+
},
74+
})
75+
76+
def __init__(self, token, **kwargs):
77+
"""
78+
Initialize QQ Push Object
79+
80+
Args:
81+
token (str): User push token from QQ Push provider (e.g., Qmsg)
82+
"""
83+
super().__init__(**kwargs)
84+
85+
self.token = validate_regex(
86+
token, *self.template_tokens['token']['regex']
87+
)
88+
if not self.token:
89+
msg = 'The QQ Push token ({}) is invalid.'.format(token)
90+
self.logger.warning(msg)
91+
raise TypeError(msg)
92+
93+
self.webhook_url = f'{self.notify_url}{self.token}'
94+
95+
def url(self, privacy=False, *args, **kwargs):
96+
"""
97+
Returns the URL built dynamically based on specified arguments.
98+
"""
99+
params = self.url_parameters(privacy=privacy, *args, **kwargs)
100+
return '{schema}://{token}/?{params}'.format(
101+
schema=self.secure_protocol,
102+
token=self.pprint(self.token, privacy, mode=PrivacyMode.Secret),
103+
params=self.urlencode(params),
104+
)
105+
106+
@property
107+
def url_identifier(self):
108+
"""
109+
Returns a unique identifier for this plugin instance
110+
"""
111+
return (self.secure_protocol, self.token)
112+
113+
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
114+
"""
115+
Send a QQ Push Notification
116+
"""
117+
payload = {
118+
'msg': f'{title}\n{body}' if title else body
119+
}
120+
121+
headers = {
122+
'User-Agent': self.app_id,
123+
'Content-Type': 'application/x-www-form-urlencoded',
124+
}
125+
126+
self.throttle()
127+
try:
128+
response = requests.post(
129+
self.webhook_url,
130+
headers=headers,
131+
data=payload,
132+
verify=self.verify_certificate,
133+
timeout=self.request_timeout,
134+
)
135+
136+
if response.status_code != requests.codes.ok:
137+
self.logger.warning(
138+
'QQ Push notification failed: %d - %s',
139+
response.status_code, response.text)
140+
return False
141+
142+
except requests.RequestException as e:
143+
self.logger.warning(f'QQ Push Exception: {e}')
144+
return False
145+
146+
self.logger.info('QQ Push notification sent successfully.')
147+
return True
148+
149+
@staticmethod
150+
def parse_url(url):
151+
"""
152+
Parses the URL and returns arguments to re-instantiate the object
153+
"""
154+
results = NotifyBase.parse_url(url, verify_host=False)
155+
if not results:
156+
return results
157+
158+
if 'token' in results['qsd'] and results['qsd']['token']:
159+
results['token'] = NotifyQQ.unquote(results['qsd']['token'])
160+
else:
161+
results['token'] = NotifyQQ.unquote(results['host'])
162+
163+
return results
164+
165+
@staticmethod
166+
def parse_native_url(url):
167+
"""
168+
Parse native QQ push-style URL into Apprise format
169+
"""
170+
match = re.match(
171+
r'^https://qmsg\.zendee\.cn/send/([a-z0-9]+)$', url, re.I)
172+
if not match:
173+
return None
174+
175+
return NotifyQQ.parse_url(
176+
'{schema}://{token}'.format(
177+
schema=NotifyQQ.secure_protocol,
178+
token=match.group(1)))

packaging/redhat/python-apprise.spec

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,12 @@ Windows, Microsoft Teams, Misskey, MQTT, MSG91, MyAndroid, Nexmo, Nextcloud,
4848
NextcloudTalk, Notica, Notifiarr, Notifico, ntfy, Office365, OneSignal,
4949
Opsgenie, PagerDuty, PagerTree, ParsePlatform, Plivo, PopcornNotify, Prowl,
5050
Pushalot, PushBullet, Pushjet, PushMe, Pushover, Pushplus, PushSafer, Pushy,
51-
PushDeer, Revolt, Reddit, Resend, Rocket.Chat, RSyslog, SendGrid, ServerChan,
52-
Seven, SFR, Signal, SimplePush, Sinch, Slack, SMPP, SMSEagle, SMS Manager,
53-
SMTP2Go, SparkPost, Splunk, Spike, Spug Push, Super Toasty, Streamlabs, Stride,
54-
Synology Chat, Syslog, Techulus Push, Telegram, Threema Gateway, Twilio,
55-
Twitter, Twist, Vapid, VictorOps, Voipms, Vonage, WebPush, WeCom Bot, WhatsApp,
56-
Webex Teams, Workflows, WxPusher, XBMC}
51+
PushDeer, QQ Push, Revolt, Reddit, Resend, Rocket.Chat, RSyslog, SendGrid,
52+
ServerChan, Seven, SFR, Signal, SimplePush, Sinch, Slack, SMPP, SMSEagle,
53+
SMS Manager, SMTP2Go, SparkPost, Splunk, Spike, Spug Push, Super Toasty,
54+
Streamlabs, Stride, Synology Chat, Syslog, Techulus Push, Telegram, Threema
55+
Gateway, Twilio, Twitter, Twist, Vapid, VictorOps, Voipms, Vonage, WebPush,
56+
WeCom Bot, WhatsApp, Webex Teams, Workflows, WxPusher, XBMC}
5757

5858
Name: python-%{pypi_name}
5959
Version: 1.9.3

test/test_plugin_qq.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# -*- coding: utf-8 -*-
2+
# BSD 2-Clause License
3+
#
4+
# Apprise - Push Notification Library.
5+
# Copyright (c) 2025, Chris Caron <lead2gold@gmail.com>
6+
#
7+
# Redistribution and use in source and binary forms, with or without
8+
# modification, are permitted provided that the following conditions are met:
9+
#
10+
# 1. Redistributions of source code must retain the above copyright notice,
11+
# this list of conditions and the following disclaimer.
12+
#
13+
# 2. Redistributions in binary form must reproduce the above copyright notice,
14+
# this list of conditions and the following disclaimer in the documentation
15+
# and/or other materials provided with the distribution.
16+
#
17+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27+
# POSSIBILITY OF SUCH DAMAGE.
28+
29+
import requests
30+
from apprise.plugins.qq import NotifyQQ
31+
from helpers import AppriseURLTester
32+
33+
import logging
34+
logging.disable(logging.CRITICAL)
35+
36+
apprise_url_tests = (
37+
('qq://', {
38+
'instance': TypeError,
39+
}),
40+
('qq://invalid!', {
41+
'instance': TypeError,
42+
}),
43+
('qq://abc123def456ghi789jkl012mno345pq', {
44+
'instance': NotifyQQ,
45+
'privacy_url': 'qq://****/',
46+
}),
47+
('qq://?token=abc123def456ghi789jkl012mno345pq', {
48+
'instance': NotifyQQ,
49+
'privacy_url': 'qq://****/',
50+
}),
51+
('https://qmsg.zendee.cn/send/abc123def456ghi789jkl012mno345pq', {
52+
'instance': NotifyQQ,
53+
}),
54+
('qq://abc123def456ghi789jkl012mno345pq', {
55+
'instance': NotifyQQ,
56+
'response': False,
57+
'requests_response_code': requests.codes.internal_server_error,
58+
}),
59+
('qq://abc123def456ghi789jkl012mno345pq', {
60+
'instance': NotifyQQ,
61+
'response': False,
62+
'requests_response_code': 999,
63+
}),
64+
('qq://ffffffffffffffffffffffffffffffff', {
65+
'instance': NotifyQQ,
66+
'test_requests_exceptions': True,
67+
}),
68+
)
69+
70+
71+
def test_plugin_qq_urls():
72+
73+
AppriseURLTester(tests=apprise_url_tests).run_all()

0 commit comments

Comments
 (0)