mirror of
https://github.com/caronc/apprise.git
synced 2025-12-15 10:04:06 +08:00
discord:// supports ping= feature (#1466)
This commit is contained in:
@@ -43,6 +43,7 @@
|
||||
# - https://discord.com/developers/docs/resources/webhook
|
||||
#
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from itertools import chain
|
||||
from json import dumps
|
||||
import re
|
||||
|
||||
@@ -51,12 +52,12 @@ import requests
|
||||
from ..attachment.base import AttachBase
|
||||
from ..common import NotifyFormat, NotifyImageSize, NotifyType
|
||||
from ..locale import gettext_lazy as _
|
||||
from ..utils.parse import parse_bool, validate_regex
|
||||
from ..utils.parse import parse_bool, parse_list, validate_regex
|
||||
from .base import NotifyBase
|
||||
|
||||
# Used to detect user/role IDs
|
||||
USER_ROLE_DETECTION_RE = re.compile(
|
||||
r"\s*(?:<@(?P<role>&?)(?P<id>[0-9]+)>|@(?P<value>[a-z0-9]+))", re.I
|
||||
r"\s*(?:<?@(?P<role>&?)(?P<id>[0-9]+)>?|@(?P<value>[a-z0-9]+))", re.I
|
||||
)
|
||||
|
||||
|
||||
@@ -194,6 +195,10 @@ class NotifyDiscord(NotifyBase):
|
||||
"default": False,
|
||||
"map_to": "include_image",
|
||||
},
|
||||
"ping": {
|
||||
"name": _("Ping Users/Roles"),
|
||||
"type": "list:string",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -211,6 +216,7 @@ class NotifyDiscord(NotifyBase):
|
||||
href=None,
|
||||
thread=None,
|
||||
flags=None,
|
||||
ping=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize Discord Object."""
|
||||
@@ -285,6 +291,9 @@ class NotifyDiscord(NotifyBase):
|
||||
# Default to 1.0
|
||||
self.ratelimit_remaining = 1.0
|
||||
|
||||
# Ping targets (raw tokens from URL, already split by parse_list)
|
||||
self.ping = parse_list(ping)
|
||||
|
||||
return
|
||||
|
||||
def send(
|
||||
@@ -324,6 +333,9 @@ class NotifyDiscord(NotifyBase):
|
||||
# Associate our thread_id with our message
|
||||
params = {"thread_id": self.thread_id} if self.thread_id else None
|
||||
|
||||
# Apply any pingable content to the payload
|
||||
payload.update(self.ping_payload(body, " ".join(self.ping)))
|
||||
|
||||
if body:
|
||||
# our fields variable
|
||||
fields = []
|
||||
@@ -383,34 +395,7 @@ class NotifyDiscord(NotifyBase):
|
||||
# not markdown
|
||||
payload["content"] = (
|
||||
body if not title else f"{title}\r\n{body}"
|
||||
)
|
||||
|
||||
# parse for user id's <@123> and role IDs <@&456>
|
||||
results = USER_ROLE_DETECTION_RE.findall(body)
|
||||
if results:
|
||||
payload["allow_mentions"] = {
|
||||
"parse": [],
|
||||
"users": [],
|
||||
"roles": [],
|
||||
}
|
||||
|
||||
_content = []
|
||||
for is_role, no, value in results:
|
||||
if value:
|
||||
payload["allow_mentions"]["parse"].append(value)
|
||||
_content.append(f"@{value}")
|
||||
|
||||
elif is_role:
|
||||
payload["allow_mentions"]["roles"].append(no)
|
||||
_content.append(f"<@&{no}>")
|
||||
|
||||
else: # is_user
|
||||
payload["allow_mentions"]["users"].append(no)
|
||||
_content.append(f"<@{no}>")
|
||||
|
||||
if self.notify_format == NotifyFormat.MARKDOWN:
|
||||
# Add pingable elements to content field
|
||||
payload["content"] = "👉 " + " ".join(_content)
|
||||
) + payload.get("content", "")
|
||||
|
||||
if not self._send(payload, params=params):
|
||||
# We failed to post our message
|
||||
@@ -660,6 +645,9 @@ class NotifyDiscord(NotifyBase):
|
||||
if self.thread_id:
|
||||
params["thread"] = self.thread_id
|
||||
|
||||
if self.ping:
|
||||
params["ping"] = ",".join(self.ping)
|
||||
|
||||
# Ensure our botname is set
|
||||
botname = f"{self.user}@" if self.user else ""
|
||||
|
||||
@@ -771,6 +759,10 @@ class NotifyDiscord(NotifyBase):
|
||||
# Markdown is implied
|
||||
results["format"] = NotifyFormat.MARKDOWN
|
||||
|
||||
# Extract ping targets, comma/space separated
|
||||
if "ping" in results["qsd"]:
|
||||
results["ping"] = NotifyDiscord.unquote(results["qsd"]["ping"])
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
@@ -806,6 +798,59 @@ class NotifyDiscord(NotifyBase):
|
||||
|
||||
return None
|
||||
|
||||
def ping_payload(self, *args):
|
||||
"""
|
||||
Takes a body and applies the payload associated with pinging
|
||||
the users detected within
|
||||
"""
|
||||
|
||||
# initialize a payload object we can prepare
|
||||
payload = {}
|
||||
|
||||
roles = set()
|
||||
users = set()
|
||||
parse = set()
|
||||
|
||||
for arg in args:
|
||||
# parse for user id's <@123> and role IDs <@&456>
|
||||
results = USER_ROLE_DETECTION_RE.findall(arg)
|
||||
if not results:
|
||||
continue
|
||||
|
||||
_content = []
|
||||
for is_role, no, value in results:
|
||||
if value:
|
||||
parse.add(value)
|
||||
_content.append(f"@{value}")
|
||||
|
||||
elif is_role:
|
||||
roles.add(no)
|
||||
_content.append(f"<@&{no}>")
|
||||
|
||||
else: # is_user
|
||||
users.add(no)
|
||||
_content.append(f"<@{no}>")
|
||||
|
||||
if not (roles or users or parse):
|
||||
# Nothing to add
|
||||
return payload
|
||||
|
||||
# First time through...
|
||||
payload["allow_mentions"] = {
|
||||
"parse": list(parse),
|
||||
"users": list(users),
|
||||
"roles": list(roles),
|
||||
}
|
||||
|
||||
# Add pingable elements to content field
|
||||
payload["content"] = "👉 " + " ".join(chain(
|
||||
[f"@{value}" for value in parse],
|
||||
[f"<@&{value}>" for value in roles],
|
||||
[f"<@{value}>" for value in users],
|
||||
))
|
||||
|
||||
return payload
|
||||
|
||||
@staticmethod
|
||||
def extract_markdown_sections(markdown):
|
||||
"""Takes a string in a markdown type format and extracts the headers
|
||||
|
||||
@@ -88,7 +88,7 @@ apprise_url_tests = (
|
||||
),
|
||||
# test image= field
|
||||
(
|
||||
"discord://{}/{}?format=markdown&footer=Yes&image=Yes".format(
|
||||
"discord://{}/{}?format=markdown&footer=Yes&image=Yes&ping=Joe".format(
|
||||
"i" * 24, "t" * 64
|
||||
),
|
||||
{
|
||||
@@ -404,6 +404,55 @@ def test_plugin_discord_notifications(mock_post):
|
||||
assert "everyone" in payload["allow_mentions"]["parse"]
|
||||
assert "admin" in payload["allow_mentions"]["parse"]
|
||||
|
||||
# Reset our object
|
||||
mock_post.reset_mock()
|
||||
|
||||
# Test our header parsing when not lead with a header
|
||||
body = """ """
|
||||
|
||||
results = NotifyDiscord.parse_url(
|
||||
# & -> %26 for role otherwise & separates our URL from further parsing
|
||||
f"discord://{webhook_id}/{webhook_token}/?ping=@joe,<@321>,<@%26654>"
|
||||
)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["user"] is None
|
||||
assert results["webhook_id"] == webhook_id
|
||||
assert results["webhook_token"] == webhook_token
|
||||
assert results["password"] is None
|
||||
assert results["port"] is None
|
||||
assert results["host"] == webhook_id
|
||||
assert results["fullpath"] == f"/{webhook_token}/"
|
||||
assert results["path"] == f"/{webhook_token}/"
|
||||
assert results["query"] is None
|
||||
assert results["schema"] == "discord"
|
||||
assert results["url"] == f"discord://{webhook_id}/{webhook_token}/"
|
||||
instance = NotifyDiscord(**results)
|
||||
assert isinstance(instance, NotifyDiscord)
|
||||
|
||||
response = instance.send(body=body)
|
||||
assert response is True
|
||||
assert mock_post.call_count == 1
|
||||
|
||||
details = mock_post.call_args_list[0]
|
||||
assert (
|
||||
details[0][0]
|
||||
== f"https://discord.com/api/webhooks/{webhook_id}/{webhook_token}"
|
||||
)
|
||||
|
||||
payload = loads(details[1]["data"])
|
||||
|
||||
assert "allow_mentions" in payload
|
||||
assert len(payload["allow_mentions"]["users"]) == 1
|
||||
assert "321" in payload["allow_mentions"]["users"]
|
||||
assert "<@321>" in payload["content"]
|
||||
assert len(payload["allow_mentions"]["roles"]) == 1
|
||||
assert "654" in payload["allow_mentions"]["roles"]
|
||||
assert "<@&654>" in payload["content"]
|
||||
assert len(payload["allow_mentions"]["parse"]) == 1
|
||||
assert "joe" in payload["allow_mentions"]["parse"]
|
||||
assert "@joe" in payload["content"]
|
||||
|
||||
|
||||
@mock.patch("requests.post")
|
||||
@mock.patch("time.sleep")
|
||||
|
||||
Reference in New Issue
Block a user