Skip to content

Commit 28e1f2f

Browse files
Added presence update on change of profile information and config flags for selective presence tracking
Signed-off-by: Michael Hollister <[email protected]>
1 parent 696cc9e commit 28e1f2f

File tree

12 files changed

+150
-10
lines changed

12 files changed

+150
-10
lines changed

changelog.d/16992.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added presence update on change of profile information and config flags for selective presence tracking. Contributed by @Michael-Hollister.

synapse/api/presence.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ class UserPresenceState:
8383
last_user_sync_ts: int
8484
status_msg: Optional[str]
8585
currently_active: bool
86+
displayname: Optional[str]
87+
avatar_url: Optional[str]
8688

8789
def as_dict(self) -> JsonDict:
8890
return attr.asdict(self)
@@ -101,4 +103,6 @@ def default(cls, user_id: str) -> "UserPresenceState":
101103
last_user_sync_ts=0,
102104
status_msg=None,
103105
currently_active=False,
106+
displayname=None,
107+
avatar_url=None,
104108
)

synapse/config/server.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,16 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
384384
# Whether to internally track presence, requires that presence is enabled,
385385
self.track_presence = self.presence_enabled and presence_enabled != "untracked"
386386

387+
# Disabling server-side presence tracking
388+
self.sync_presence_tracking = presence_config.get(
389+
"sync_presence_tracking", True
390+
)
391+
392+
# Disabling federation presence tracking
393+
self.federation_presence_tracking = presence_config.get(
394+
"federation_presence_tracking", True
395+
)
396+
387397
# Custom presence router module
388398
# This is the legacy way of configuring it (the config should now be put in the modules section)
389399
self.presence_router_module_class = None

synapse/federation/federation_server.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,17 @@ async def on_edu(self, edu_type: str, origin: str, content: dict) -> None:
14101410
if not self.config.server.track_presence and edu_type == EduTypes.PRESENCE:
14111411
return
14121412

1413+
if (
1414+
not self.config.server.federation_presence_tracking
1415+
and edu_type == EduTypes.PRESENCE
1416+
):
1417+
filtered_edus = []
1418+
for e in content["push"]:
1419+
# Process only profile presence updates to reduce resource impact
1420+
if "status_msg" in e or "displayname" in e or "avatar_url" in e:
1421+
filtered_edus.append(e)
1422+
content["push"] = filtered_edus
1423+
14131424
# Check if we have a handler on this instance
14141425
handler = self.edu_handlers.get(edu_type)
14151426
if handler:

synapse/handlers/presence.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ def __init__(self, hs: "HomeServer"):
201201

202202
self._presence_enabled = hs.config.server.presence_enabled
203203
self._track_presence = hs.config.server.track_presence
204+
self._sync_presence_tracking = hs.config.server.sync_presence_tracking
204205

205206
self._federation = None
206207
if hs.should_send_federation():
@@ -451,6 +452,8 @@ async def send_full_presence_to_users(self, user_ids: StrCollection) -> None:
451452
state = {
452453
"presence": current_presence_state.state,
453454
"status_message": current_presence_state.status_msg,
455+
"displayname": current_presence_state.displayname,
456+
"avatar_url": current_presence_state.avatar_url,
454457
}
455458

