Added Lark Support (#1361)

pull/1366/head
Chris Caron 2025-07-06 21:12:40 -04:00 committed by GitHub
parent 1d5a304638
commit 62c762cb6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 273 additions and 1 deletions

View File

@ -40,6 +40,7 @@ Kavenegar
KODI
Kumulos
LaMetric
Lark
Line
MacOSX
Mailgun

View File

@ -79,6 +79,7 @@ The table below identifies the services this tool supports and some example serv
| [KODI](https://github.com/caronc/apprise/wiki/Notify_kodi) | kodi:// or kodis:// | (TCP) 8080 or 443 | kodi://hostname<br />kodi://user@hostname<br />kodi://user:password@hostname:port
| [Kumulos](https://github.com/caronc/apprise/wiki/Notify_kumulos) | kumulos:// | (TCP) 443 | kumulos://apikey/serverkey
| [LaMetric Time](https://github.com/caronc/apprise/wiki/Notify_lametric) | lametric:// | (TCP) 443 | lametric://apikey@device_ipaddr<br/>lametric://apikey@hostname:port<br/>lametric://client_id@client_secret
| [Lark](https://github.com/caronc/apprise/wiki/Notify_lark) | lark:// | (TCP) 443 | lark://BotToken
| [Line](https://github.com/caronc/apprise/wiki/Notify_line) | line:// | (TCP) 443 | line://Token@User<br/>line://Token/User1/User2/UserN
| [Mailgun](https://github.com/caronc/apprise/wiki/Notify_mailgun) | mailgun:// | (TCP) 443 | mailgun://user@hostname/apikey<br />mailgun://user@hostname/apikey/email<br />mailgun://user@hostname/apikey/email1/email2/emailN<br />mailgun://user@hostname/apikey/?name="From%20User"
| [Mastodon](https://github.com/caronc/apprise/wiki/Notify_mastodon) | mastodon:// or mastodons://| (TCP) 80 or 443 | mastodon://access_key@hostname<br />mastodon://access_key@hostname/@user<br />mastodon://access_key@hostname/@user1/@user2/@userN

194
apprise/plugins/lark.py Normal file
View File

@ -0,0 +1,194 @@
# -*- coding: utf-8 -*-
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2025, 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.
# Details at:
# https://open.larksuite.com/document/client-docs/bot-v3/add-bot
import re
import requests
import json
from ..utils.parse import validate_regex
from ..url import PrivacyMode
from .base import NotifyBase
from ..locale import gettext_lazy as _
from ..common import NotifyType
class NotifyLark(NotifyBase):
"""
A wrapper for Lark (Feishu) Notifications via Webhook
"""
# The default descriptive name associated with the Notification
service_name = _('Lark (Feishu)')
service_url = 'https://open.larksuite.com/'
# The default protocol
secure_protocol = 'lark'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_lark'
# This is the static part of the webhook URL; only the token varies.
notify_url = 'https://open.larksuite.com/open-apis/bot/v2/hook/'
# Define object templates
templates = (
'{schema}://{token}',
)
template_tokens = dict(NotifyBase.template_tokens, **{
'token': {
'name': _('Bot Token'),
'type': 'string',
'private': True,
'required': True,
'regex': (r'^[a-z0-9-]+$', 'i'),
},
})
def __init__(self, token, **kwargs):
"""
Initialize Email Object
The smtp_host and secure_mode can be automatically detected depending
on how the URL was built
"""
super().__init__(**kwargs)
# The token associated with the account
self.token = validate_regex(
token, *self.template_tokens['token']['regex'])
if not self.token:
msg = 'The Lark Bot Token token specified ({}) is invalid.'\
.format(token)
self.logger.warning(msg)
raise TypeError(msg)
self.webhook_url = f'{self.notify_url}{self.token}'
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
params = self.url_parameters(privacy=privacy, *args, **kwargs)
return '{schema}://{token}/?{params}'.format(
schema=self.secure_protocol,
token=self.pprint(self.token, privacy, mode=PrivacyMode.Secret),
params=NotifyLark.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Returns all of the identifiers that make this URL unique from
another similar one. Targets or end points should never be identified
here.
"""
self.throttle()
payload = {
'msg_type': 'text',
'content': {
'text': f'{title}\n{body}' if title else body
}
}
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
}
# Always call throttle before any remote server i/o is made
self.throttle()
try:
r = requests.post(
self.webhook_url,
headers=headers,
data=json.dumps(payload),
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
self.logger.warning(
'Lark notification failed: %d - %s',
r.status_code, r.text)
return False
except requests.RequestException as e:
self.logger.warning(f'Lark Exception: {e}')
return False
self.logger.info('Lark notification sent successfully.')
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another similar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.token)
@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
# Set our token if found as an argument
if 'token' in results['qsd'] and len(results['qsd']['token']):
results['token'] = NotifyLark.unquote(results['qsd']['token'])
else:
# Fall back to hose (if defined here)
results['token'] = NotifyLark.unquote(results['host'])
return results
@staticmethod
def parse_native_url(url):
"""
Support https://open.larksuite.com/open-apis/bot/v2/hook//WEBHOOK_TOKEN
"""
match = re.match(
r'^https://open\.larksuite\.com/open-apis/bot/v2/hook/([\w-]+)$',
url, re.I)
if not match:
return None
return NotifyLark.parse_url('{schema}://{token}'.format(
schema=NotifyLark.secure_protocol, token=match.group(1)))

View File

@ -42,7 +42,7 @@ it easy to access:
Africas Talking, Apprise API, APRS, AWS SES, AWS SNS, Bark, BlueSky, Burst SMS,
BulkSMS, BulkVS, Chanify, Clickatell, ClickSend, DAPNET, DingTalk, Discord, E-Mail, Emby,
FCM, Feishu, Flock, Free Mobile, Google Chat, Gotify, Growl, Guilded, Home
Assistant, httpSMS, IFTTT, Join, Kavenegar, KODI, Kumulos, LaMetric, Line,
Assistant, httpSMS, IFTTT, Join, Kavenegar, KODI, Kumulos, LaMetric, Lark, Line,
MacOSX, Mailgun, Mastodon, Mattermost, Matrix, MessageBird, Microsoft
Windows, Microsoft Teams, Misskey, MQTT, MSG91, MyAndroid, Nexmo, Nextcloud,
NextcloudTalk, Notica, Notifiarr, Notifico, ntfy, Office365, OneSignal,

76
test/test_plugin_lark.py Normal file
View File

@ -0,0 +1,76 @@
import requests
from apprise.plugins.lark import NotifyLark
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('lark://', {
# Teams Token missing
'instance': TypeError,
}),
('lark://:@/', {
# We don't have strict host checking on for lark, so this URL
# actually becomes parseable and :@ becomes a hostname.
# The below errors because a second token wasn't found
'instance': TypeError,
}),
('lark://{}'.format('abcd-1234'), {
# token provided - we're good
'instance': NotifyLark,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'lark://****/',
}),
('lark://{}'.format('abcd-1234'), {
# token provided - we're good
'instance': NotifyLark,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'lark://****/',
}),
('lark://?token={}'.format('abcd-1234'), {
# token provided - we're good
'instance': NotifyLark,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'lark://****/',
}),
# Support Native URLs with arguments
('https://open.larksuite.com/open-apis/bot/v2/hook/{}'.format(
'abcd-1234'), {
# token provided - we're good
'instance': NotifyLark,
}),
('lark://{}'.format('abcd-1234'), {
'instance': NotifyLark,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('lark://{}'.format('abcd-1234'), {
'instance': NotifyLark,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('lark://{}'.format('a' * 80), {
'instance': NotifyLark,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_lark_urls():
"""
NotifyLark() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()