mirror of
https://github.com/morpheus65535/bazarr.git
synced 2026-08-02 11:10:10 -04:00
1167 lines
39 KiB
Python
1167 lines
39 KiB
Python
# BSD 2-Clause License
|
|
#
|
|
# Apprise - Push Notification Library.
|
|
# Copyright (c) 2026, 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.
|
|
|
|
# For LaMetric to work, you need to first setup a custom application on their
|
|
# website. it can be done as follows:
|
|
|
|
# Cloud Mode:
|
|
# - Sign Up and login to the developer webpage https://developer.lametric.com
|
|
#
|
|
# - Create a **Indicator App** if you haven't already done so from here:
|
|
# https://developer.lametric.com/applications/sources
|
|
#
|
|
# There is a great official tutorial on how to do this here:
|
|
# https://lametric-documentation.readthedocs.io/en/latest/\
|
|
# guides/first-steps/first-lametric-indicator-app.html
|
|
#
|
|
# - Make sure to set the **Communication Type** to **PUSH**.
|
|
#
|
|
# - You will be able to **Publish** your app once you've finished setting it
|
|
# up. This will allow it to be accessible from the internet using the
|
|
# `cloud` mode of this Apprise Plugin. The **Publish** button shows up
|
|
# from within the settings of your Lametric App upon clicking on the
|
|
# **Draft Vx** folder (where `x` is the version - usually a 1)
|
|
#
|
|
# When you've completed, the site would have provided you a **PUSH URL** that
|
|
# looks like this:
|
|
# https://developer.lametric.com/api/v1/dev/widget/update/\
|
|
# com.lametric.{app_id}/{app_ver}
|
|
#
|
|
# You will need to record the `{app_id}` and `{app_ver}` to use the `cloud`
|
|
# mode.
|
|
#
|
|
# The same page should also provide you with an **Access Token**. It's
|
|
# approximately 86 characters with two equal (`=`) characters at the end of it.
|
|
# This becomes your `{app_token}`. Here is an example of what one might
|
|
# look like:
|
|
# K2MxWI0NzU0ZmI2NjJlZYTgViMDgDRiN8YjlmZjRmNTc4NDVhJzk0RiNjNh0EyKWW==`
|
|
#
|
|
# The syntax for the cloud mode is:
|
|
# * `lametric://{app_token}@{app_id}/{app_ver}?mode=cloud`
|
|
|
|
# Device Mode:
|
|
# - Sign Up and login to the developer webpage https://developer.lametric.com
|
|
# - Locate your Device API Key; you can find it here:
|
|
# https://developer.lametric.com/user/devices
|
|
# - From here you can get your your API Key for the device you plan to notify.
|
|
# - Your devices IP Address can be found in LaMetric Time app at:
|
|
# Settings -> Wi-Fi -> IP Address
|
|
#
|
|
# The syntax for the device mode is:
|
|
# * `lametric://{apikey}@{host}`
|
|
|
|
# A great source for API examples (Device Mode):
|
|
# - https://lametric-documentation.readthedocs.io/en/latest/reference-docs\
|
|
# /device-notifications.html
|
|
#
|
|
# A great source for API examples (Cloud Mode):
|
|
# - https://lametric-documentation.readthedocs.io/en/latest/reference-docs\
|
|
# /lametric-cloud-reference.html
|
|
|
|
# A great source for the icon reference:
|
|
# - https://developer.lametric.com/icons
|
|
|
|
|
|
import contextlib
|
|
from json import dumps
|
|
import re
|
|
|
|
import requests
|
|
|
|
from ..common import NotifyType
|
|
from ..locale import gettext_lazy as _
|
|
from ..utils.parse import is_hostname, is_ipaddr, validate_regex
|
|
from .base import NotifyBase
|
|
|
|
# A URL Parser to detect App ID
|
|
LAMETRIC_APP_ID_DETECTOR_RE = re.compile(
|
|
r"(com\.lametric\.)?(?P<app_id>[0-9a-z.-]{1,64})"
|
|
r"(/(?P<app_ver>[1-9][0-9]*))?",
|
|
re.I,
|
|
)
|
|
|
|
# Tokens are huge
|
|
LAMETRIC_IS_APP_TOKEN = re.compile(r"^[a-z0-9]{80,}==$", re.I)
|
|
|
|
|
|
class LametricMode:
|
|
"""Define Lametric Notification Modes."""
|
|
|
|
# App posts upstream to the developer API on Lametric's website
|
|
CLOUD = "cloud"
|
|
|
|
# Device mode posts directly to the device that you identify
|
|
DEVICE = "device"
|
|
|
|
|
|
LAMETRIC_MODES = (
|
|
LametricMode.CLOUD,
|
|
LametricMode.DEVICE,
|
|
)
|
|
|
|
|
|
class LametricPriority:
|
|
"""Priority of the message."""
|
|
|
|
# info: this priority means that notification will be displayed on the
|
|
# same “level” as all other notifications on the device that come
|
|
# from apps (for example facebook app). This notification will not
|
|
# be shown when screensaver is active. By default message is sent
|
|
# with "info" priority. This level of notification should be used
|
|
# for notifications like news, weather, temperature, etc.
|
|
INFO = "info"
|
|
|
|
# warning: notifications with this priority will interrupt ones sent with
|
|
# lower priority (“info”). Should be used to notify the user
|
|
# about something important but not critical. For example,
|
|
# events like “someone is coming home” should use this priority
|
|
# when sending notifications from smart home.
|
|
WARNING = "warning"
|
|
|
|
# critical: the most important notifications. Interrupts notification
|
|
# with priority info or warning and is displayed even if
|
|
# screensaver is active. Use with care as these notifications
|
|
# can pop in the middle of the night. Must be used only for
|
|
# really important notifications like notifications from smoke
|
|
# detectors, water leak sensors, etc. Use it for events that
|
|
# require human interaction immediately.
|
|
CRITICAL = "critical"
|
|
|
|
|
|
LAMETRIC_PRIORITIES = (
|
|
LametricPriority.INFO,
|
|
LametricPriority.WARNING,
|
|
LametricPriority.CRITICAL,
|
|
)
|
|
|
|
|
|
class LametricIconType:
|
|
"""Represents the nature of notification."""
|
|
|
|
# info - "i" icon will be displayed prior to the notification. Means that
|
|
# notification contains information, no need to take actions on it.
|
|
INFO = "info"
|
|
|
|
# alert: "!!!" icon will be displayed prior to the notification. Use it
|
|
# when you want the user to pay attention to that notification as
|
|
# it indicates that something bad happened and user must take
|
|
# immediate action.
|
|
ALERT = "alert"
|
|
|
|
# none: no notification icon will be shown.
|
|
NONE = "none"
|
|
|
|
|
|
LAMETRIC_ICON_TYPES = (
|
|
LametricIconType.INFO,
|
|
LametricIconType.ALERT,
|
|
LametricIconType.NONE,
|
|
)
|
|
|
|
|
|
class LametricSoundCategory:
|
|
"""Define Sound Categories."""
|
|
|
|
NOTIFICATIONS = "notifications"
|
|
ALARMS = "alarms"
|
|
|
|
|
|
class LametricSound:
|
|
"""There are 2 categories of sounds, to make things simple we just lump
|
|
them all togther in one class object.
|
|
|
|
Syntax is (Category, (AlarmID, Alias1, Alias2, ...))
|
|
"""
|
|
|
|
# Alarm Category Sounds
|
|
ALARM01 = (LametricSoundCategory.ALARMS, ("alarm1", "a1", "a01"))
|
|
ALARM02 = (LametricSoundCategory.ALARMS, ("alarm2", "a2", "a02"))
|
|
ALARM03 = (LametricSoundCategory.ALARMS, ("alarm3", "a3", "a03"))
|
|
ALARM04 = (LametricSoundCategory.ALARMS, ("alarm4", "a4", "a04"))
|
|
ALARM05 = (LametricSoundCategory.ALARMS, ("alarm5", "a5", "a05"))
|
|
ALARM06 = (LametricSoundCategory.ALARMS, ("alarm6", "a6", "a06"))
|
|
ALARM07 = (LametricSoundCategory.ALARMS, ("alarm7", "a7", "a07"))
|
|
ALARM08 = (LametricSoundCategory.ALARMS, ("alarm8", "a8", "a08"))
|
|
ALARM09 = (LametricSoundCategory.ALARMS, ("alarm9", "a9", "a09"))
|
|
ALARM10 = (LametricSoundCategory.ALARMS, ("alarm10", "a10"))
|
|
ALARM11 = (LametricSoundCategory.ALARMS, ("alarm11", "a11"))
|
|
ALARM12 = (LametricSoundCategory.ALARMS, ("alarm12", "a12"))
|
|
ALARM13 = (LametricSoundCategory.ALARMS, ("alarm13", "a13"))
|
|
|
|
# Notification Category Sounds
|
|
BICYCLE = (LametricSoundCategory.NOTIFICATIONS, ("bicycle", "bike"))
|
|
CAR = (LametricSoundCategory.NOTIFICATIONS, ("car",))
|
|
CASH = (LametricSoundCategory.NOTIFICATIONS, ("cash",))
|
|
CAT = (LametricSoundCategory.NOTIFICATIONS, ("cat",))
|
|
DOG01 = (LametricSoundCategory.NOTIFICATIONS, ("dog", "dog1", "dog01"))
|
|
DOG02 = (LametricSoundCategory.NOTIFICATIONS, ("dog2", "dog02"))
|
|
ENERGY = (LametricSoundCategory.NOTIFICATIONS, ("energy",))
|
|
KNOCK = (LametricSoundCategory.NOTIFICATIONS, ("knock-knock", "knock"))
|
|
EMAIL = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("letter_email", "letter", "email"),
|
|
)
|
|
LOSE01 = (LametricSoundCategory.NOTIFICATIONS, ("lose1", "lose01", "lose"))
|
|
LOSE02 = (LametricSoundCategory.NOTIFICATIONS, ("lose2", "lose02"))
|
|
NEGATIVE01 = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("negative1", "negative01", "neg01", "neg1", "-"),
|
|
)
|
|
NEGATIVE02 = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("negative2", "negative02", "neg02", "neg2", "--"),
|
|
)
|
|
NEGATIVE03 = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("negative3", "negative03", "neg03", "neg3", "---"),
|
|
)
|
|
NEGATIVE04 = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("negative4", "negative04", "neg04", "neg4", "----"),
|
|
)
|
|
NEGATIVE05 = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("negative5", "negative05", "neg05", "neg5", "-----"),
|
|
)
|
|
NOTIFICATION01 = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("notification", "notification1", "notification01", "not01", "not1"),
|
|
)
|
|
NOTIFICATION02 = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("notification2", "notification02", "not02", "not2"),
|
|
)
|
|
NOTIFICATION03 = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("notification3", "notification03", "not03", "not3"),
|
|
)
|
|
NOTIFICATION04 = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("notification4", "notification04", "not04", "not4"),
|
|
)
|
|
OPEN_DOOR = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("open_door", "open", "door"),
|
|
)
|
|
POSITIVE01 = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("positive1", "positive01", "pos01", "p1", "+"),
|
|
)
|
|
POSITIVE02 = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("positive2", "positive02", "pos02", "p2", "++"),
|
|
)
|
|
POSITIVE03 = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("positive3", "positive03", "pos03", "p3", "+++"),
|
|
)
|
|
POSITIVE04 = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("positive4", "positive04", "pos04", "p4", "++++"),
|
|
)
|
|
POSITIVE05 = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("positive5", "positive05", "pos05", "p5", "+++++"),
|
|
)
|
|
POSITIVE06 = (
|
|
LametricSoundCategory.NOTIFICATIONS,
|
|
("positive6", "positive06", "pos06", "p6", "++++++"),
|
|
)
|
|
STATISTIC = (LametricSoundCategory.NOTIFICATIONS, ("statistic", "stat"))
|
|
THUNDER = (LametricSoundCategory.NOTIFICATIONS, "thunder")
|
|
WATER01 = (LametricSoundCategory.NOTIFICATIONS, ("water1", "water01"))
|
|
WATER02 = (LametricSoundCategory.NOTIFICATIONS, ("water2", "water02"))
|
|
WIN01 = (LametricSoundCategory.NOTIFICATIONS, ("win", "win01", "win1"))
|
|
WIN02 = (LametricSoundCategory.NOTIFICATIONS, ("win2", "win02"))
|
|
WIND = (LametricSoundCategory.NOTIFICATIONS, ("wind",))
|
|
WIND_SHORT = (LametricSoundCategory.NOTIFICATIONS, ("wind_short",))
|
|
|
|
|
|
# A listing of all the sounds; the order DOES matter, content is read from
|
|
# top down and then right to left (over aliases). Longer similar sounding
|
|
# elements should be placed higher in the list over others. for example
|
|
# ALARM10 should come before ALARM01 (because ALARM01 can match on 'alarm1'
|
|
# which is very close to 'alarm10'
|
|
LAMETRIC_SOUNDS = (
|
|
# Alarm Category Entries
|
|
LametricSound.ALARM13,
|
|
LametricSound.ALARM12,
|
|
LametricSound.ALARM11,
|
|
LametricSound.ALARM10,
|
|
LametricSound.ALARM09,
|
|
LametricSound.ALARM08,
|
|
LametricSound.ALARM07,
|
|
LametricSound.ALARM06,
|
|
LametricSound.ALARM05,
|
|
LametricSound.ALARM04,
|
|
LametricSound.ALARM03,
|
|
LametricSound.ALARM02,
|
|
LametricSound.ALARM01,
|
|
# Notification Category Entries
|
|
LametricSound.BICYCLE,
|
|
LametricSound.CAR,
|
|
LametricSound.CASH,
|
|
LametricSound.CAT,
|
|
LametricSound.DOG02,
|
|
LametricSound.DOG01,
|
|
LametricSound.ENERGY,
|
|
LametricSound.KNOCK,
|
|
LametricSound.EMAIL,
|
|
LametricSound.LOSE02,
|
|
LametricSound.LOSE01,
|
|
LametricSound.NEGATIVE01,
|
|
LametricSound.NEGATIVE02,
|
|
LametricSound.NEGATIVE03,
|
|
LametricSound.NEGATIVE04,
|
|
LametricSound.NEGATIVE05,
|
|
LametricSound.NOTIFICATION04,
|
|
LametricSound.NOTIFICATION03,
|
|
LametricSound.NOTIFICATION02,
|
|
LametricSound.NOTIFICATION01,
|
|
LametricSound.OPEN_DOOR,
|
|
LametricSound.POSITIVE01,
|
|
LametricSound.POSITIVE02,
|
|
LametricSound.POSITIVE03,
|
|
LametricSound.POSITIVE04,
|
|
LametricSound.POSITIVE05,
|
|
LametricSound.POSITIVE01,
|
|
LametricSound.STATISTIC,
|
|
LametricSound.THUNDER,
|
|
LametricSound.WATER02,
|
|
LametricSound.WATER01,
|
|
LametricSound.WIND,
|
|
LametricSound.WIND_SHORT,
|
|
LametricSound.WIN01,
|
|
LametricSound.WIN02,
|
|
)
|
|
|
|
|
|
class NotifyLametric(NotifyBase):
|
|
"""A wrapper for LaMetric Notifications."""
|
|
|
|
# The default descriptive name associated with the Notification
|
|
service_name = "LaMetric"
|
|
|
|
# The services URL
|
|
service_url = "https://lametric.com"
|
|
|
|
# The default protocol
|
|
protocol = "lametric"
|
|
|
|
# The default secure protocol
|
|
secure_protocol = "lametrics"
|
|
|
|
# Allow 300 requests per minute.
|
|
# 60/300 = 0.2
|
|
request_rate_per_sec = 0.20
|
|
|
|
# A URL that takes you to the setup/help of the specific protocol
|
|
setup_url = "https://appriseit.com/services/lametric/"
|
|
|
|
# Lametric does have titles when creating a message
|
|
title_maxlen = 0
|
|
|
|
# URL used for notifying Lametric App's created in the Dev Portal
|
|
cloud_notify_url = (
|
|
"https://developer.lametric.com/api/v1"
|
|
"/dev/widget/update/com.lametric.{app_id}/{app_ver}"
|
|
)
|
|
|
|
# URL used for local notifications directly to the device
|
|
device_notify_url = "{schema}://{host}{port}/api/v2/device/notifications"
|
|
|
|
# The Device User ID
|
|
default_device_user = "dev"
|
|
|
|
# Track all icon mappings back to Apprise Icon NotifyType's
|
|
# See: https://developer.lametric.com/icons
|
|
# Icon ID looks like <prefix>XXX, where <prefix> is:
|
|
# - "i" (for static icon)
|
|
# - "a" (for animation)
|
|
# - XXX - is the number of the icon and can be found at:
|
|
# https://developer.lametric.com/icons
|
|
lametric_icon_id_mapping = {
|
|
# 620/Info
|
|
NotifyType.INFO: "i620",
|
|
# 9182/info_good
|
|
NotifyType.SUCCESS: "i9182",
|
|
# 9183/info_caution
|
|
NotifyType.WARNING: "i9183",
|
|
# 9184/info_error
|
|
NotifyType.FAILURE: "i9184",
|
|
}
|
|
|
|
# Define object templates
|
|
templates = (
|
|
# Cloud (App) Mode
|
|
"{schema}://{app_token}@{app_id}",
|
|
"{schema}://{app_token}@{app_id}/{app_ver}",
|
|
# Device Mode
|
|
"{schema}://{apikey}@{host}",
|
|
"{schema}://{user}:{apikey}@{host}",
|
|
"{schema}://{apikey}@{host}:{port}",
|
|
"{schema}://{user}:{apikey}@{host}:{port}",
|
|
)
|
|
|
|
# Define our template tokens
|
|
template_tokens = dict(
|
|
NotifyBase.template_tokens,
|
|
**{
|
|
# Used for Local Device mode
|
|
"apikey": {
|
|
"name": _("Device API Key"),
|
|
"type": "string",
|
|
"private": True,
|
|
},
|
|
# Used for Cloud mode
|
|
"app_id": {
|
|
"name": _("App ID"),
|
|
"type": "string",
|
|
"private": True,
|
|
},
|
|
# Used for Cloud mode
|
|
"app_ver": {
|
|
"name": _("App Version"),
|
|
"type": "string",
|
|
"regex": (r"^[1-9][0-9]*$", ""),
|
|
"default": "1",
|
|
},
|
|
# Used for Cloud mode
|
|
"app_token": {
|
|
"name": _("App Access Token"),
|
|
"type": "string",
|
|
"regex": (r"^[A-Z0-9]{80,}==$", "i"),
|
|
},
|
|
"host": {
|
|
"name": _("Hostname"),
|
|
"type": "string",
|
|
},
|
|
"port": {
|
|
"name": _("Port"),
|
|
"type": "int",
|
|
"min": 1,
|
|
"max": 65535,
|
|
"default": 8080,
|
|
},
|
|
"user": {
|
|
"name": _("Username"),
|
|
"type": "string",
|
|
},
|
|
},
|
|
)
|
|
|
|
# Define our template arguments
|
|
template_args = dict(
|
|
NotifyBase.template_args,
|
|
**{
|
|
"apikey": {
|
|
"alias_of": "apikey",
|
|
},
|
|
"app_id": {
|
|
"alias_of": "app_id",
|
|
},
|
|
"app_ver": {
|
|
"alias_of": "app_ver",
|
|
},
|
|
"app_token": {
|
|
"alias_of": "app_token",
|
|
},
|
|
"priority": {
|
|
"name": _("Priority"),
|
|
"type": "choice:string",
|
|
"values": LAMETRIC_PRIORITIES,
|
|
"default": LametricPriority.INFO,
|
|
},
|
|
"icon": {
|
|
"name": _("Custom Icon"),
|
|
"type": "string",
|
|
},
|
|
"icon_type": {
|
|
"name": _("Icon Type"),
|
|
"type": "choice:string",
|
|
"values": LAMETRIC_ICON_TYPES,
|
|
"default": LametricIconType.NONE,
|
|
},
|
|
"mode": {
|
|
"name": _("Mode"),
|
|
"type": "choice:string",
|
|
"values": LAMETRIC_MODES,
|
|
"default": LametricMode.DEVICE,
|
|
},
|
|
"sound": {
|
|
"name": _("Sound"),
|
|
"type": "string",
|
|
},
|
|
# Lifetime is in seconds
|
|
"cycles": {
|
|
"name": _("Cycles"),
|
|
"type": "int",
|
|
"min": 0,
|
|
"default": 1,
|
|
},
|
|
},
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
apikey=None,
|
|
app_token=None,
|
|
app_id=None,
|
|
app_ver=None,
|
|
priority=None,
|
|
icon=None,
|
|
icon_type=None,
|
|
sound=None,
|
|
mode=None,
|
|
cycles=None,
|
|
**kwargs,
|
|
):
|
|
"""Initialize LaMetric Object."""
|
|
super().__init__(**kwargs)
|
|
|
|
self.mode = (
|
|
mode.strip().lower()
|
|
if isinstance(mode, str)
|
|
else self.template_args["mode"]["default"]
|
|
)
|
|
|
|
# Default Cloud Argument
|
|
self.lametric_app_id = None
|
|
self.lametric_app_ver = None
|
|
self.lametric_app_access_token = None
|
|
|
|
# Default Device/Cloud Argument
|
|
self.lametric_apikey = None
|
|
|
|
if self.mode not in LAMETRIC_MODES:
|
|
msg = f"An invalid LaMetric Mode ({mode}) was specified."
|
|
self.logger.warning(msg)
|
|
raise TypeError(msg)
|
|
|
|
if self.mode == LametricMode.CLOUD:
|
|
try:
|
|
results = LAMETRIC_APP_ID_DETECTOR_RE.match(app_id)
|
|
except TypeError:
|
|
msg = (
|
|
"An invalid LaMetric Application ID "
|
|
f"({app_id}) was specified."
|
|
)
|
|
self.logger.warning(msg)
|
|
raise TypeError(msg) from None
|
|
|
|
# Detect our Access Token
|
|
self.lametric_app_access_token = validate_regex(
|
|
app_token, *self.template_tokens["app_token"]["regex"]
|
|
)
|
|
if not self.lametric_app_access_token:
|
|
msg = (
|
|
"An invalid LaMetric Application Access Token "
|
|
f"({app_token}) was specified."
|
|
)
|
|
self.logger.warning(msg)
|
|
raise TypeError(msg)
|
|
|
|
# If app_ver is specified, it over-rides all
|
|
if app_ver:
|
|
self.lametric_app_ver = validate_regex(
|
|
app_ver, *self.template_tokens["app_ver"]["regex"]
|
|
)
|
|
if not self.lametric_app_ver:
|
|
msg = (
|
|
"An invalid LaMetric Application Version "
|
|
f"({app_ver}) was specified."
|
|
)
|
|
self.logger.warning(msg)
|
|
raise TypeError(msg)
|
|
|
|
else:
|
|
# If app_ver wasn't specified, we parse it from the
|
|
# Application ID
|
|
self.lametric_app_ver = (
|
|
results.group("app_ver")
|
|
if results.group("app_ver")
|
|
else self.template_tokens["app_ver"]["default"]
|
|
)
|
|
|
|
# Store our Application ID
|
|
self.lametric_app_id = results.group("app_id")
|
|
|
|
if self.mode == LametricMode.DEVICE:
|
|
self.lametric_apikey = validate_regex(apikey)
|
|
if not self.lametric_apikey:
|
|
msg = (
|
|
"An invalid LaMetric Device API Key "
|
|
f"({apikey}) was specified."
|
|
)
|
|
self.logger.warning(msg)
|
|
raise TypeError(msg)
|
|
|
|
if priority not in LAMETRIC_PRIORITIES:
|
|
self.priority = self.template_args["priority"]["default"]
|
|
|
|
else:
|
|
self.priority = priority
|
|
|
|
# assign our icon (if it was defined); we also eliminate
|
|
# any hashtag (#) entries that might be present
|
|
self.icon = (
|
|
re.search(r"[#\s]*(?P<value>.+?)\s*$", icon).group("value")
|
|
if isinstance(icon, str)
|
|
else None
|
|
)
|
|
|
|
if icon_type not in LAMETRIC_ICON_TYPES:
|
|
self.icon_type = self.template_args["icon_type"]["default"]
|
|
|
|
else:
|
|
self.icon_type = icon_type
|
|
|
|
# The number of times the message should be displayed
|
|
self.cycles = (
|
|
self.template_args["cycles"]["default"]
|
|
if not (
|
|
isinstance(cycles, int)
|
|
and cycles > self.template_args["cycles"]["min"]
|
|
)
|
|
else cycles
|
|
)
|
|
|
|
self.sound = None
|
|
if isinstance(sound, str):
|
|
# If sound is set, get it's match
|
|
self.sound = self.sound_lookup(sound.strip().lower())
|
|
if self.sound is None:
|
|
self.logger.warning(
|
|
f"An invalid LaMetric sound ({sound}) was specified."
|
|
)
|
|
return
|
|
|
|
@staticmethod
|
|
def sound_lookup(lookup):
|
|
"""A simple match function that takes string and returns the
|
|
LametricSound object it was found in."""
|
|
|
|
for x in LAMETRIC_SOUNDS:
|
|
match = next((f for f in x[1] if f.startswith(lookup)), None)
|
|
if match:
|
|
# We're done
|
|
return x
|
|
|
|
# No match was found
|
|
return None
|
|
|
|
def _cloud_notification_payload(self, body, notify_type, headers):
|
|
"""Return URL and payload for cloud directed requests."""
|
|
|
|
# Update header entries
|
|
headers.update(
|
|
{
|
|
"X-Access-Token": self.lametric_apikey,
|
|
}
|
|
)
|
|
|
|
if self.sound:
|
|
self.logger.warning(
|
|
"LaMetric sound setting is unavailable in Cloud mode"
|
|
)
|
|
|
|
if self.priority != self.template_args["priority"]["default"]:
|
|
self.logger.warning(
|
|
"LaMetric priority setting is unavailable in Cloud mode"
|
|
)
|
|
|
|
if self.icon_type != self.template_args["icon_type"]["default"]:
|
|
self.logger.warning(
|
|
"LaMetric icon_type setting is unavailable in Cloud mode"
|
|
)
|
|
|
|
if self.cycles != self.template_args["cycles"]["default"]:
|
|
self.logger.warning(
|
|
"LaMetric cycle settings is unavailable in Cloud mode"
|
|
)
|
|
|
|
# Assign our icon if the user specified a custom one, otherwise
|
|
# choose from our pre-set list (based on notify_type)
|
|
icon = (
|
|
self.icon
|
|
if self.icon
|
|
else self.lametric_icon_id_mapping[notify_type]
|
|
)
|
|
|
|
# Our Payload
|
|
# Cloud Notifications don't have as much functionality
|
|
# You can not set priority and/or sound
|
|
payload = {
|
|
"frames": [
|
|
{
|
|
"icon": icon,
|
|
"text": body,
|
|
"index": 0,
|
|
}
|
|
]
|
|
}
|
|
|
|
# Prepare our Cloud Notify URL
|
|
notify_url = self.cloud_notify_url.format(
|
|
app_id=self.lametric_app_id, app_ver=self.lametric_app_ver
|
|
)
|
|
|
|
# Return request parameters
|
|
return (notify_url, None, payload)
|
|
|
|
def _device_notification_payload(self, body, notify_type, headers):
|
|
"""Return URL and Payload for Device directed requests."""
|
|
|
|
# Assign our icon if the user specified a custom one, otherwise
|
|
# choose from our pre-set list (based on notify_type)
|
|
icon = (
|
|
self.icon
|
|
if self.icon
|
|
else self.lametric_icon_id_mapping[notify_type]
|
|
)
|
|
|
|
# Our Payload
|
|
payload = {
|
|
# Priority of the message
|
|
"priority": self.priority,
|
|
# Icon Type: Represents the nature of notification
|
|
"icon_type": self.icon_type,
|
|
# The time notification lives in queue to be displayed in
|
|
# milliseconds (ms). The default lifetime is 2 minutes (120000ms).
|
|
# If notification stayed in queue for longer than lifetime
|
|
# milliseconds - it will not be displayed.
|
|
"lifetime": 120000,
|
|
"model": {
|
|
# cycles - the number of times message should be displayed. If
|
|
# cycles is set to 0, notification will stay on the screen
|
|
# until user dismisses it manually. By default it is set to 1.
|
|
"cycles": self.cycles,
|
|
"frames": [
|
|
{
|
|
"icon": icon,
|
|
"text": body,
|
|
}
|
|
],
|
|
},
|
|
}
|
|
|
|
if self.sound:
|
|
# Sound was set, so add it to the payload
|
|
payload["model"]["sound"] = {
|
|
# The sound category
|
|
"category": self.sound[0],
|
|
# The first element of our tuple is always the id
|
|
"id": self.sound[1][0],
|
|
# repeat - defines the number of times sound must be played.
|
|
# If set to 0 sound will be played until notification is
|
|
# dismissed. By default the value is set to 1.
|
|
"repeat": 1,
|
|
}
|
|
|
|
if not self.user:
|
|
# Use default user if there wasn't one otherwise specified
|
|
self.user = self.default_device_user
|
|
|
|
# Prepare our authentication
|
|
auth = (self.user, self.password)
|
|
|
|
# Prepare our Direct Access Notify URL
|
|
notify_url = self.device_notify_url.format(
|
|
schema="https" if self.secure else "http",
|
|
host=self.host,
|
|
port=":{}".format(
|
|
self.port
|
|
if self.port
|
|
else self.template_tokens["port"]["default"]
|
|
),
|
|
)
|
|
|
|
# Return request parameters
|
|
return (notify_url, auth, payload)
|
|
|
|
def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
|
|
"""Perform LaMetric Notification."""
|
|
|
|
# Prepare our headers:
|
|
headers = {
|
|
"User-Agent": self.app_id,
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json",
|
|
"Cache-Control": "no-cache",
|
|
}
|
|
|
|
# Depending on the mode, the payload is gathered by
|
|
# - _device_notification_payload()
|
|
# - _cloud_notification_payload()
|
|
(notify_url, auth, payload) = getattr(
|
|
self, f"_{self.mode}_notification_payload"
|
|
)(body=body, notify_type=notify_type, headers=headers)
|
|
|
|
self.logger.debug(
|
|
"LaMetric POST URL:"
|
|
f" {notify_url} (cert_verify={self.verify_certificate!r})"
|
|
)
|
|
self.logger.debug(f"LaMetric Payload: {payload!s}")
|
|
|
|
# Always call throttle before any remote server i/o is made
|
|
self.throttle()
|
|
|
|
try:
|
|
r = requests.post(
|
|
notify_url,
|
|
data=dumps(payload),
|
|
headers=headers,
|
|
auth=auth,
|
|
verify=self.verify_certificate,
|
|
timeout=self.request_timeout,
|
|
allow_redirects=self.redirects,
|
|
)
|
|
# An ideal response would be:
|
|
# {
|
|
# "success": {
|
|
# "id": "<notification id>"
|
|
# }
|
|
# }
|
|
|
|
if r.status_code not in (
|
|
requests.codes.created,
|
|
requests.codes.ok,
|
|
):
|
|
# We had a problem
|
|
status_str = NotifyLametric.http_response_code_lookup(
|
|
r.status_code
|
|
)
|
|
|
|
self.logger.warning(
|
|
"Failed to send LaMetric notification: "
|
|
"{}{}error={}.".format(
|
|
status_str, ", " if status_str else "", r.status_code
|
|
)
|
|
)
|
|
|
|
self.logger.debug(
|
|
"Response Details:\r\n%r", (r.content or b"")[:2000]
|
|
)
|
|
|
|
# Return; we're done
|
|
return False
|
|
|
|
else:
|
|
self.logger.info("Sent LaMetric notification.")
|
|
|
|
except requests.RequestException as e:
|
|
self.logger.warning(
|
|
"A Connection error occurred sending LaMetric "
|
|
f"notification to {self.host}."
|
|
)
|
|
self.logger.debug(f"Socket Exception: {e!s}")
|
|
|
|
# Return; we're done
|
|
return False
|
|
|
|
return True
|
|
|
|
@property
|
|
def url_identifier(self):
|
|
"""Returns all of the identifiers that make this URL unique from
|
|
another simliar one.
|
|
|
|
Targets or end points should never be identified here.
|
|
"""
|
|
if self.mode == LametricMode.DEVICE:
|
|
return (
|
|
self.secure_protocol if self.secure else self.protocol,
|
|
self.user,
|
|
self.lametric_apikey,
|
|
self.host,
|
|
(
|
|
self.port
|
|
if self.port
|
|
else (
|
|
443
|
|
if self.secure
|
|
else self.template_tokens["port"]["default"]
|
|
)
|
|
),
|
|
)
|
|
|
|
return (
|
|
self.protocol,
|
|
self.lametric_app_access_token,
|
|
self.lametric_app_id,
|
|
self.lametric_app_ver,
|
|
)
|
|
|
|
def url(self, privacy=False, *args, **kwargs):
|
|
"""Returns the URL built dynamically based on specified arguments."""
|
|
|
|
# Define any URL parameters
|
|
params = {
|
|
"mode": self.mode,
|
|
}
|
|
|
|
# Extend our parameters
|
|
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
|
|
|
if self.icon:
|
|
# Assign our icon IF one was specified
|
|
params["icon"] = self.icon
|
|
|
|
if self.mode == LametricMode.CLOUD:
|
|
# Upstream/LaMetric App Return
|
|
return "{schema}://{token}@{app_id}/{app_ver}/?{params}".format(
|
|
schema=self.protocol,
|
|
token=self.pprint(
|
|
self.lametric_app_access_token, privacy, safe=""
|
|
),
|
|
app_id=self.pprint(self.lametric_app_id, privacy, safe=""),
|
|
app_ver=NotifyLametric.quote(self.lametric_app_ver, safe=""),
|
|
params=NotifyLametric.urlencode(params),
|
|
)
|
|
|
|
#
|
|
# If we reach here then we're dealing with LametricMode.DEVICE
|
|
#
|
|
if self.priority != self.template_args["priority"]["default"]:
|
|
params["priority"] = self.priority
|
|
|
|
if self.icon_type != self.template_args["icon_type"]["default"]:
|
|
params["icon_type"] = self.icon_type
|
|
|
|
if self.cycles != self.template_args["cycles"]["default"]:
|
|
params["cycles"] = self.cycles
|
|
|
|
if self.sound:
|
|
# Store our sound entry
|
|
# The first element of our tuple is always the id
|
|
params["sound"] = self.sound[1][0]
|
|
|
|
auth = ""
|
|
if self.user and self.password:
|
|
auth = "{user}:{apikey}@".format(
|
|
user=NotifyLametric.quote(self.user, safe=""),
|
|
apikey=self.pprint(self.lametric_apikey, privacy, safe=""),
|
|
)
|
|
else: # self.apikey is set
|
|
auth = "{apikey}@".format(
|
|
apikey=self.pprint(self.lametric_apikey, privacy, safe=""),
|
|
)
|
|
|
|
# Local Return
|
|
return "{schema}://{auth}{hostname}{port}/?{params}".format(
|
|
schema=self.secure_protocol if self.secure else self.protocol,
|
|
auth=auth,
|
|
# never encode hostname since we're expecting it to be a valid one
|
|
hostname=self.host,
|
|
port=(
|
|
""
|
|
if self.port is None
|
|
or self.port == self.template_tokens["port"]["default"]
|
|
else f":{self.port}"
|
|
),
|
|
params=NotifyLametric.urlencode(params),
|
|
)
|
|
|
|
@staticmethod
|
|
def parse_url(url):
|
|
"""Parses the URL and returns enough arguments that can allow us to re-
|
|
instantiate this object."""
|
|
|
|
results = NotifyBase.parse_url(url, verify_host=False)
|
|
if not results:
|
|
# We're done early as we couldn't load the results
|
|
return results
|
|
|
|
if results.get("user") and not results.get("password"):
|
|
# Handle URL like:
|
|
# schema://user@host
|
|
|
|
# This becomes the password
|
|
results["password"] = results["user"]
|
|
results["user"] = None
|
|
|
|
# Get unquoted entries
|
|
entries = NotifyLametric.split_path(results["fullpath"])
|
|
|
|
# Priority Handling
|
|
if "priority" in results["qsd"] and results["qsd"]["priority"]:
|
|
results["priority"] = NotifyLametric.unquote(
|
|
results["qsd"]["priority"].strip().lower()
|
|
)
|
|
|
|
# Icon Type
|
|
if "icon" in results["qsd"] and results["qsd"]["icon"]:
|
|
results["icon"] = NotifyLametric.unquote(
|
|
results["qsd"]["icon"].strip().lower()
|
|
)
|
|
|
|
# Icon Type
|
|
if "icon_type" in results["qsd"] and results["qsd"]["icon_type"]:
|
|
results["icon_type"] = NotifyLametric.unquote(
|
|
results["qsd"]["icon_type"].strip().lower()
|
|
)
|
|
|
|
# Sound
|
|
if "sound" in results["qsd"] and results["qsd"]["sound"]:
|
|
results["sound"] = NotifyLametric.unquote(
|
|
results["qsd"]["sound"].strip().lower()
|
|
)
|
|
|
|
# API Key (Device Mode)
|
|
if "apikey" in results["qsd"] and results["qsd"]["apikey"]:
|
|
# Extract API Key from an argument
|
|
results["apikey"] = NotifyLametric.unquote(
|
|
results["qsd"]["apikey"]
|
|
)
|
|
|
|
# App ID
|
|
if "app" in results["qsd"] and results["qsd"]["app"]:
|
|
# Extract the App ID from an argument
|
|
results["app_id"] = NotifyLametric.unquote(results["qsd"]["app"])
|
|
|
|
# App Version
|
|
if "app_ver" in results["qsd"] and results["qsd"]["app_ver"]:
|
|
# Extract the App ID from an argument
|
|
results["app_ver"] = NotifyLametric.unquote(
|
|
results["qsd"]["app_ver"]
|
|
)
|
|
|
|
elif entries:
|
|
# Store our app id
|
|
results["app_ver"] = entries.pop(0)
|
|
|
|
if "token" in results["qsd"] and results["qsd"]["token"]:
|
|
# Extract Application Access Token from an argument
|
|
results["app_token"] = NotifyLametric.unquote(
|
|
results["qsd"]["token"]
|
|
)
|
|
|
|
# Mode override
|
|
if "mode" in results["qsd"] and results["qsd"]["mode"]:
|
|
results["mode"] = NotifyLametric.unquote(
|
|
results["qsd"]["mode"].strip().lower()
|
|
)
|
|
else:
|
|
# We can try to detect the mode based on the validity of the
|
|
# hostname. We can also scan the validity of the Application
|
|
# Access token
|
|
#
|
|
# This isn't a surfire way to do things though; it's best to
|
|
# specify the mode= flag
|
|
results["mode"] = (
|
|
LametricMode.DEVICE
|
|
if (
|
|
(
|
|
is_hostname(results["host"])
|
|
or is_ipaddr(results["host"])
|
|
)
|
|
and
|
|
# make sure password is not an Access Token
|
|
(
|
|
results["password"]
|
|
and not LAMETRIC_IS_APP_TOKEN.match(
|
|
results["password"]
|
|
)
|
|
)
|
|
and
|
|
# Scan for app_ flags
|
|
next((f for f in results if f.startswith("app_")), None)
|
|
is None
|
|
)
|
|
else LametricMode.CLOUD
|
|
)
|
|
|
|
# Handle defaults if not set
|
|
if results["mode"] == LametricMode.DEVICE:
|
|
# Device Mode Defaults
|
|
if "apikey" not in results:
|
|
results["apikey"] = NotifyLametric.unquote(results["password"])
|
|
|
|
else:
|
|
# CLOUD Mode Defaults
|
|
if "app_id" not in results:
|
|
results["app_id"] = NotifyLametric.unquote(results["host"])
|
|
if "app_token" not in results:
|
|
results["app_token"] = NotifyLametric.unquote(
|
|
results["password"]
|
|
)
|
|
|
|
# Set cycles
|
|
with contextlib.suppress(TypeError, ValueError):
|
|
results["cycles"] = abs(int(results["qsd"].get("cycles")))
|
|
|
|
return results
|
|
|
|
@staticmethod
|
|
def parse_native_url(url):
|
|
"""
|
|
Support
|
|
https://developer.lametric.com/api/v1/dev/\
|
|
widget/update/com.lametric.{APP_ID}/1
|
|
|
|
https://developer.lametric.com/api/v1/dev/\
|
|
widget/update/com.lametric.{APP_ID}/{APP_VER}
|
|
"""
|
|
|
|
# If users do provide the Native URL they wll also want to add
|
|
# ?token={APP_ACCESS_TOKEN} to the parameters at the end or the
|
|
# URL will fail to load in later stages.
|
|
result = re.match(
|
|
r"^http(?P<secure>s)?://(?P<host>[^/]+)"
|
|
r"/api/(?P<api_ver>v[1-9]*[0-9]+)"
|
|
r"/dev/widget/update/"
|
|
r"com\.lametric\.(?P<app_id>[0-9a-z.-]{1,64})"
|
|
r"(/(?P<app_ver>[1-9][0-9]*))?/?"
|
|
r"(?P<params>\?.+)?$",
|
|
url,
|
|
re.I,
|
|
)
|
|
|
|
if result:
|
|
return NotifyLametric.parse_url(
|
|
"{schema}://{app_id}{app_ver}/{params}".format(
|
|
schema=(
|
|
NotifyLametric.secure_protocol
|
|
if result.group("secure")
|
|
else NotifyLametric.protocol
|
|
),
|
|
app_id=result.group("app_id"),
|
|
app_ver=(
|
|
"/{}".format(result.group("app_ver"))
|
|
if result.group("app_ver")
|
|
else ""
|
|
),
|
|
params=(
|
|
""
|
|
if not result.group("params")
|
|
else result.group("params")
|
|
),
|
|
)
|
|
)
|
|
|
|
return None
|