diff --git a/KEYWORDS b/KEYWORDS
index 21c9ab3c..dfd0795d 100644
--- a/KEYWORDS
+++ b/KEYWORDS
@@ -40,6 +40,7 @@ Kavenegar
KODI
Kumulos
LaMetric
+Lark
Line
MacOSX
Mailgun
diff --git a/README.md b/README.md
index cea77dbb..b6e0cd95 100644
--- a/README.md
+++ b/README.md
@@ -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
kodi://user@hostname
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
lametric://apikey@hostname:port
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
line://Token/User1/User2/UserN
| [Mailgun](https://github.com/caronc/apprise/wiki/Notify_mailgun) | mailgun:// | (TCP) 443 | mailgun://user@hostname/apikey
mailgun://user@hostname/apikey/email
mailgun://user@hostname/apikey/email1/email2/emailN
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
mastodon://access_key@hostname/@user
mastodon://access_key@hostname/@user1/@user2/@userN
diff --git a/apprise/plugins/lark.py b/apprise/plugins/lark.py
new file mode 100644
index 00000000..59470193
--- /dev/null
+++ b/apprise/plugins/lark.py
@@ -0,0 +1,194 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2025, Chris Caron
+#
+# 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)))
diff --git a/packaging/redhat/python-apprise.spec b/packaging/redhat/python-apprise.spec
index 77f22d29..7aa7f700 100644
--- a/packaging/redhat/python-apprise.spec
+++ b/packaging/redhat/python-apprise.spec
@@ -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,
diff --git a/test/test_plugin_lark.py b/test/test_plugin_lark.py
new file mode 100644
index 00000000..108d127d
--- /dev/null
+++ b/test/test_plugin_lark.py
@@ -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()