Apprise Emoji Support Added (#1011)

This commit is contained in:
Chris Caron 2023-12-15 21:59:58 -05:00 committed by GitHub
parent eb4e47cc45
commit 76831f9a8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 2649 additions and 5 deletions

View File

@ -36,6 +36,7 @@ from .utils import is_exclusive_match
from .utils import parse_list from .utils import parse_list
from .utils import parse_urls from .utils import parse_urls
from .utils import cwe312_url from .utils import cwe312_url
from .emojis import apply_emojis
from .logger import logger from .logger import logger
from .AppriseAsset import AppriseAsset from .AppriseAsset import AppriseAsset
from .AppriseConfig import AppriseConfig from .AppriseConfig import AppriseConfig
@ -376,7 +377,7 @@ class Apprise:
body, title, body, title,
notify_type=notify_type, body_format=body_format, notify_type=notify_type, body_format=body_format,
tag=tag, match_always=match_always, attach=attach, tag=tag, match_always=match_always, attach=attach,
interpret_escapes=interpret_escapes interpret_escapes=interpret_escapes,
) )
except TypeError: except TypeError:
@ -501,6 +502,11 @@ class Apprise:
key = server.notify_format if server.title_maxlen > 0\ key = server.notify_format if server.title_maxlen > 0\
else f'_{server.notify_format}' 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: if key not in conversion_title_map:
# Prepare our title # Prepare our title
@ -542,6 +548,16 @@ class Apprise:
logger.error(msg) logger.error(msg)
raise TypeError(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( kwargs = dict(
body=conversion_body_map[key], body=conversion_body_map[key],
title=conversion_title_map[key], title=conversion_title_map[key],

View File

@ -121,6 +121,12 @@ class AppriseAsset:
# notifications are sent sequentially (one after another) # notifications are sent sequentially (one after another)
async_mode = True 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 # 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 passing it upstream. Such as converting \t to an actual tab and \n
# to a new line. # to a new line.

View File

@ -213,6 +213,8 @@ def print_version_msg():
'increase the verbosity. I.e.: -vvvv') 'increase the verbosity. I.e.: -vvvv')
@click.option('--interpret-escapes', '-e', is_flag=True, @click.option('--interpret-escapes', '-e', is_flag=True,
help='Enable interpretation of backslash escapes') 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('--debug', '-D', is_flag=True, help='Debug mode')
@click.option('--version', '-V', is_flag=True, @click.option('--version', '-V', is_flag=True,
help='Display the apprise version and exit.') help='Display the apprise version and exit.')
@ -220,7 +222,8 @@ def print_version_msg():
metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',) metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',)
def main(body, title, config, attach, urls, notification_type, theme, tag, def main(body, title, config, attach, urls, notification_type, theme, tag,
input_format, dry_run, recursion_depth, verbose, disable_async, 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 Send a notification to all of the specified servers identified by their
URLs the content provided within the title, body and notification-type. 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_escapes, interpret_escapes=interpret_escapes,
# Interpret Emojis
interpret_emojis=None if not interpret_emojis else True,
# Set the theme # Set the theme
theme=theme, theme=theme,

2262
apprise/emojis.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -32,6 +32,7 @@ from functools import partial
from ..URLBase import URLBase from ..URLBase import URLBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_bool
from ..common import NOTIFY_TYPES from ..common import NOTIFY_TYPES
from ..common import NotifyFormat from ..common import NotifyFormat
from ..common import NOTIFY_FORMATS from ..common import NOTIFY_FORMATS
@ -135,6 +136,9 @@ class NotifyBase(URLBase):
# Default Overflow Mode # Default Overflow Mode
overflow_mode = OverflowMode.UPSTREAM overflow_mode = OverflowMode.UPSTREAM
# Default Emoji Interpretation
interpret_emojis = False
# Support Attachments; this defaults to being disabled. # Support Attachments; this defaults to being disabled.
# Since apprise allows you to send attachments without a body or title # Since apprise allows you to send attachments without a body or title
# defined, by letting Apprise know the plugin won't support attachments # defined, by letting Apprise know the plugin won't support attachments
@ -183,6 +187,16 @@ class NotifyBase(URLBase):
# runtime. # runtime.
'_lookup_default': 'notify_format', '_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): def __init__(self, **kwargs):
@ -194,6 +208,29 @@ class NotifyBase(URLBase):
super().__init__(**kwargs) 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: if 'format' in kwargs:
# Store the specified format if specified # Store the specified format if specified
notify_format = kwargs.get('format', '') notify_format = kwargs.get('format', '')
@ -548,6 +585,10 @@ class NotifyBase(URLBase):
results['overflow'])) results['overflow']))
del results['overflow'] del results['overflow']
# Allow emoji's override
if 'emojis' in results['qsd']:
results['emojis'] = parse_bool(results['qsd'].get('emojis'))
return results return results
@staticmethod @staticmethod

View File

@ -70,6 +70,11 @@ The Apprise options are as follows:
Enable interpretation of backslash escapes. For example, this would convert Enable interpretation of backslash escapes. For example, this would convert
sequences such as \n and \r to their respected ascii new-line and carriage 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`: `-d`, `--dry-run`:
Perform a trial run but only prints the notification services to-be Perform a trial run but only prints the notification services to-be
triggered to **stdout**. Notifications are never sent using this mode. triggered to **stdout**. Notifications are never sent using this mode.

View File

@ -1520,7 +1520,7 @@ def test_apprise_details_plugin_verification():
# General Parameters # General Parameters
'user', 'password', 'port', 'host', 'schema', 'fullpath', 'user', 'password', 'port', 'host', 'schema', 'fullpath',
# NotifyBase parameters: # NotifyBase parameters:
'format', 'overflow', 'format', 'overflow', 'emojis',
# URLBase parameters: # URLBase parameters:
'verify', 'cto', 'rto', 'verify', 'cto', 'rto',
]) ])

View File

@ -599,6 +599,23 @@ def test_apprise_cli_nux_env(tmpdir):
]) ])
assert result.exit_code == 0 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): def test_apprise_cli_details(tmpdir):
""" """

View 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) == ''

View File

@ -27,6 +27,9 @@
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
import pytest import pytest
import json
from unittest import mock
import requests
from random import choice from random import choice
from string import ascii_uppercase as str_alpha from string import ascii_uppercase as str_alpha
from string import digits as str_num from string import digits as str_num
@ -34,6 +37,8 @@ from string import digits as str_num
from apprise import NotifyBase from apprise import NotifyBase
from apprise.common import NotifyFormat from apprise.common import NotifyFormat
from apprise.common import OverflowMode from apprise.common import OverflowMode
from apprise.AppriseAsset import AppriseAsset
from apprise.Apprise import Apprise
# Disable logging for a cleaner testing output # Disable logging for a cleaner testing output
import logging import logging
@ -382,9 +387,9 @@ def test_notify_overflow_split():
offset += len(_body) 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 # Our title get's stripped off since it's not of valid markdown
assert body == chunks[0].get('body') assert body == chunks[0].get('body')
assert chunks[0].get('title') == "" 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()