456459
# Copy the presence state to the tip of the presence stream.
@@ -579,7 +582,11 @@ async def user_syncing(
579582
Called by the sync and events servlets to record that a user has connected to
580583
this worker and is waiting for some events.
581584
"""
582-
if not affect_presence or not self._track_presence:
585+
if (
586+
not affect_presence
587+
or not self._track_presence
588+
or not self._sync_presence_tracking
589+
):
583590
return _NullContextManager()
584591

585592
# Note that this causes last_active_ts to be incremented which is not
@@ -648,6 +655,8 @@ async def process_replication_rows(
648655
row.last_user_sync_ts,
649656
row.status_msg,
650657
row.currently_active,
658+
row.displayname,
659+
row.avatar_url,
651660
)
652661
for row in rows
653662
]
@@ -1140,7 +1149,11 @@ async def user_syncing(
11401149
client that is being used by a user.
11411150
presence_state: The presence state indicated in the sync request
11421151
"""
1143-
if not affect_presence or not self._track_presence:
1152+
if (
1153+
not affect_presence
1154+
or not self._track_presence
1155+
or not self._sync_presence_tracking
1156+
):
11441157
return _NullContextManager()
11451158

11461159
curr_sync = self._user_device_to_num_current_syncs.get((user_id, device_id), 0)
@@ -1340,6 +1353,8 @@ async def incoming_presence(self, origin: str, content: JsonDict) -> None:
13401353

13411354
new_fields["status_msg"] = push.get("status_msg", None)
13421355
new_fields["currently_active"] = push.get("currently_active", False)
1356+
new_fields["displayname"] = push.get("displayname", None)
1357+
new_fields["avatar_url"] = push.get("avatar_url", None)
13431358

13441359
prev_state = await self.current_state_for_user(user_id)
13451360
updates.append(prev_state.copy_and_replace(**new_fields))
@@ -1369,6 +1384,8 @@ async def set_state(
13691384
the `state` dict.
13701385
"""
13711386
status_msg = state.get("status_msg", None)
1387+
displayname = state.get("displayname", None)
1388+
avatar_url = state.get("avatar_url", None)
13721389
presence = state["presence"]
13731390

13741391
if presence not in self.VALID_PRESENCE:
@@ -1414,6 +1431,8 @@ async def set_state(
14141431
else:
14151432
# Syncs do not override the status message.
14161433
new_fields["status_msg"] = status_msg
1434+
new_fields["displayname"] = displayname
1435+
new_fields["avatar_url"] = avatar_url
14171436

14181437
await self._update_states(
14191438
[prev_state.copy_and_replace(**new_fields)], force_notify=force_notify
@@ -1634,6 +1653,8 @@ async def _handle_state_delta(self, room_id: str, deltas: List[StateDelta]) -> N
16341653
if state.state != PresenceState.OFFLINE
16351654
or now - state.last_active_ts < 7 * 24 * 60 * 60 * 1000
16361655
or state.status_msg is not None
1656+
or state.displayname is not None
1657+
or state.avatar_url is not None
16371658
]
16381659

16391660
await self._federation_queue.send_presence_to_destinations(
@@ -1668,6 +1689,14 @@ def should_notify(
16681689
notify_reason_counter.labels(user_location, "status_msg_change").inc()
16691690
return True
16701691

1692+
if old_state.displayname != new_state.displayname:
1693+
notify_reason_counter.labels(user_location, "displayname_change").inc()
1694+
return True
1695+
1696+
if old_state.avatar_url != new_state.avatar_url:
1697+
notify_reason_counter.labels(user_location, "avatar_url_change").inc()
1698+
return True
1699+
16711700
if old_state.state != new_state.state:
16721701
notify_reason_counter.labels(user_location, "state_change").inc()
16731702
state_transition_counter.labels(
@@ -1725,6 +1754,8 @@ def format_user_presence_state(
17251754
* status_msg: Optional. Included if `status_msg` is set on `state`. The user's
17261755
status.
17271756
* currently_active: Optional. Included only if `state.state` is "online".
1757+
* displayname: Optional. The current display name for this user, if any.
1758+
* avatar_url: Optional. The current avatar URL for this user, if any.
17281759
17291760
Example:
17301761
@@ -1733,7 +1764,9 @@ def format_user_presence_state(
17331764
"user_id": "@alice:example.com",
17341765
"last_active_ago": 16783813918,
17351766
"status_msg": "Hello world!",
1736-
"currently_active": True
1767+
"currently_active": True,
1768+
"displayname": "Alice",
1769+
"avatar_url": "mxc://localhost/wefuiwegh8742w"
17371770
}
17381771
"""
17391772
content: JsonDict = {"presence": state.state}
@@ -1745,6 +1778,10 @@ def format_user_presence_state(
17451778
content["status_msg"] = state.status_msg
17461779
if state.state == PresenceState.ONLINE:
17471780
content["currently_active"] = state.currently_active
1781+
if state.displayname:
1782+
content["displayname"] = state.displayname
1783+
if state.avatar_url:
1784+
content["avatar_url"] = state.avatar_url
17481785

17491786
return content
17501787

synapse/handlers/profile.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,19 @@ async def set_displayname(
200200
if propagate:
201201
await self._update_join_states(requester, target_user)
202202

203+
if self.hs.config.server.track_presence:
204+
presence_handler = self.hs.get_presence_handler()
205+
current_presence_state = await presence_handler.get_state(target_user)
206+
207+
state = {
208+
"presence": current_presence_state.state,
209+
"status_message": current_presence_state.status_msg,
210+
"displayname": new_displayname,
211+
"avatar_url": current_presence_state.avatar_url,
212+
}
213+
214+
await presence_handler.set_state(target_user, requester.device_id, state)
215+
203216
async def get_avatar_url(self, target_user: UserID) -> Optional[str]:
204217
if self.hs.is_mine(target_user):
205218
try:
@@ -293,6 +306,19 @@ async def set_avatar_url(
293306
if propagate:
294307
await self._update_join_states(requester, target_user)
295308

309+
if self.hs.config.server.track_presence:
310+
presence_handler = self.hs.get_presence_handler()
311+
current_presence_state = await presence_handler.get_state(target_user)
312+
313+
state = {
314+
"presence": current_presence_state.state,
315+
"status_message": current_presence_state.status_msg,
316+
"displayname": current_presence_state.displayname,
317+
"avatar_url": new_avatar_url,
318+
}
319+
320+
await presence_handler.set_state(target_user, requester.device_id, state)
321+
296322
@cached()
297323
async def check_avatar_size_and_mime_type(self, mxc: str) -> bool:
298324
"""Check that the size and content type of the avatar at the given MXC URI are

synapse/replication/tcp/streams/_base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,8 @@ class PresenceStreamRow:
330330
last_user_sync_ts: int
331331
status_msg: str
332332
currently_active: bool
333+
displayname: str
334+
avatar_url: str
333335

334336
NAME = "presence"
335337
ROW_TYPE = PresenceStreamRow

synapse/storage/databases/main/presence.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ def _update_presence_txn(
181181
"last_user_sync_ts",
182182
"status_msg",
183183
"currently_active",
184+
"displayname",
185+
"avatar_url",
184186
"instance_name",
185187
),
186188
values=[
@@ -193,6 +195,8 @@ def _update_presence_txn(
193195
state.last_user_sync_ts,
194196
state.status_msg,
195197
state.currently_active,
198+
state.displayname,
199+
state.avatar_url,
196200
self._instance_name,
197201
)
198202
for stream_id, state in zip(stream_orderings, presence_states)
@@ -232,7 +236,8 @@ def get_all_presence_updates_txn(
232236
sql = """
233237
SELECT stream_id, user_id, state, last_active_ts,
234238
last_federation_update_ts, last_user_sync_ts,
235-
status_msg, currently_active
239+
status_msg, currently_active, displayname,
240+
avatar_url
236241
FROM presence_stream
237242
WHERE ? < stream_id AND stream_id <= ?
238243
ORDER BY stream_id ASC
@@ -285,6 +290,8 @@ async def get_presence_for_users(
285290
"last_user_sync_ts",
286291
"status_msg",
287292
"currently_active",
293+
"displayname",
294+
"avatar_url",
288295
),
289296
desc="get_presence_for_users",
290297
),
@@ -299,8 +306,10 @@ async def get_presence_for_users(
299306
last_user_sync_ts=last_user_sync_ts,
300307
status_msg=status_msg,
301308
currently_active=bool(currently_active),
309+
displayname=displayname,
310+
avatar_url=avatar_url,
302311
)
303-
for user_id, state, last_active_ts, last_federation_update_ts, last_user_sync_ts, status_msg, currently_active in rows
312+
for user_id, state, last_active_ts, last_federation_update_ts, last_user_sync_ts, status_msg, currently_active, displayname, avatar_url, in rows
304313
}
305314

306315
async def should_user_receive_full_presence_with_token(
@@ -427,6 +436,8 @@ async def get_presence_for_all_users(
427436
"last_user_sync_ts",
428437
"status_msg",
429438
"currently_active",
439+
"displayname",
440+
"avatar_url",
430441
),
431442
order_direction="ASC",
432443
),
@@ -440,6 +451,8 @@ async def get_presence_for_all_users(
440451
last_user_sync_ts,
441452
status_msg,
442453
currently_active,
454+
displayname,
455+
avatar_url,
443456
) in rows:
444457
users_to_state[user_id] = UserPresenceState(
445458
user_id=user_id,
@@ -449,6 +462,8 @@ async def get_presence_for_all_users(
449462
last_user_sync_ts=last_user_sync_ts,
450463
status_msg=status_msg,
451464
currently_active=bool(currently_active),
465+
displayname=displayname,
466+
avatar_url=avatar_url,
452467
)
453468

454469
# We've run out of updates to query
@@ -471,7 +486,8 @@ def _get_active_presence(self, db_conn: Connection) -> List[UserPresenceState]:
471486
# query.
472487
sql = (
473488
"SELECT user_id, state, last_active_ts, last_federation_update_ts,"
474-
" last_user_sync_ts, status_msg, currently_active FROM presence_stream"
489+
" last_user_sync_ts, status_msg, currently_active, displayname, avatar_url "
490+
" FROM presence_stream"
475491
" WHERE state != ?"
476492
)
477493

@@ -489,8 +505,10 @@ def _get_active_presence(self, db_conn: Connection) -> List[UserPresenceState]:
489505
last_user_sync_ts=last_user_sync_ts,
490506
status_msg=status_msg,
491507
currently_active=bool(currently_active),
508+
displayname=displayname,
509+
avatar_url=avatar_url,
492510
)
493-
for user_id, state, last_active_ts, last_federation_update_ts, last_user_sync_ts, status_msg, currently_active in rows
511+
for user_id, state, last_active_ts, last_federation_update_ts, last_user_sync_ts, status_msg, currently_active, displayname, avatar_url, in rows
494512
]
495513

496514
def take_presence_startup_info(self) -> List[UserPresenceState]:

synapse/storage/schema/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
#
2020
#
2121

22-
SCHEMA_VERSION = 84 # remember to update the list below when updating
22+
SCHEMA_VERSION = 85 # remember to update the list below when updating
2323
"""Represents the expectations made by the codebase about the database schema
2424
2525
This should be incremented whenever the codebase changes its requirements on the
@@ -132,12 +132,15 @@
132132
133133
Changes in SCHEMA_VERSION = 83
134134
- The event_txn_id is no longer used.
135+
136+
Changes in SCHEMA_VERSION = 85
137+
- Added displayname and avatar_url columns to presence_stream
135138
"""
136139

137140

138141
SCHEMA_COMPAT_VERSION = (
139-
# The event_txn_id table and tables from MSC2716 no longer exist.
140-
83
142+
# Added displayname and avatar_url columns to presence_stream
143+
85
141144
)
142145
"""Limit on how far the synapse codebase can be rolled back without breaking db compat
143146
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--
2+
-- This file is licensed under the Affero General Public License (AGPL) version 3.
3+
--
4+
-- Copyright (C) 2023 New Vector, Ltd
5+
--
6+
--
7+
-- This file is licensed under the Affero General Public License (AGPL) version 3.
8+
--
9+
-- Copyright (C) 2024 New Vector, Ltd
10+
--
11+
-- This program is free software: you can redistribute it and/or modify
12+
-- it under the terms of the GNU Affero General Public License as
13+
-- published by the Free Software Foundation, either version 3 of the
14+
-- License, or (at your option) any later version.
15+
--
16+
-- See the GNU Affero General Public License for more details:
17+
-- <https://www.gnu.org/licenses/agpl-3.0.html>.
18+
19+
ALTER TABLE presence_stream ADD COLUMN displayname TEXT;
20+
ALTER TABLE presence_stream ADD COLUMN avatar_url TEXT;

tests/api/test_filtering.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,8 @@ def test_filter_presence_match(self) -> None:
450450
last_user_sync_ts=0,
451451
status_msg=None,
452452
currently_active=False,
453+
displayname=None,
454+
avatar_url=None,
453455
),
454456
]
455457

@@ -478,6 +480,8 @@ def test_filter_presence_no_match(self) -> None:
478480
last_user_sync_ts=0,
479481
status_msg=None,
480482
currently_active=False,
483+
displayname=None,
484+
avatar_url=None,
481485
),
482486
]
483487

0 commit comments

Comments
 (0)