mirror of
https://github.com/caronc/apprise.git
synced 2024-11-25 01:24:03 +01:00
Apprise Emoji Support Added (#1011)
This commit is contained in:
parent
eb4e47cc45
commit
76831f9a8b
@ -36,6 +36,7 @@ from .utils import is_exclusive_match
|
||||
from .utils import parse_list
|
||||
from .utils import parse_urls
|
||||
from .utils import cwe312_url
|
||||
from .emojis import apply_emojis
|
||||
from .logger import logger
|
||||
from .AppriseAsset import AppriseAsset
|
||||
from .AppriseConfig import AppriseConfig
|
||||
@ -376,7 +377,7 @@ class Apprise:
|
||||
body, title,
|
||||
notify_type=notify_type, body_format=body_format,
|
||||
tag=tag, match_always=match_always, attach=attach,
|
||||
interpret_escapes=interpret_escapes
|
||||
interpret_escapes=interpret_escapes,
|
||||
)
|
||||
|
||||
except TypeError:
|
||||
@ -501,6 +502,11 @@ class Apprise:
|
||||
key = server.notify_format if server.title_maxlen > 0\
|
||||
else f'_{server.notify_format}'
|
||||
|
||||
if server.interpret_emojis:
|
||||
# alter our key slightly to handle emojis since their value is
|
||||
# pulled out of the notification
|
||||
key += "-emojis"
|
||||
|
||||
if key not in conversion_title_map:
|
||||
|
||||
# Prepare our title
|
||||
@ -542,6 +548,16 @@ class Apprise:
|
||||
logger.error(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
if server.interpret_emojis:
|
||||
#
|
||||
# Convert our :emoji: definitions
|
||||
#
|
||||
|
||||
conversion_body_map[key] = \
|
||||
apply_emojis(conversion_body_map[key])
|
||||
conversion_title_map[key] = \
|
||||
apply_emojis(conversion_title_map[key])
|
||||
|
||||
kwargs = dict(
|
||||
body=conversion_body_map[key],
|
||||
title=conversion_title_map[key],
|
||||
|
@ -121,6 +121,12 @@ class AppriseAsset:
|
||||
# notifications are sent sequentially (one after another)
|
||||
async_mode = True
|
||||
|
||||
# Support :smile:, and other alike keywords swapping them for their
|
||||
# unicode value. A value of None leaves the interpretation up to the
|
||||
# end user to control (allowing them to specify emojis=yes on the
|
||||
# URL)
|
||||
interpret_emojis = None
|
||||
|
||||
# Whether or not to interpret escapes found within the input text prior
|
||||
# to passing it upstream. Such as converting \t to an actual tab and \n
|
||||
# to a new line.
|
||||
|
@ -213,6 +213,8 @@ def print_version_msg():
|
||||
'increase the verbosity. I.e.: -vvvv')
|
||||
@click.option('--interpret-escapes', '-e', is_flag=True,
|
||||
help='Enable interpretation of backslash escapes')
|
||||
@click.option('--interpret-emojis', '-j', is_flag=True,
|
||||
help='Enable interpretation of :emoji: definitions')
|
||||
@click.option('--debug', '-D', is_flag=True, help='Debug mode')
|
||||
@click.option('--version', '-V', is_flag=True,
|
||||
help='Display the apprise version and exit.')
|
||||
@ -220,7 +222,8 @@ def print_version_msg():
|
||||
metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',)
|
||||
def main(body, title, config, attach, urls, notification_type, theme, tag,
|
||||
input_format, dry_run, recursion_depth, verbose, disable_async,
|
||||
details, interpret_escapes, plugin_path, debug, version):
|
||||
details, interpret_escapes, interpret_emojis, plugin_path, debug,
|
||||
version):
|
||||
"""
|
||||
Send a notification to all of the specified servers identified by their
|
||||
URLs the content provided within the title, body and notification-type.
|
||||
@ -308,6 +311,9 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
||||
# Interpret Escapes
|
||||
interpret_escapes=interpret_escapes,
|
||||
|
||||
# Interpret Emojis
|
||||
interpret_emojis=None if not interpret_emojis else True,
|
||||
|
||||
# Set the theme
|
||||
theme=theme,
|
||||
|
||||
|
2262
apprise/emojis.py
Normal file
2262
apprise/emojis.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -32,6 +32,7 @@ from functools import partial
|
||||
|
||||
from ..URLBase import URLBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_bool
|
||||
from ..common import NOTIFY_TYPES
|
||||
from ..common import NotifyFormat
|
||||
from ..common import NOTIFY_FORMATS
|
||||
@ -135,6 +136,9 @@ class NotifyBase(URLBase):
|
||||
# Default Overflow Mode
|
||||
overflow_mode = OverflowMode.UPSTREAM
|
||||
|
||||
# Default Emoji Interpretation
|
||||
interpret_emojis = False
|
||||
|
||||
# Support Attachments; this defaults to being disabled.
|
||||
# Since apprise allows you to send attachments without a body or title
|
||||
# defined, by letting Apprise know the plugin won't support attachments
|
||||
@ -183,6 +187,16 @@ class NotifyBase(URLBase):
|
||||
# runtime.
|
||||
'_lookup_default': 'notify_format',
|
||||
},
|
||||
'emojis': {
|
||||
'name': _('Interpret Emojis'),
|
||||
# SSL Certificate Authority Verification
|
||||
'type': 'bool',
|
||||
# Provide a default
|
||||
'default': interpret_emojis,
|
||||
# look up default using the following parent class value at
|
||||
# runtime.
|
||||
'_lookup_default': 'interpret_emojis',
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@ -194,6 +208,29 @@ class NotifyBase(URLBase):
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Store our interpret_emoji's setting
|
||||
# If asset emoji value is set to a default of True and the user
|
||||
# specifies it to be false, this is accepted and False over-rides.
|
||||
#
|
||||
# If asset emoji value is set to a default of None, a user may
|
||||
# optionally over-ride this and set it to True from the Apprise
|
||||
# URL. ?emojis=yes
|
||||
#
|
||||
# If asset emoji value is set to a default of False, then all emoji's
|
||||
# are turned off (no user over-rides allowed)
|
||||
#
|
||||
|
||||
# Take a default
|
||||
self.interpret_emojis = self.asset.interpret_emojis
|
||||
if 'emojis' in kwargs:
|
||||
# possibly over-ride default
|
||||
self.interpret_emojis = True if self.interpret_emojis \
|
||||
in (None, True) and \
|
||||
parse_bool(
|
||||
kwargs.get('emojis', False),
|
||||
default=NotifyBase.template_args['emojis']['default']) \
|
||||
else False
|
||||
|
||||
if 'format' in kwargs:
|
||||
# Store the specified format if specified
|
||||
notify_format = kwargs.get('format', '')
|
||||
@ -548,6 +585,10 @@ class NotifyBase(URLBase):
|
||||
results['overflow']))
|
||||
del results['overflow']
|
||||
|
||||
# Allow emoji's override
|
||||
if 'emojis' in results['qsd']:
|
||||
results['emojis'] = parse_bool(results['qsd'].get('emojis'))
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
|
@ -70,6 +70,11 @@ The Apprise options are as follows:
|
||||
Enable interpretation of backslash escapes. For example, this would convert
|
||||
sequences such as \n and \r to their respected ascii new-line and carriage
|
||||
|
||||
`-j`, `--interpret-emojis`
|
||||
Enable interpretation of emoji strings. For example, this would convert
|
||||
sequences such as :smile: or :grin: to their respected unicode emoji
|
||||
character.
|
||||
|
||||
`-d`, `--dry-run`:
|
||||
Perform a trial run but only prints the notification services to-be
|
||||
triggered to **stdout**. Notifications are never sent using this mode.
|
||||
|
@ -1520,7 +1520,7 @@ def test_apprise_details_plugin_verification():
|
||||
# General Parameters
|
||||
'user', 'password', 'port', 'host', 'schema', 'fullpath',
|
||||
# NotifyBase parameters:
|
||||
'format', 'overflow',
|
||||
'format', 'overflow', 'emojis',
|
||||
# URLBase parameters:
|
||||
'verify', 'cto', 'rto',
|
||||
])
|
||||
|
@ -599,6 +599,23 @@ def test_apprise_cli_nux_env(tmpdir):
|
||||
])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Test Emojis:
|
||||
result = runner.invoke(cli.main, [
|
||||
'-j',
|
||||
'-t', ':smile:',
|
||||
'-b', ':grin:',
|
||||
'good://localhost',
|
||||
])
|
||||
assert result.exit_code == 0
|
||||
|
||||
result = runner.invoke(cli.main, [
|
||||
'--interpret-emojis',
|
||||
'-t', ':smile:',
|
||||
'-b', ':grin:',
|
||||
'good://localhost',
|
||||
])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_apprise_cli_details(tmpdir):
|
||||
"""
|
||||
|
56
test/test_apprise_emojis.py
Normal file
56
test/test_apprise_emojis.py
Normal file
@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from apprise import emojis
|
||||
import sys
|
||||
|
||||
# Disable logging for a cleaner testing output
|
||||
import logging
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
# Ensure we don't create .pyc files for these tests
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
|
||||
def test_emojis():
|
||||
"emojis: apply_emojis() testing """
|
||||
|
||||
assert emojis.apply_emojis('') == ''
|
||||
assert emojis.apply_emojis('no change') == 'no change'
|
||||
assert emojis.apply_emojis(':smile:') == '😄'
|
||||
assert emojis.apply_emojis(':smile::smile:') == '😄😄'
|
||||
|
||||
# Missing Delimiters
|
||||
assert emojis.apply_emojis(':smile') == ':smile'
|
||||
assert emojis.apply_emojis('smile:') == 'smile:'
|
||||
|
||||
# Bad data
|
||||
assert emojis.apply_emojis(None) == ''
|
||||
assert emojis.apply_emojis(object) == ''
|
||||
assert emojis.apply_emojis(True) == ''
|
||||
assert emojis.apply_emojis(4.0) == ''
|
@ -27,6 +27,9 @@
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from unittest import mock
|
||||
import requests
|
||||
from random import choice
|
||||
from string import ascii_uppercase as str_alpha
|
||||
from string import digits as str_num
|
||||
@ -34,6 +37,8 @@ from string import digits as str_num
|
||||
from apprise import NotifyBase
|
||||
from apprise.common import NotifyFormat
|
||||
from apprise.common import OverflowMode
|
||||
from apprise.AppriseAsset import AppriseAsset
|
||||
from apprise.Apprise import Apprise
|
||||
|
||||
# Disable logging for a cleaner testing output
|
||||
import logging
|
||||
@ -382,9 +387,9 @@ def test_notify_overflow_split():
|
||||
offset += len(_body)
|
||||
|
||||
|
||||
def test_notify_overflow_general():
|
||||
def test_notify_markdown_general():
|
||||
"""
|
||||
API: Overflow General Testing
|
||||
API: Markdown General Testing
|
||||
|
||||
"""
|
||||
|
||||
@ -431,3 +436,233 @@ def test_notify_overflow_general():
|
||||
# Our title get's stripped off since it's not of valid markdown
|
||||
assert body == chunks[0].get('body')
|
||||
assert chunks[0].get('title') == ""
|
||||
|
||||
|
||||
@mock.patch('requests.post')
|
||||
def test_notify_emoji_general(mock_post):
|
||||
"""
|
||||
API: Emoji General Testing
|
||||
|
||||
"""
|
||||
|
||||
# Prepare our response
|
||||
response = requests.Request()
|
||||
response.status_code = requests.codes.ok
|
||||
|
||||
# Prepare Mock
|
||||
mock_post.return_value = response
|
||||
|
||||
# Set up our emojis
|
||||
title = ":smile:"
|
||||
body = ":grin:"
|
||||
|
||||
# general reference used below (using default values)
|
||||
asset = AppriseAsset()
|
||||
|
||||
#
|
||||
# Test default emoji asset value
|
||||
#
|
||||
|
||||
# Load our object
|
||||
ap_obj = Apprise(asset=asset)
|
||||
ap_obj.add('json://localhost')
|
||||
assert len(ap_obj) == 1
|
||||
|
||||
assert ap_obj.notify(title=title, body=body) is True
|
||||
assert mock_post.call_count == 1
|
||||
|
||||
details = mock_post.call_args_list[0]
|
||||
dataset = json.loads(details[1]['data'])
|
||||
|
||||
# No changes
|
||||
assert dataset['title'] == title
|
||||
assert dataset['message'] == body
|
||||
|
||||
mock_post.reset_mock()
|
||||
|
||||
#
|
||||
# Test URL over-ride while default value set in asset
|
||||
#
|
||||
|
||||
# Load our object
|
||||
ap_obj = Apprise(asset=asset)
|
||||
ap_obj.add('json://localhost?emojis=no')
|
||||
assert len(ap_obj) == 1
|
||||
|
||||
assert ap_obj.notify(title=title, body=body) is True
|
||||
assert mock_post.call_count == 1
|
||||
|
||||
details = mock_post.call_args_list[0]
|
||||
dataset = json.loads(details[1]['data'])
|
||||
|
||||
# No changes
|
||||
assert dataset['title'] == title
|
||||
assert dataset['message'] == body
|
||||
|
||||
mock_post.reset_mock()
|
||||
|
||||
#
|
||||
# Test URL over-ride while default value set in asset pt 2
|
||||
#
|
||||
|
||||
# Load our object
|
||||
ap_obj = Apprise(asset=asset)
|
||||
ap_obj.add('json://localhost?emojis=yes')
|
||||
assert len(ap_obj) == 1
|
||||
|
||||
assert ap_obj.notify(title=title, body=body) is True
|
||||
assert mock_post.call_count == 1
|
||||
|
||||
details = mock_post.call_args_list[0]
|
||||
dataset = json.loads(details[1]['data'])
|
||||
|
||||
# Emoji's are displayed
|
||||
assert dataset['title'] == '😄'
|
||||
assert dataset['message'] == '😃'
|
||||
|
||||
mock_post.reset_mock()
|
||||
|
||||
#
|
||||
# Test URL over-ride while default value set in asset pt 2
|
||||
#
|
||||
|
||||
# Load our object
|
||||
ap_obj = Apprise(asset=asset)
|
||||
ap_obj.add('json://localhost?emojis=no')
|
||||
assert len(ap_obj) == 1
|
||||
|
||||
assert ap_obj.notify(title=title, body=body) is True
|
||||
assert mock_post.call_count == 1
|
||||
|
||||
details = mock_post.call_args_list[0]
|
||||
dataset = json.loads(details[1]['data'])
|
||||
|
||||
# No changes
|
||||
assert dataset['title'] == title
|
||||
assert dataset['message'] == body
|
||||
|
||||
mock_post.reset_mock()
|
||||
|
||||
#
|
||||
# Test Default Emoji settings
|
||||
#
|
||||
|
||||
# Set our interpret emoji's flag
|
||||
asset = AppriseAsset(interpret_emojis=True)
|
||||
|
||||
# Re-create our object
|
||||
ap_obj = Apprise(asset=asset)
|
||||
|
||||
# Load our object
|
||||
ap_obj.add('json://localhost')
|
||||
assert len(ap_obj) == 1
|
||||
|
||||
assert ap_obj.notify(title=title, body=body) is True
|
||||
assert mock_post.call_count == 1
|
||||
|
||||
details = mock_post.call_args_list[0]
|
||||
dataset = json.loads(details[1]['data'])
|
||||
|
||||
# emoji's are displayed
|
||||
assert dataset['title'] == '😄'
|
||||
assert dataset['message'] == '😃'
|
||||
|
||||
mock_post.reset_mock()
|
||||
|
||||
#
|
||||
# With Emoji's turned on by default, the user can optionally turn them
|
||||
# off.
|
||||
#
|
||||
|
||||
# Re-create our object
|
||||
ap_obj = Apprise(asset=asset)
|
||||
|
||||
ap_obj.add('json://localhost?emojis=no')
|
||||
assert len(ap_obj) == 1
|
||||
|
||||
assert ap_obj.notify(title=title, body=body) is True
|
||||
assert mock_post.call_count == 1
|
||||
|
||||
details = mock_post.call_args_list[0]
|
||||
dataset = json.loads(details[1]['data'])
|
||||
|
||||
# No changes
|
||||
assert dataset['title'] == title
|
||||
assert dataset['message'] == body
|
||||
|
||||
mock_post.reset_mock()
|
||||
|
||||
#
|
||||
# With Emoji's turned on by default, there is no change when emojis
|
||||
# flag is set to on
|
||||
#
|
||||
|
||||
# Re-create our object
|
||||
ap_obj = Apprise(asset=asset)
|
||||
|
||||
ap_obj.add('json://localhost?emojis=yes')
|
||||
assert len(ap_obj) == 1
|
||||
|
||||
assert ap_obj.notify(title=title, body=body) is True
|
||||
assert mock_post.call_count == 1
|
||||
|
||||
details = mock_post.call_args_list[0]
|
||||
dataset = json.loads(details[1]['data'])
|
||||
|
||||
# emoji's are displayed
|
||||
assert dataset['title'] == '😄'
|
||||
assert dataset['message'] == '😃'
|
||||
|
||||
mock_post.reset_mock()
|
||||
|
||||
#
|
||||
# Enforce the disabling of emojis
|
||||
#
|
||||
|
||||
# Set our interpret emoji's flag
|
||||
asset = AppriseAsset(interpret_emojis=False)
|
||||
|
||||
# Re-create our object
|
||||
ap_obj = Apprise(asset=asset)
|
||||
|
||||
# Load our object
|
||||
ap_obj.add('json://localhost')
|
||||
assert len(ap_obj) == 1
|
||||
|
||||
assert ap_obj.notify(title=title, body=body) is True
|
||||
assert mock_post.call_count == 1
|
||||
|
||||
details = mock_post.call_args_list[0]
|
||||
dataset = json.loads(details[1]['data'])
|
||||
|
||||
# Disabled - no emojis
|
||||
assert dataset['title'] == title
|
||||
assert dataset['message'] == body
|
||||
|
||||
mock_post.reset_mock()
|
||||
|
||||
#
|
||||
# Enforce the disabling of emojis
|
||||
#
|
||||
|
||||
# Set our interpret emoji's flag
|
||||
asset = AppriseAsset(interpret_emojis=False)
|
||||
|
||||
# Re-create our object
|
||||
ap_obj = Apprise(asset=asset)
|
||||
|
||||
# Load our object
|
||||
ap_obj.add('json://localhost?emojis=yes')
|
||||
assert len(ap_obj) == 1
|
||||
|
||||
assert ap_obj.notify(title=title, body=body) is True
|
||||
assert mock_post.call_count == 1
|
||||
|
||||
details = mock_post.call_args_list[0]
|
||||
dataset = json.loads(details[1]['data'])
|
||||
|
||||
# Disabled - no emojis
|
||||
assert dataset['title'] == title
|
||||
assert dataset['message'] == body
|
||||
|
||||
mock_post.reset_mock()
|
||||
|
Loading…
Reference in New Issue
Block a user