Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added presence update on change of profile information and config flags for selective presence tracking #16992

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
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 tracking of user profile updates and config flags for disabling user activity tracking. Contributed by @Michael-Hollister.
21 changes: 19 additions & 2 deletions docs/usage/configuration/config_documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ Example configuration:
presence:
enabled: false
include_offline_users_on_sync: false
local_activity_tracking: true
remote_activity_tracking: true
```

`enabled` can also be set to a special value of "untracked" which ignores updates
Expand All @@ -259,6 +261,21 @@ When clients perform an initial or `full_state` sync, presence results for offli
not included by default. Setting `include_offline_users_on_sync` to `true` will always include
offline users in the results. Defaults to false.

Enabling presence tracking can be resource intensive for the presence handler when server-side
tracking of user activity is enabled. Below are some additional configuration options which may
help improve the performance of the presence feature without outright disabling it:
* `local_activity_tracking` (Default enabled): Determines if the server tracks a user's activity
when syncing or fetching events. If disabled, the server will not automatically update the
user's presence activity when the /sync or /events endpoints are called. Note that client
applications can still update their presence by calling the presence /status endpoint.
* `remote_activity_tracking` (Default enabled): Determines if the server will accept presence
EDUs from remote servers that are exclusively user activity updates. If disabled, the server
will reject processing these EDUs. However if a presence EDU contains profile updates to any of
the `status_msg`, `displayname`, or `avatar_url` fields, then the server will accept the EDU.

If the presence `enabled` field is set to "untracked", then these options will both act as if
set to false.

---
### `require_auth_for_profile_requests`

Expand Down Expand Up @@ -1765,7 +1782,7 @@ rc_3pid_validation:

This option sets ratelimiting how often invites can be sent in a room or to a
specific user. `per_room` defaults to `per_second: 0.3`, `burst_count: 10`,
`per_user` defaults to `per_second: 0.003`, `burst_count: 5`, and `per_issuer`
`per_user` defaults to `per_second: 0.003`, `burst_count: 5`, and `per_issuer`
defaults to `per_second: 0.3`, `burst_count: 10`.

Client requests that invite user(s) when [creating a
Expand Down Expand Up @@ -1966,7 +1983,7 @@ max_image_pixels: 35M
---
### `remote_media_download_burst_count`

Remote media downloads are ratelimited using a [leaky bucket algorithm](https://en.wikipedia.org/wiki/Leaky_bucket), where a given "bucket" is keyed to the IP address of the requester when requesting remote media downloads. This configuration option sets the size of the bucket against which the size in bytes of downloads are penalized - if the bucket is full, ie a given number of bytes have already been downloaded, further downloads will be denied until the bucket drains. Defaults to 500MiB. See also `remote_media_download_per_second` which determines the rate at which the "bucket" is emptied and thus has available space to authorize new requests.
Remote media downloads are ratelimited using a [leaky bucket algorithm](https://en.wikipedia.org/wiki/Leaky_bucket), where a given "bucket" is keyed to the IP address of the requester when requesting remote media downloads. This configuration option sets the size of the bucket against which the size in bytes of downloads are penalized - if the bucket is full, ie a given number of bytes have already been downloaded, further downloads will be denied until the bucket drains. Defaults to 500MiB. See also `remote_media_download_per_second` which determines the rate at which the "bucket" is emptied and thus has available space to authorize new requests.

Example configuration:
```yaml
Expand Down
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 @@ -389,6 +389,16 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
"include_offline_users_on_sync", False
)

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

# Disabling federation presence tracking
self.presence_remote_activity_tracking = presence_config.get(
"remote_activity_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
21 changes: 21 additions & 0 deletions synapse/federation/federation_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1425,9 +1425,30 @@ def register_instances_for_edu(
self._edu_type_to_instance[edu_type] = instance_names

async def on_edu(self, edu_type: str, origin: str, content: dict) -> None:
"""Passes an EDU to a registered handler if one exists

This potentially modifies the `content` dict for `m.presence` EDUs when
presence `remote_activity_tracking` is disabled.

Args:
edu_type: The type of the incoming EDU to process
origin: The server we received the event from
content: The content of the EDU
"""
if not self.config.server.track_presence and edu_type == EduTypes.PRESENCE:
return

if (
not self.config.server.presence_remote_activity_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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a docstring to on_edu mentioning that it potentially modifies the passed content dictionary?

Looking at the calling functions, this isn't a problem. But it's good to call out for future reference.


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

self._presence_enabled = hs.config.server.presence_enabled
self._track_presence = hs.config.server.track_presence
self._presence_local_activity_tracking = (
hs.config.server.presence_local_activity_tracking
)

self._federation = None
if hs.should_send_federation():
Expand Down Expand Up @@ -451,6 +454,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 +584,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._presence_local_activity_tracking
):
return _NullContextManager()

# Note that this causes last_active_ts to be incremented which is not
Expand Down Expand Up @@ -648,6 +657,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 +1151,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._presence_local_activity_tracking
):
return _NullContextManager()

curr_sync = self._user_device_to_num_current_syncs.get((user_id, device_id), 0)
Expand Down Expand Up @@ -1340,6 +1355,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 +1386,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 +1433,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 +1655,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 +1691,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 +1756,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 +1766,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 +1780,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 @@ -202,6 +202,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 @@ -295,6 +308,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
Loading
Loading