Skip to content

Commit

Permalink
Added presence update on change of profile information and config fla…
Browse files Browse the repository at this point in the history
…gs for selective presence tracking

Signed-off-by: Michael Hollister <[email protected]>
  • Loading branch information
Michael-Hollister committed Mar 11, 2024
1 parent 696cc9e commit 28e1f2f
Show file tree
Hide file tree
Showing 12 changed files with 150 additions and 10 deletions.
1 change: 1 addition & 0 deletions changelog.d/16992.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added presence update on change of profile information and config flags for selective presence tracking. Contributed by @Michael-Hollister.
4 changes: 4 additions & 0 deletions synapse/api/presence.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ class UserPresenceState:
last_user_sync_ts: int
status_msg: Optional[str]
currently_active: bool
displayname: Optional[str]
avatar_url: Optional[str]

def as_dict(self) -> JsonDict:
return attr.asdict(self)
Expand All @@ -101,4 +103,6 @@ def default(cls, user_id: str) -> "UserPresenceState":
last_user_sync_ts=0,
status_msg=None,
currently_active=False,
displayname=None,
avatar_url=None,
)
10 changes: 10 additions & 0 deletions synapse/config/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,16 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
# Whether to internally track presence, requires that presence is enabled,
self.track_presence = self.presence_enabled and presence_enabled != "untracked"

# Disabling server-side presence tracking
self.sync_presence_tracking = presence_config.get(
"sync_presence_tracking", True
)

# Disabling federation presence tracking
self.federation_presence_tracking = presence_config.get(
"federation_presence_tracking", True
)

# Custom presence router module
# This is the legacy way of configuring it (the config should now be put in the modules section)
self.presence_router_module_class = None
Expand Down
11 changes: 11 additions & 0 deletions synapse/federation/federation_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1410,6 +1410,17 @@ async def on_edu(self, edu_type: str, origin: str, content: dict) -> None:
if not self.config.server.track_presence and edu_type == EduTypes.PRESENCE:
return

if (
not self.config.server.federation_presence_tracking
and edu_type == EduTypes.PRESENCE
):
filtered_edus = []
for e in content["push"]:
# Process only profile presence updates to reduce resource impact
if "status_msg" in e or "displayname" in e or "avatar_url" in e:
filtered_edus.append(e)
content["push"] = filtered_edus

# Check if we have a handler on this instance
handler = self.edu_handlers.get(edu_type)
if handler:
Expand Down
43 changes: 40 additions & 3 deletions synapse/handlers/presence.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ def __init__(self, hs: "HomeServer"):

self._presence_enabled = hs.config.server.presence_enabled
self._track_presence = hs.config.server.track_presence
self._sync_presence_tracking = hs.config.server.sync_presence_tracking

self._federation = None
if hs.should_send_federation():
Expand Down Expand Up @@ -451,6 +452,8 @@ async def send_full_presence_to_users(self, user_ids: StrCollection) -> None:
state = {
"presence": current_presence_state.state,
"status_message": current_presence_state.status_msg,
"displayname": current_presence_state.displayname,
"avatar_url": current_presence_state.avatar_url,
}

