Skip to content

Commit

Permalink
Add support of MSC4174
Browse files Browse the repository at this point in the history
  • Loading branch information
MatMaul committed Dec 3, 2024
1 parent d648c8c commit 5f5a5a0
Show file tree
Hide file tree
Showing 10 changed files with 1,016 additions and 28 deletions.
1 change: 1 addition & 0 deletions changelog.d/17987.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implements MSC4174.
6 changes: 6 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,9 @@ ignore_missing_imports = True

[mypy-multipart.*]
ignore_missing_imports = True

[mypy-pywebpush.*]
ignore_missing_imports = True

[mypy-py_vapid.*]
ignore_missing_imports = True
582 changes: 579 additions & 3 deletions poetry.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ Pympler = { version = "*", optional = true }
parameterized = { version = ">=0.7.4", optional = true }
idna = { version = ">=2.5", optional = true }
pyicu = { version = ">=2.10.2", optional = true }
pywebpush = { version = ">=2.0", optional = true }
py-vapid = { version = ">=1.9", optional = true }

[tool.poetry.extras]
# NB: Packages that should be part of `pip install matrix-synapse[all]` need to be specified
Expand All @@ -277,6 +279,7 @@ test = ["parameterized", "idna"]
# requires libicu's development headers installed on the system (e.g. libicu-dev on
# Debian-based distributions).
user-search = ["pyicu"]
webpush = ["pywebpush", "py-vapid"]

# The duplication here is awful. I hate hate hate hate hate it. However, for now I want
# to ensure you can still `pip install matrix-synapse[all]` like today. Two motivations:
Expand Down Expand Up @@ -310,6 +313,9 @@ all = [
"pympler",
# improved user search
"pyicu",
# WebPush support
"pywebpush",
"py-vapid",
# omitted:
# - test: it's useful to have this separate from dev deps in the olddeps job
# - systemd: this is a system-based requirement
Expand Down
50 changes: 50 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@
except ImportError:
HAS_AUTHLIB = False

# Determine whether pywebpush is installed.
try:
import pywebpush # noqa: F401

HAS_PYWEBPUSH = True
except ImportError:
HAS_PYWEBPUSH = False

if TYPE_CHECKING:
# Only import this if we're type checking, as it might not be installed at runtime.
from authlib.jose.rfc7517 import JsonWebKey
Expand Down Expand Up @@ -256,6 +264,28 @@ class MSC3866Config:
require_approval_for_new_accounts: bool = False


@attr.s(auto_attribs=True, frozen=True, slots=True)
class MSC4174Config:
"""Configuration for MSC4174"""

enabled: bool = attr.ib(default=False, validator=attr.validators.instance_of(bool))

@enabled.validator
def _check_enabled(self, attribute: attr.Attribute, value: bool) -> None:
# Only allow enabling MSC4174 if pywebpush is installed
if value and not HAS_PYWEBPUSH:
raise ConfigError(
"MSC4174 is enabled but pywebpush is not installed. "
"Please install pywebpush to use MSC4174.",
("experimental", "msc4174", "enabled"),
)

vapid_contact_email: str = ""
vapid_private_key: str = ""
vapid_app_server_key: str = ""
ttl: int = 15 * 60


class ExperimentalConfig(Config):
"""Config section for enabling experimental features"""

Expand Down Expand Up @@ -448,3 +478,23 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:

# MSC4222: Adding `state_after` to sync v2
self.msc4222_enabled: bool = experimental.get("msc4222_enabled", False)

raw_msc4174_config = experimental.get("msc4174", {})
self.msc4174 = MSC4174Config(**raw_msc4174_config)

if self.msc4174.enabled:
if not self.msc4174.vapid_contact_email:
raise ConfigError(
"'vapid_contact_email' must be provided when enabling webpush",
("experimental", "msc4174", "vapid_contact_email"),
)
if not self.msc4174.vapid_private_key:
raise ConfigError(
"'vapid_private_key' must be provided when enabling webpush",
("experimental", "msc4174", "vapid_private_key"),
)
if not self.msc4174.vapid_app_server_key:
raise ConfigError(
"'vapid_app_server_key' must be provided when enabling webpush",
("experimental", "msc4174", "vapid_app_server_key"),
)
55 changes: 30 additions & 25 deletions synapse/push/httppusher.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ def __init__(self, hs: "HomeServer", pusher_config: PusherConfig):
self.device_display_name = pusher_config.device_display_name
self.device_id = pusher_config.device_id
self.pushkey_ts = pusher_config.ts
self.data = pusher_config.data
self.backoff_delay = HttpPusher.INITIAL_BACKOFF_SEC
self.failing_since = pusher_config.failing_since
self.timed_call: Optional[IDelayedCall] = None
Expand All @@ -123,36 +122,39 @@ def __init__(self, hs: "HomeServer", pusher_config: PusherConfig):

self.push_jitter_delay_ms = hs.config.push.push_jitter_delay_ms

self.data = pusher_config.data
if self.data is None:
if pusher_config.data is None:
raise PusherConfigException("'data' key can not be null for HTTP pusher")
self.data = pusher_config.data

self.name = "%s/%s/%s" % (
pusher_config.user_name,
pusher_config.app_id,
pusher_config.pushkey,
)

# Validate that there's a URL and it is of the proper form.
if "url" not in self.data:
raise PusherConfigException("'url' required in data for HTTP pusher")

url = self.data["url"]
if not isinstance(url, str):
raise PusherConfigException("'url' must be a string")
url_parts = urllib.parse.urlparse(url)
# Note that the specification also says the scheme must be HTTPS, but
# it isn't up to the homeserver to verify that.
if url_parts.path != "/_matrix/push/v1/notify":
raise PusherConfigException(
"'url' must have a path of '/_matrix/push/v1/notify'"
)
self.url = ""
if pusher_config.kind == "http":
# Validate that there's a URL and it is of the proper form.
if "url" not in self.data:
raise PusherConfigException("'url' required in data for HTTP pusher")

url = self.data["url"]
if not isinstance(url, str):
raise PusherConfigException("'url' must be a string")
url_parts = urllib.parse.urlparse(url)
# Note that the specification also says the scheme must be HTTPS, but
# it isn't up to the homeserver to verify that.
if url_parts.path != "/_matrix/push/v1/notify":
raise PusherConfigException(
"'url' must have a path of '/_matrix/push/v1/notify'"
)
self.url = url

self.data_minus_url = {}
self.data_minus_url.update(self.data)
del self.data_minus_url["url"]

self.url = url
self.http_client = hs.get_proxied_blocklisted_http_client()
self.data_minus_url = {}
self.data_minus_url.update(self.data)
del self.data_minus_url["url"]
self.badge_count_last_call: Optional[int] = None

def on_started(self, should_check_for_notifs: bool) -> None:
Expand Down Expand Up @@ -183,7 +185,10 @@ async def _update_badge(self) -> None:
)
if self.badge_count_last_call is None or self.badge_count_last_call != badge:
self.badge_count_last_call = badge
await self._send_badge(badge)
if await self.send_badge(badge):
http_badges_processed_counter.inc()
else:
http_badges_failed_counter.inc()

