Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit debbc8d

Browse files
author
Mathieu Velten
committed
Add a module API to send an HTTP push notification
1 parent 6d10337 commit debbc8d

File tree

3 files changed

+109
-61
lines changed

3 files changed

+109
-61
lines changed

changelog.d/15387.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a module API to send an HTTP push notification.

synapse/module_api/__init__.py

+35
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
Generator,
2424
Iterable,
2525
List,
26+
Mapping,
2627
Optional,
2728
Tuple,
2829
TypeVar,
@@ -105,6 +106,7 @@
105106
ON_LEGACY_SEND_MAIL_CALLBACK,
106107
ON_USER_REGISTRATION_CALLBACK,
107108
)
109+
from synapse.push.httppusher import HttpPusher
108110
from synapse.rest.client.login import LoginResponse
109111
from synapse.storage import DataStore
110112
from synapse.storage.background_updates import (
@@ -248,6 +250,7 @@ def __init__(self, hs: "HomeServer", auth_handler: AuthHandler) -> None:
248250
self._registration_handler = hs.get_registration_handler()
249251
self._send_email_handler = hs.get_send_email_handler()
250252
self._push_rules_handler = hs.get_push_rules_handler()
253+
self._pusherpool = hs.get_pusherpool()
251254
self._device_handler = hs.get_device_handler()
252255
self.custom_template_dir = hs.config.server.custom_template_directory
253256
self._callbacks = hs.get_module_api_callbacks()
@@ -1226,6 +1229,38 @@ async def sleep(self, seconds: float) -> None:
12261229

12271230
await self._clock.sleep(seconds)
12281231

1232+
async def send_http_push_notification(
1233+
self,
1234+
user_id: str,
1235+
device_id: str,
1236+
content: JsonDict,
1237+
tweaks: Mapping[str, str] = {},
1238+
) -> bool:
1239+
"""Send an HTTP push notification that is forwarded to the registered push gateway
1240+
for the specified device.
1241+
1242+
Added in Synapse v1.82.0.
1243+
1244+
Args:
1245+
user_id: The user ID of the device where to send the push notification.
1246+
device_id: The device ID of the device where to send the push notification.
1247+
content: A dict of values that will be put in the `notification` field of the push
1248+
(cf Push Gatway spec). `devices` field will be overrided if included.
1249+
tweaks: A dict of `tweaks` that will be inserted in the `devices` section, cf spec.
1250+
1251+
Returns:
1252+
True if at least one push was succesfully sent, False in case of error or if no
1253+
pusher is registered for the specified device.
1254+
"""
1255+
sent = False
1256+
if user_id in self._pusherpool.pushers:
1257+
for p in self._pusherpool.pushers[user_id].values():
1258+
if isinstance(p, HttpPusher) and p.device_id == device_id:
1259+
res = await p.dispatch_push(content, tweaks)
1260+
if res is not False:
1261+
sent = True
1262+
return sent
1263+
12291264
async def send_mail(
12301265
self,
12311266
recipient: str,

synapse/push/httppusher.py

+73-61
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# limitations under the License.
1515
import logging
1616
import urllib.parse
17-
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union
17+
from typing import TYPE_CHECKING, Dict, Iterable, List, Mapping, Optional, Tuple, Union
1818

1919
from prometheus_client import Counter
2020

@@ -27,6 +27,7 @@
2727
from synapse.metrics.background_process_metrics import run_as_background_process
2828
from synapse.push import Pusher, PusherConfig, PusherConfigException
2929
from synapse.storage.databases.main.event_push_actions import HttpPushAction
30+
from synapse.types import JsonDict
3031

3132
from . import push_tools
3233

@@ -56,7 +57,7 @@
5657
)
5758

5859

59-
def tweaks_for_actions(actions: List[Union[str, Dict]]) -> Dict[str, Any]:
60+
def tweaks_for_actions(actions: List[Union[str, Dict]]) -> Dict[str, str]:
6061
"""
6162
Converts a list of actions into a `tweaks` dict (which can then be passed to
6263
the push gateway).
@@ -101,6 +102,7 @@ def __init__(self, hs: "HomeServer", pusher_config: PusherConfig):
101102
self._storage_controllers = self.hs.get_storage_controllers()
102103
self.app_display_name = pusher_config.app_display_name
103104
self.device_display_name = pusher_config.device_display_name
105+
self.device_id = pusher_config.device_id
104106
self.pushkey_ts = pusher_config.ts
105107
self.data = pusher_config.data
106108
self.backoff_delay = HttpPusher.INITIAL_BACKOFF_SEC
@@ -324,7 +326,7 @@ async def _process_one(self, push_action: HttpPushAction) -> bool:
324326
event = await self.store.get_event(push_action.event_id, allow_none=True)
325327
if event is None:
326328
return True # It's been redacted
327-
rejected = await self.dispatch_push(event, tweaks, badge)
329+
rejected = await self.dispatch_push_event(event, tweaks, badge)
328330
if rejected is False:
329331
return False
330332

@@ -342,9 +344,12 @@ async def _process_one(self, push_action: HttpPushAction) -> bool:
342344
await self._pusherpool.remove_pusher(self.app_id, pk, self.user_id)
343345
return True
344346

345-
async def _build_notification_dict(
346-
self, event: EventBase, tweaks: Dict[str, bool], badge: int
347-
) -> Dict[str, Any]:
347+
async def _build_event_notification(
348+
self,
349+
event: EventBase,
350+
tweaks: Mapping[str, str],
351+
badge: int,
352+
) -> Tuple[JsonDict, Mapping[str, str]]:
348353
priority = "low"
349354
if (
350355
event.type == EventTypes.Encrypted
@@ -358,80 +363,70 @@ async def _build_notification_dict(
358363
# This was checked in the __init__, but mypy doesn't seem to know that.
359364
assert self.data is not None
360365
if self.data.get("format") == "event_id_only":
361-
d: Dict[str, Any] = {
362-
"notification": {
363-
"event_id": event.event_id,
364-
"room_id": event.room_id,
365-
"counts": {"unread": badge},
366-
"prio": priority,
367-
"devices": [
368-
{
369-
"app_id": self.app_id,
370-
"pushkey": self.pushkey,
371-
"pushkey_ts": int(self.pushkey_ts / 1000),
372-
"data": self.data_minus_url,
373-
}
374-
],
375-
}
366+
content: JsonDict = {
367+
"event_id": event.event_id,
368+
"room_id": event.room_id,
369+
"counts": {"unread": badge},
370+
"prio": priority,
376371
}
377-
return d
372+
return content, {}
378373

379374
ctx = await push_tools.get_context_for_event(
380375
self._storage_controllers, event, self.user_id
381376
)
382377

383-
d = {
384-
"notification": {
385-
"id": event.event_id, # deprecated: remove soon
386-
"event_id": event.event_id,
387-
"room_id": event.room_id,
388-
"type": event.type,
389-
"sender": event.user_id,
390-
"prio": priority,
391-
"counts": {
392-
"unread": badge,
393-
# 'missed_calls': 2
394-
},
395-
"devices": [
396-
{
397-
"app_id": self.app_id,
398-
"pushkey": self.pushkey,
399-
"pushkey_ts": int(self.pushkey_ts / 1000),
400-
"data": self.data_minus_url,
401-
"tweaks": tweaks,
402-
}
403-
],
404-
}
378+
content = {
379+
"id": event.event_id, # deprecated: remove soon
380+
"event_id": event.event_id,
381+
"room_id": event.room_id,
382+
"type": event.type,
383+
"sender": event.user_id,
384+
"prio": priority,
385+
"counts": {
386+
"unread": badge,
387+
# 'missed_calls': 2
388+
},
405389
}
406390
if event.type == "m.room.member" and event.is_state():
407-
d["notification"]["membership"] = event.content["membership"]
408-
d["notification"]["user_is_target"] = event.state_key == self.user_id
391+
content["membership"] = event.content["membership"]
392+
content["user_is_target"] = event.state_key == self.user_id
409393
if self.hs.config.push.push_include_content and event.content:
410-
d["notification"]["content"] = event.content
394+
content["content"] = event.content
411395

412396
# We no longer send aliases separately, instead, we send the human
413397
# readable name of the room, which may be an alias.
414398
if "sender_display_name" in ctx and len(ctx["sender_display_name"]) > 0:
415-
d["notification"]["sender_display_name"] = ctx["sender_display_name"]
399+
content["sender_display_name"] = ctx["sender_display_name"]
416400
if "name" in ctx and len(ctx["name"]) > 0:
417-
d["notification"]["room_name"] = ctx["name"]
401+
content["room_name"] = ctx["name"]
402+
403+
return (content, tweaks)
404+
405+
def _build_notification_dict(
406+
self, content: JsonDict, tweaks: Mapping[str, str]
407+
) -> JsonDict:
408+
device = {
409+
"app_id": self.app_id,
410+
"pushkey": self.pushkey,
411+
"pushkey_ts": int(self.pushkey_ts / 1000),
412+
"data": self.data_minus_url,
413+
}
414+
if tweaks:
415+
device["tweaks"] = tweaks
418416

419-
return d
417+
content["devices"] = [device]
418+
419+
return {"notification": content}
420420

421421
async def dispatch_push(
422-
self, event: EventBase, tweaks: Dict[str, bool], badge: int
422+
self, content: JsonDict, tweaks: Mapping[str, str] = {}
423423
) -> Union[bool, Iterable[str]]:
424-
notification_dict = await self._build_notification_dict(event, tweaks, badge)
425-
if not notification_dict:
426-
return []
424+
notif_dict = self._build_notification_dict(content, tweaks)
427425
try:
428-
resp = await self.http_client.post_json_get_json(
429-
self.url, notification_dict
430-
)
426+
resp = await self.http_client.post_json_get_json(self.url, notif_dict)
431427
except Exception as e:
432428
logger.warning(
433-
"Failed to push event %s to %s: %s %s",
434-
event.event_id,
429+
"Failed to push data to %s: %s %s",
435430
self.name,
436431
type(e),
437432
e,
@@ -440,10 +435,27 @@ async def dispatch_push(
440435
rejected = []
441436
if "rejected" in resp:
442437
rejected = resp["rejected"]
443-
if not rejected:
444-
self.badge_count_last_call = badge
445438
return rejected
446439

440+
async def dispatch_push_event(
441+
self,
442+
event: EventBase,
443+
tweaks: Mapping[str, str],
444+
badge: int,
445+
) -> Union[bool, Iterable[str]]:
446+
content, tweaks = await self._build_event_notification(event, tweaks, badge)
447+
if not content:
448+
return []
449+
450+
res = await self.dispatch_push(content, tweaks)
451+
452+
if res is False:
453+
return False
454+
if not res:
455+
self.badge_count_last_call = badge
456+
457+
return res
458+
447459
async def _send_badge(self, badge: int) -> None:
448460
"""
449461
Args:

0 commit comments

Comments
 (0)