# Copy the presence state to the tip of the presence stream.
Expand Down Expand Up @@ -579,7 +582,11 @@ async def user_syncing(
Called by the sync and events servlets to record that a user has connected to
this worker and is waiting for some events.
"""
if not affect_presence or not self._track_presence:
if (
not affect_presence
or not self._track_presence
or not self._sync_presence_tracking
):
return _NullContextManager()

# Note that this causes last_active_ts to be incremented which is not
Expand Down Expand Up @@ -648,6 +655,8 @@ async def process_replication_rows(
row.last_user_sync_ts,
row.status_msg,
row.currently_active,
row.displayname,
row.avatar_url,
)
for row in rows
]
Expand Down Expand Up @@ -1140,7 +1149,11 @@ async def user_syncing(
client that is being used by a user.
presence_state: The presence state indicated in the sync request
"""
if not affect_presence or not self._track_presence:
if (
not affect_presence
or not self._track_presence
or not self._sync_presence_tracking
):
return _NullContextManager()

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

new_fields["status_msg"] = push.get("status_msg", None)
new_fields["currently_active"] = push.get("currently_active", False)
new_fields["displayname"] = push.get("displayname", None)
new_fields["avatar_url"] = push.get("avatar_url", None)

prev_state = await self.current_state_for_user(user_id)
updates.append(prev_state.copy_and_replace(**new_fields))
Expand Down Expand Up @@ -1369,6 +1384,8 @@ async def set_state(
the `state` dict.
"""
status_msg = state.get("status_msg", None)
displayname = state.get("displayname", None)
avatar_url = state.get("avatar_url", None)
presence = state["presence"]

if presence not in self.VALID_PRESENCE:
Expand Down Expand Up @@ -1414,6 +1431,8 @@ async def set_state(
else:
# Syncs do not override the status message.
new_fields["status_msg"] = status_msg
new_fields["displayname"] = displayname
new_fields["avatar_url"] = avatar_url

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

await self._federation_queue.send_presence_to_destinations(
Expand Down Expand Up @@ -1668,6 +1689,14 @@ def should_notify(
notify_reason_counter.labels(user_location, "status_msg_change").inc()
return True

if old_state.displayname != new_state.displayname:
notify_reason_counter.labels(user_location, "displayname_change").inc()
return True

if old_state.avatar_url != new_state.avatar_url:
notify_reason_counter.labels(user_location, "avatar_url_change").inc()
return True

if old_state.state != new_state.state:
notify_reason_counter.labels(user_location, "state_change").inc()
state_transition_counter.labels(
Expand Down Expand Up @@ -1725,6 +1754,8 @@ def format_user_presence_state(
* status_msg: Optional. Included if `status_msg` is set on `state`. The user's
status.
* currently_active: Optional. Included only if `state.state` is "online".
* displayname: Optional. The current display name for this user, if any.
* avatar_url: Optional. The current avatar URL for this user, if any.
Example:
Expand All @@ -1733,7 +1764,9 @@ def format_user_presence_state(
"user_id": "@alice:example.com",
"last_active_ago": 16783813918,
"status_msg": "Hello world!",
"currently_active": True
"currently_active": True,
"displayname": "Alice",
"avatar_url": "mxc://localhost/wefuiwegh8742w"
}
"""
content: JsonDict = {"presence": state.state}
Expand All @@ -1745,6 +1778,10 @@ def format_user_presence_state(
content["status_msg"] = state.status_msg
if state.state == PresenceState.ONLINE:
content["currently_active"] = state.currently_active
if state.displayname:
content["displayname"] = state.displayname
if state.avatar_url:
content["avatar_url"] = state.avatar_url

return content

Expand Down
26 changes: 26 additions & 0 deletions synapse/handlers/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,19 @@ async def set_displayname(
if propagate:
await self._update_join_states(requester, target_user)

if self.hs.config.server.track_presence:
presence_handler = self.hs.get_presence_handler()
current_presence_state = await presence_handler.get_state(target_user)

state = {
"presence": current_presence_state.state,
"status_message": current_presence_state.status_msg,
"displayname": new_displayname,
"avatar_url": current_presence_state.avatar_url,
}

await presence_handler.set_state(target_user, requester.device_id, state)

async def get_avatar_url(self, target_user: UserID) -> Optional[str]:
if self.hs.is_mine(target_user):
try:
Expand Down Expand Up @@ -293,6 +306,19 @@ async def set_avatar_url(
if propagate:
await self._update_join_states(requester, target_user)

if self.hs.config.server.track_presence:
presence_handler = self.hs.get_presence_handler()
current_presence_state = await presence_handler.get_state(target_user)

state = {
"presence": current_presence_state.state,
"status_message": current_presence_state.status_msg,
"displayname": current_presence_state.displayname,
"avatar_url": new_avatar_url,
}

await presence_handler.set_state(target_user, requester.device_id, state)

@cached()
async def check_avatar_size_and_mime_type(self, mxc: str) -> bool:
"""Check that the size and content type of the avatar at the given MXC URI are
Expand Down
2 changes: 2 additions & 0 deletions synapse/replication/tcp/streams/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@ class PresenceStreamRow:
last_user_sync_ts: int
status_msg: str
currently_active: bool
displayname: str
avatar_url: str

NAME = "presence"
ROW_TYPE = PresenceStreamRow
Expand Down
26 changes: 22 additions & 4 deletions synapse/storage/databases/main/presence.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ def _update_presence_txn(
"last_user_sync_ts",
"status_msg",
"currently_active",
"displayname",
"avatar_url",
"instance_name",
),
values=[
Expand All @@ -193,6 +195,8 @@ def _update_presence_txn(
state.last_user_sync_ts,
state.status_msg,
state.currently_active,
state.displayname,
state.avatar_url,
self._instance_name,
)
for stream_id, state in zip(stream_orderings, presence_states)
Expand Down Expand Up @@ -232,7 +236,8 @@ def get_all_presence_updates_txn(
sql = """
SELECT stream_id, user_id, state, last_active_ts,
last_federation_update_ts, last_user_sync_ts,
status_msg, currently_active
status_msg, currently_active, displayname,
avatar_url
FROM presence_stream
WHERE ? < stream_id AND stream_id <= ?
ORDER BY stream_id ASC
Expand Down Expand Up @@ -285,6 +290,8 @@ async def get_presence_for_users(
"last_user_sync_ts",
"status_msg",
"currently_active",
"displayname",
"avatar_url",
),
desc="get_presence_for_users",
),
Expand All @@ -299,8 +306,10 @@ async def get_presence_for_users(
last_user_sync_ts=last_user_sync_ts,
status_msg=status_msg,
currently_active=bool(currently_active),
displayname=displayname,
avatar_url=avatar_url,
)
for user_id, state, last_active_ts, last_federation_update_ts, last_user_sync_ts, status_msg, currently_active in rows
for user_id, state, last_active_ts, last_federation_update_ts, last_user_sync_ts, status_msg, currently_active, displayname, avatar_url, in rows
}

async def should_user_receive_full_presence_with_token(
Expand Down Expand Up @@ -427,6 +436,8 @@ async def get_presence_for_all_users(
"last_user_sync_ts",
"status_msg",
"currently_active",
"displayname",
"avatar_url",
),
order_direction="ASC",
),
Expand All @@ -440,6 +451,8 @@ async def get_presence_for_all_users(
last_user_sync_ts,
status_msg,
currently_active,
displayname,
avatar_url,
) in rows:
users_to_state[user_id] = UserPresenceState(
user_id=user_id,
Expand All @@ -449,6 +462,8 @@ async def get_presence_for_all_users(
last_user_sync_ts=last_user_sync_ts,
status_msg=status_msg,
currently_active=bool(currently_active),
displayname=displayname,
avatar_url=avatar_url,
)

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

Expand All @@ -489,8 +505,10 @@ def _get_active_presence(self, db_conn: Connection) -> List[UserPresenceState]:
last_user_sync_ts=last_user_sync_ts,
status_msg=status_msg,
currently_active=bool(currently_active),
displayname=displayname,
avatar_url=avatar_url,
)
for user_id, state, last_active_ts, last_federation_update_ts, last_user_sync_ts, status_msg, currently_active in rows
for user_id, state, last_active_ts, last_federation_update_ts, last_user_sync_ts, status_msg, currently_active, displayname, avatar_url, in rows
]

def take_presence_startup_info(self) -> List[UserPresenceState]:
Expand Down
9 changes: 6 additions & 3 deletions synapse/storage/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
#
#

SCHEMA_VERSION = 84 # remember to update the list below when updating
SCHEMA_VERSION = 85 # remember to update the list below when updating
"""Represents the expectations made by the codebase about the database schema
This should be incremented whenever the codebase changes its requirements on the
Expand Down Expand Up @@ -132,12 +132,15 @@
Changes in SCHEMA_VERSION = 83
- The event_txn_id is no longer used.
Changes in SCHEMA_VERSION = 85
- Added displayname and avatar_url columns to presence_stream
"""


SCHEMA_COMPAT_VERSION = (
# The event_txn_id table and tables from MSC2716 no longer exist.
83
# Added displayname and avatar_url columns to presence_stream
85
)
"""Limit on how far the synapse codebase can be rolled back without breaking db compat
Expand Down
20 changes: 20 additions & 0 deletions synapse/storage/schema/main/delta/85/01presence_stream_updates.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--
-- This file is licensed under the Affero General Public License (AGPL) version 3.
--
-- Copyright (C) 2023 New Vector, Ltd
--
--
-- This file is licensed under the Affero General Public License (AGPL) version 3.
--
-- Copyright (C) 2024 New Vector, Ltd
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as
-- published by the Free Software Foundation, either version 3 of the
-- License, or (at your option) any later version.
--
-- See the GNU Affero General Public License for more details:
-- <https://www.gnu.org/licenses/agpl-3.0.html>.

ALTER TABLE presence_stream ADD COLUMN displayname TEXT;
ALTER TABLE presence_stream ADD COLUMN avatar_url TEXT;
4 changes: 4 additions & 0 deletions tests/api/test_filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,8 @@ def test_filter_presence_match(self) -> None:
last_user_sync_ts=0,
status_msg=None,
currently_active=False,
displayname=None,
avatar_url=None,
),
]

Expand Down Expand Up @@ -478,6 +480,8 @@ def test_filter_presence_no_match(self) -> None:
last_user_sync_ts=0,
status_msg=None,
currently_active=False,
displayname=None,
avatar_url=None,
),
]

Expand Down
Loading

0 comments on commit 28e1f2f

Please sign in to comment.