def on_timer(self) -> None:
self._start_processing()
Expand Down Expand Up @@ -504,7 +509,7 @@ async def dispatch_push_event(

return res

async def _send_badge(self, badge: int) -> None:
async def send_badge(self, badge: int) -> bool:
"""
Args:
badge: number of unread messages
Expand All @@ -528,9 +533,9 @@ async def _send_badge(self, badge: int) -> None:
}
try:
await self.http_client.post_json_get_json(self.url, d)
http_badges_processed_counter.inc()
return True
except Exception as e:
logger.warning(
"Failed to send badge count to %s: %s %s", self.name, type(e), e
)
http_badges_failed_counter.inc()
return False
9 changes: 9 additions & 0 deletions synapse/push/pusher.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import logging
from typing import TYPE_CHECKING, Callable, Dict, Optional

import synapse.config.experimental
from synapse.push import Pusher, PusherConfig
from synapse.push.emailpusher import EmailPusher
from synapse.push.httppusher import HttpPusher
Expand All @@ -42,6 +43,14 @@ def __init__(self, hs: "HomeServer"):
"http": HttpPusher
}

if (
synapse.config.experimental.HAS_PYWEBPUSH
and self.config.experimental.msc4174.enabled
):
from synapse.push.webpushpusher import WebPushPusher

self.pusher_types["webpush"] = WebPushPusher

logger.info("email enable notifs: %r", hs.config.email.email_enable_notifs)
if hs.config.email.email_enable_notifs:
self.mailers: Dict[str, Mailer] = {}
Expand Down
Loading

0 comments on commit 5f5a5a0

Please sign in to comment.