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_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],

View File

@ -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.

View File

@ -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

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 ..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

View File

@ -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.

View File

@ -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',
])

View File

@ -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):
"""

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.
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()