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

Commit c276bd9

Browse files
authored
Send some ephemeral events to appservices (#8437)
Optionally sends typing, presence, and read receipt information to appservices.
1 parent 654e239 commit c276bd9

File tree

16 files changed

+563
-122
lines changed

16 files changed

+563
-122
lines changed

changelog.d/8437.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implement [MSC2409](https://github.com/matrix-org/matrix-doc/pull/2409) to send typing, read receipts, and presence events to appservices.

mypy.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ files =
1515
synapse/events/builder.py,
1616
synapse/events/spamcheck.py,
1717
synapse/federation,
18+
synapse/handlers/appservice.py,
1819
synapse/handlers/account_data.py,
1920
synapse/handlers/auth.py,
2021
synapse/handlers/cas_handler.py,

synapse/appservice/__init__.py

Lines changed: 118 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414
# limitations under the License.
1515
import logging
1616
import re
17-
from typing import TYPE_CHECKING
17+
from typing import TYPE_CHECKING, Iterable, List, Match, Optional
1818

1919
from synapse.api.constants import EventTypes
20-
from synapse.appservice.api import ApplicationServiceApi
21-
from synapse.types import GroupID, get_domain_from_id
20+
from synapse.events import EventBase
21+
from synapse.types import GroupID, JsonDict, UserID, get_domain_from_id
2222
from synapse.util.caches.descriptors import cached
2323

2424
if TYPE_CHECKING:
25+
from synapse.appservice.api import ApplicationServiceApi
2526
from synapse.storage.databases.main import DataStore
2627

2728
logger = logging.getLogger(__name__)
@@ -32,38 +33,6 @@ class ApplicationServiceState:
3233
UP = "up"
3334

3435

35-
class AppServiceTransaction:
36-
"""Represents an application service transaction."""
37-
38-
def __init__(self, service, id, events):
39-
self.service = service
40-
self.id = id
41-
self.events = events
42-
43-
async def send(self, as_api: ApplicationServiceApi) -> bool:
44-
"""Sends this transaction using the provided AS API interface.
45-
46-
Args:
47-
as_api: The API to use to send.
48-
Returns:
49-
True if the transaction was sent.
50-
"""
51-
return await as_api.push_bulk(
52-
service=self.service, events=self.events, txn_id=self.id
53-
)
54-
55-
async def complete(self, store: "DataStore") -> None:
56-
"""Completes this transaction as successful.
57-
58-
Marks this transaction ID on the application service and removes the
59-
transaction contents from the database.
60-
61-
Args:
62-
store: The database store to operate on.
63-
"""
64-
await store.complete_appservice_txn(service=self.service, txn_id=self.id)
65-
66-
6736
class ApplicationService:
6837
"""Defines an application service. This definition is mostly what is
6938
provided to the /register AS API.
@@ -91,6 +60,7 @@ def __init__(
9160
protocols=None,
9261
rate_limited=True,
9362
ip_range_whitelist=None,
63+
supports_ephemeral=False,
9464
):
9565
self.token = token
9666
self.url = (
@@ -102,6 +72,7 @@ def __init__(
10272
self.namespaces = self._check_namespaces(namespaces)
10373
self.id = id
10474
self.ip_range_whitelist = ip_range_whitelist
75+
self.supports_ephemeral = supports_ephemeral
10576

10677
if "|" in self.id:
10778
raise Exception("application service ID cannot contain '|' character")
@@ -161,19 +132,21 @@ def _check_namespaces(self, namespaces):
161132
raise ValueError("Expected string for 'regex' in ns '%s'" % ns)
162133
return namespaces
163134

164-
def _matches_regex(self, test_string, namespace_key):
135+
def _matches_regex(self, test_string: str, namespace_key: str) -> Optional[Match]:
165136
for regex_obj in self.namespaces[namespace_key]:
166137
if regex_obj["regex"].match(test_string):
167138
return regex_obj
168139
return None
169140

170-
def _is_exclusive(self, ns_key, test_string):
141+
def _is_exclusive(self, ns_key: str, test_string: str) -> bool:
171142
regex_obj = self._matches_regex(test_string, ns_key)
172143
if regex_obj:
173144
return regex_obj["exclusive"]
174145
return False
175146

176-
async def _matches_user(self, event, store):
147+
async def _matches_user(
148+
self, event: Optional[EventBase], store: Optional["DataStore"] = None
149+
) -> bool:
177150
if not event:
178151
return False
179152

@@ -188,27 +161,38 @@ async def _matches_user(self, event, store):
188161
if not store:
189162
return False
190163

191-
does_match = await self._matches_user_in_member_list(event.room_id, store)
164+
does_match = await self.matches_user_in_member_list(event.room_id, store)
192165
return does_match
193166

194-
@cached(num_args=1, cache_context=True)
195-
async def _matches_user_in_member_list(self, room_id, store, cache_context):
196-
member_list = await store.get_users_in_room(
197-
room_id, on_invalidate=cache_context.invalidate
198-
)
167+
@cached(num_args=1)
168+
async def matches_user_in_member_list(
169+
self, room_id: str, store: "DataStore"
170+
) -> bool:
171+
"""Check if this service is interested a room based upon it's membership
172+
173+
Args:
174+
room_id: The room to check.
175+
store: The datastore to query.
176+
177+
Returns:
178+
True if this service would like to know about this room.
179+
"""
180+
member_list = await store.get_users_in_room(room_id)
199181

200182
# check joined member events
201183
for user_id in member_list:
202184
if self.is_interested_in_user(user_id):
203185
return True
204186
return False
205187

206-
def _matches_room_id(self, event):
188+
def _matches_room_id(self, event: EventBase) -> bool:
207189
if hasattr(event, "room_id"):
208190
return self.is_interested_in_room(event.room_id)
209191
return False
210192

211-
async def _matches_aliases(self, event, store):
193+
async def _matches_aliases(
194+
self, event: EventBase, store: Optional["DataStore"] = None
195+
) -> bool:
212196
if not store or not event:
213197
return False
214198

@@ -218,52 +202,82 @@ async def _matches_aliases(self, event, store):
218202
return True
219203
return False
220204

221-
async def is_interested(self, event, store=None) -> bool:
205+
async def is_interested(
206+
self, event: EventBase, store: Optional["DataStore"] = None
207+
) -> bool:
222208
"""Check if this service is interested in this event.
223209
224210
Args:
225-
event(Event): The event to check.
226-
store(DataStore)
211+
event: The event to check.
212+
store: The datastore to query.
213+
227214
Returns:
228215
True if this service would like to know about this event.
229216
"""
230217
# Do cheap checks first
231218
if self._matches_room_id(event):
232219
return True
233220

221+
# This will check the namespaces first before
222+
# checking the store, so should be run before _matches_aliases
223+
if await self._matches_user(event, store):
224+
return True
225+
226+
# This will check the store, so should be run last
234227
if await self._matches_aliases(event, store):
235228
return True
236229

237-
if await self._matches_user(event, store):
230+
return False
231+
232+
@cached(num_args=1)
233+
async def is_interested_in_presence(
234+
self, user_id: UserID, store: "DataStore"
235+
) -> bool:
236+
"""Check if this service is interested a user's presence
237+
238+
Args:
239+
user_id: The user to check.
240+
store: The datastore to query.
241+
242+
Returns:
243+
True if this service would like to know about presence for this user.
244+
"""
245+
# Find all the rooms the sender is in
246+
if self.is_interested_in_user(user_id.to_string()):
238247
return True
248+
room_ids = await store.get_rooms_for_user(user_id.to_string())
239249

250+
# Then find out if the appservice is interested in any of those rooms
251+
for room_id in room_ids:
252+
if await self.matches_user_in_member_list(room_id, store):
253+
return True
240254
return False
241255

242-
def is_interested_in_user(self, user_id):
256+
def is_interested_in_user(self, user_id: str) -> bool:
243257
return (
244-
self._matches_regex(user_id, ApplicationService.NS_USERS)
258+
bool(self._matches_regex(user_id, ApplicationService.NS_USERS))
245259
or user_id == self.sender
246260
)
247261

248-
def is_interested_in_alias(self, alias):
262+
def is_interested_in_alias(self, alias: str) -> bool:
249263
return bool(self._matches_regex(alias, ApplicationService.NS_ALIASES))
250264

251-
def is_interested_in_room(self, room_id):
265+
def is_interested_in_room(self, room_id: str) -> bool:
252266
return bool(self._matches_regex(room_id, ApplicationService.NS_ROOMS))
253267

254-
def is_exclusive_user(self, user_id):
268+
def is_exclusive_user(self, user_id: str) -> bool:
255269
return (
256270
self._is_exclusive(ApplicationService.NS_USERS, user_id)
257271
or user_id == self.sender
258272
)
259273

260-
def is_interested_in_protocol(self, protocol):
274+
def is_interested_in_protocol(self, protocol: str) -> bool:
261275
return protocol in self.protocols
262276

263-
def is_exclusive_alias(self, alias):
277+
def is_exclusive_alias(self, alias: str) -> bool:
264278
return self._is_exclusive(ApplicationService.NS_ALIASES, alias)
265279

266-
def is_exclusive_room(self, room_id):
280+
def is_exclusive_room(self, room_id: str) -> bool:
267281
return self._is_exclusive(ApplicationService.NS_ROOMS, room_id)
268282

269283
def get_exclusive_user_regexes(self):
@@ -276,22 +290,22 @@ def get_exclusive_user_regexes(self):
276290
if regex_obj["exclusive"]
277291
]
278292

279-
def get_groups_for_user(self, user_id):
293+
def get_groups_for_user(self, user_id: str) -> Iterable[str]:
280294
"""Get the groups that this user is associated with by this AS
281295
282296
Args:
283-
user_id (str): The ID of the user.
297+
user_id: The ID of the user.
284298
285299
Returns:
286-
iterable[str]: an iterable that yields group_id strings.
300+
An iterable that yields group_id strings.
287301
"""
288302
return (
289303
regex_obj["group_id"]
290304
for regex_obj in self.namespaces[ApplicationService.NS_USERS]
291305
if "group_id" in regex_obj and regex_obj["regex"].match(user_id)
292306
)
293307

294-
def is_rate_limited(self):
308+
def is_rate_limited(self) -> bool:
295309
return self.rate_limited
296310

297311
def __str__(self):
@@ -300,3 +314,45 @@ def __str__(self):
300314
dict_copy["token"] = "<redacted>"
301315
dict_copy["hs_token"] = "<redacted>"
302316
return "ApplicationService: %s" % (dict_copy,)
317+
318+
319+
class AppServiceTransaction:
320+
"""Represents an application service transaction."""
321+
322+
def __init__(
323+
self,
324+
service: ApplicationService,
325+
id: int,
326+
events: List[EventBase],
327+
ephemeral: List[JsonDict],
328+
):
329+
self.service = service
330+
self.id = id
331+
self.events = events
332+
self.ephemeral = ephemeral
333+
334+
async def send(self, as_api: "ApplicationServiceApi") -> bool:
335+
"""Sends this transaction using the provided AS API interface.
336+
337+
Args:
338+
as_api: The API to use to send.
339+
Returns:
340+
True if the transaction was sent.
341+
"""
342+
return await as_api.push_bulk(
343+
service=self.service,
344+
events=self.events,
345+
ephemeral=self.ephemeral,
346+
txn_id=self.id,
347+
)
348+
349+
async def complete(self, store: "DataStore") -> None:
350+
"""Completes this transaction as successful.
351+
352+
Marks this transaction ID on the application service and removes the
353+
transaction contents from the database.
354+
355+
Args:
356+
store: The database store to operate on.
357+
"""
358+
await store.complete_appservice_txn(service=self.service, txn_id=self.id)

synapse/appservice/api.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414
# limitations under the License.
1515
import logging
1616
import urllib
17-
from typing import TYPE_CHECKING, Optional, Tuple
17+
from typing import TYPE_CHECKING, List, Optional, Tuple
1818

1919
from prometheus_client import Counter
2020

2121
from synapse.api.constants import EventTypes, ThirdPartyEntityKind
2222
from synapse.api.errors import CodeMessageException
23+
from synapse.events import EventBase
2324
from synapse.events.utils import serialize_event
2425
from synapse.http.client import SimpleHttpClient
2526
from synapse.types import JsonDict, ThirdPartyInstanceID
@@ -201,7 +202,13 @@ async def _get() -> Optional[JsonDict]:
201202
key = (service.id, protocol)
202203
return await self.protocol_meta_cache.wrap(key, _get)
203204

204-
async def push_bulk(self, service, events, txn_id=None):
205+
async def push_bulk(
206+
self,
207+
service: "ApplicationService",
208+
events: List[EventBase],
209+
ephemeral: List[JsonDict],
210+
txn_id: Optional[int] = None,
211+
):
205212
if service.url is None:
206213
return True
207214

@@ -211,15 +218,19 @@ async def push_bulk(self, service, events, txn_id=None):
211218
logger.warning(
212219
"push_bulk: Missing txn ID sending events to %s", service.url
213220
)
214-
txn_id = str(0)
215-
txn_id = str(txn_id)
221+
txn_id = 0
222+
223+
uri = service.url + ("/transactions/%s" % urllib.parse.quote(str(txn_id)))
224+
225+
# Never send ephemeral events to appservices that do not support it
226+
if service.supports_ephemeral:
227+
body = {"events": events, "de.sorunome.msc2409.ephemeral": ephemeral}
228+
else:
229+
body = {"events": events}
216230

217-
uri = service.url + ("/transactions/%s" % urllib.parse.quote(txn_id))
218231
try:
219232
await self.put_json(
220-
uri=uri,
221-
json_body={"events": events},
222-
args={"access_token": service.hs_token},
233+
uri=uri, json_body=body, args={"access_token": service.hs_token},
223234
)
224235
sent_transactions_counter.labels(service.id).inc()
225236
sent_events_counter.labels(service.id).inc(len(events))

0 commit comments

Comments
 (0)