Skip to content

Commit 6079515

Browse files
authored
Added support for emojis (#266)
1 parent 5afbcea commit 6079515

File tree

4 files changed

+188
-1
lines changed

4 files changed

+188
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ The use of environment variables allow you to provide over-rides to default sett
414414
| `APPRISE_STATEFUL_MODE` | This can be set to the following possible modes:<br/>📌 **hash**: This is also the default. It stores the server configuration in a hash formatted that can be easily indexed and compressed.<br/>📌 **simple**: Configuration is written straight to disk using the `{KEY}.cfg` (if `TEXT` based) and `{KEY}.yml` (if `YAML` based).<br/>📌 **disabled**: Straight up deny any read/write queries to the servers stateful store. Effectively turn off the Apprise Stateful feature completely.
415415
| `APPRISE_CONFIG_LOCK` | Locks down your API hosting so that you can no longer delete/update/access stateful information. Your configuration is still referenced when stateful calls are made to `/notify`. The idea of this switch is to allow someone to set their (Apprise) configuration up and then as an added security tactic, they may choose to lock their configuration down (in a read-only state). Those who use the Apprise CLI tool may still do it, however the `--config` (`-c`) switch will not successfully reference this access point anymore. You can however use the `apprise://` plugin without any problem ([see here for more details](https://github.com/caronc/apprise/wiki/Notify_apprise_api)). This defaults to `no` and can however be set to `yes` by simply defining the global variable as such.
416416
| `APPRISE_ADMIN` | Enables admin mode. This removes the distinction between users and admins and allows listing stored configuration keys (when `STATEFUL_MODE` is set to `simple`). This defaults to `no` and can be set to `yes`.
417+
| `APPRISE_INTERPRET_EMOJIS` | Override the Apprise `interpret-emojis` setting. This defaults to `none` (not set), but can be enforced to `no` or `yes`.
417418
| `APPRISE_DENY_SERVICES` | A comma separated set of entries identifying what plugins to deny access to. You only need to identify one schema entry associated with a plugin to in turn disable all of it. Hence, if you wanted to disable the `glib` plugin, you do not need to additionally include `qt` as well since it's included as part of the (`dbus`) package; consequently specifying `qt` would in turn disable the `glib` module as well (another way to accomplish the same task). To exclude/disable more the one upstream service, simply specify additional entries separated by a `,` (comma) or ` ` (space). The `APPRISE_DENY_SERVICES` entries are ignored if the `APPRISE_ALLOW_SERVICES` is identified. By default, this is initialized to `windows, dbus, gnome, macosx, syslog` (blocking local actions from being issued inside of the docker container)
418419
| `APPRISE_ALLOW_SERVICES` | A comma separated set of entries identifying what plugins to allow access to. You may only use alpha-numeric characters as is the restriction of Apprise Schemas (schema://) anyway. To exclusively include more the one upstream service, simply specify additional entries separated by a `,` (comma) or ` ` (space). The `APPRISE_DENY_SERVICES` entries are ignored if the `APPRISE_ALLOW_SERVICES` is identified.
419420
| `APPRISE_ATTACH_ALLOW_URLS` | A comma separated set of entries identifying the HTTP Attach URLs the Apprise API shall always accept. Use wildcards such as `*` and `?` to help construct the URL/Hosts you identify. Use a space and/or a comma to identify more then one entry. By default this is set to `*` (Accept all provided URLs).
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Copyright (C) 2025 Chris Caron <[email protected]>
2+
# All rights reserved.
3+
#
4+
# This code is licensed under the MIT License.
5+
#
6+
# Permission is hereby granted, free of charge, to any person obtaining a copy
7+
# of this software and associated documentation files(the "Software"), to deal
8+
# in the Software without restriction, including without limitation the rights
9+
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
# copies of the Software, and to permit persons to whom the Software is
11+
# furnished to do so, subject to the following conditions :
12+
#
13+
# The above copyright notice and this permission notice shall be included in
14+
# all copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
19+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
# THE SOFTWARE.
23+
import json
24+
from unittest import mock
25+
26+
from django.test import SimpleTestCase, override_settings
27+
import requests
28+
29+
import apprise
30+
31+
from ..forms import NotifyForm
32+
33+
# Grant access to our Notification Manager Singleton
34+
N_MGR = apprise.manager_plugins.NotificationManager()
35+
36+
37+
class NotifyWithEmojiTests(SimpleTestCase):
38+
"""
39+
Test notifications with Emoji Settings
40+
"""
41+
42+
@mock.patch("requests.post")
43+
def test_stateful_notify_with_emoji(self, mock_post):
44+
"""
45+
Test adding a simple stateful notification with emoji flags
46+
"""
47+
48+
# Prepare our response
49+
response = requests.Request()
50+
response.status_code = requests.codes.ok
51+
52+
# Prepare Mock
53+
mock_post.return_value = response
54+
55+
# our key to use
56+
key = "test_notify_stateful_emoji"
57+
58+
# Add some content
59+
response = self.client.post("/add/{}".format(key), {"urls": "json://user:pass@localhost"})
60+
assert response.status_code == 200
61+
62+
# Preare our form data (with emoji's in it)
63+
form_data = {
64+
"title": ":grin:",
65+
"body": "test notifiction :smile:",
66+
}
67+
68+
# At a minimum, just a body is required
69+
form = NotifyForm(data=form_data)
70+
assert form.is_valid()
71+
72+
# Required to prevent None from being passed into self.client.post()
73+
del form.cleaned_data["attachment"]
74+
75+
# we always set a type if one wasn't done so already
76+
assert form.cleaned_data["type"] == apprise.NotifyType.INFO.value
77+
78+
# we always set a format if one wasn't done so already
79+
assert form.cleaned_data["format"] == apprise.NotifyFormat.TEXT.value
80+
81+
# Send our notification
82+
with override_settings(APPRISE_INTERPRET_EMOJIS=True):
83+
response = self.client.post("/notify/{}".format(key), form.cleaned_data)
84+
assert response.status_code == 200
85+
assert mock_post.call_count == 1
86+
87+
details = mock_post.call_args_list[0]
88+
assert details[0][0].startswith("http://localhost")
89+
90+
payload = json.loads(details[1]["data"])
91+
assert payload["title"] == "😃"
92+
assert payload["message"] == "test notifiction 😄"
93+
94+
# Reset our mock object
95+
mock_post.reset_mock()
96+
97+
# Send our notification
98+
with override_settings(APPRISE_INTERPRET_EMOJIS=False):
99+
response = self.client.post("/notify/{}".format(key), form.cleaned_data)
100+
assert response.status_code == 200
101+
assert mock_post.call_count == 1
102+
103+
details = mock_post.call_args_list[0]
104+
assert details[0][0].startswith("http://localhost")
105+
106+
payload = json.loads(details[1]["data"])
107+
assert payload["title"] == ":grin:"
108+
assert payload["message"] == "test notifiction :smile:"
109+
110+
# Reset our mock object
111+
mock_post.reset_mock()
112+
113+
@mock.patch("requests.post")
114+
def test_stateless_notify_with_emoji(self, mock_post):
115+
"""
116+
Test adding a simple stateless notification with emoji flags
117+
"""
118+
119+
# Prepare our response
120+
response = requests.Request()
121+
response.status_code = requests.codes.ok
122+
123+
# Prepare Mock
124+
mock_post.return_value = response
125+
126+
# Preare our payload (with emoji entries)
127+
json_data = {
128+
"urls": "json://user:pass@localhost",
129+
"title": ":grin:",
130+
"body": "test notifiction :smile:",
131+
}
132+
133+
with override_settings(APPRISE_INTERPRET_EMOJIS=True):
134+
# Send our notification as a JSON object
135+
response = self.client.post(
136+
"/notify",
137+
data=json.dumps(json_data),
138+
content_type="application/json",
139+
)
140+
assert response.status_code == 200
141+
assert mock_post.call_count == 1
142+
143+
details = mock_post.call_args_list[0]
144+
assert details[0][0].startswith("http://localhost")
145+
146+
payload = json.loads(details[1]["data"])
147+
assert payload["title"] == "😃"
148+
assert payload["message"] == "test notifiction 😄"
149+
150+
# Reset Mock
151+
mock_post.reset_mock()
152+
153+
with override_settings(APPRISE_INTERPRET_EMOJIS=False):
154+
# Send our notification as a JSON object
155+
response = self.client.post(
156+
"/notify",
157+
data=json.dumps(json_data),
158+
content_type="application/json",
159+
)
160+
assert response.status_code == 200
161+
assert mock_post.call_count == 1
162+
163+
details = mock_post.call_args_list[0]
164+
assert details[0][0].startswith("http://localhost")
165+
166+
payload = json.loads(details[1]["data"])
167+
assert payload["title"] == ":grin:"
168+
assert payload["message"] == "test notifiction :smile:"
169+
170+
# Reset Mock
171+
mock_post.reset_mock()

apprise_api/api/views.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,8 @@ def post(self, request, key):
13001300
"storage_idlen": settings.APPRISE_STORAGE_UID_LENGTH,
13011301
# Define if we flush to disk as soon as possible or not when required
13021302
"storage_mode": settings.APPRISE_STORAGE_MODE,
1303+
# Emoji configuration (values are None, True, or False)
1304+
"interpret_emojis": settings.APPRISE_INTERPRET_EMOJIS,
13031305
}
13041306

13051307
if body_format:
@@ -1380,7 +1382,7 @@ def post(self, request, key):
13801382
a_obj = apprise.Apprise(asset=asset)
13811383

13821384
# Create an apprise config object
1383-
ac_obj = apprise.AppriseConfig()
1385+
ac_obj = apprise.AppriseConfig(asset=asset)
13841386

13851387
# Load our configuration
13861388
ac_obj.add_config(config, format=format)
@@ -1766,6 +1768,8 @@ def post(self, request):
17661768
kwargs = {
17671769
# Load our dynamic plugin path
17681770
"plugin_paths": settings.APPRISE_PLUGIN_PATHS,
1771+
# Emoji configuration (values are None, True, or False)
1772+
"interpret_emojis": settings.APPRISE_INTERPRET_EMOJIS,
17691773
}
17701774
if settings.APPRISE_STATELESS_STORAGE:
17711775
# Persistent Storage is allowed with Stateless queries

apprise_api/core/settings/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,14 @@
285285
"e",
286286
"+",
287287
)
288+
289+
# Allow Interpret Emojis override
290+
APPRISE_INTERPRET_EMOJIS = None if "APPRISE_INTERPRET_EMOJIS" not in os.environ \
291+
else os.environ.get("APPRISE_INTERPRET_EMOJIS", "yes")[0].lower() in (
292+
"a",
293+
"y",
294+
"1",
295+
"t",
296+
"e",
297+
"+",
298+
)

0 commit comments

Comments
 (0)