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

Add presence support #151

Merged
merged 32 commits into from
May 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e9c4806
Add UserPresence enum to rooms.py
May 28, 2020
f791781
Rename user presence enum
May 28, 2020
beebe39
Add get_presence and set_presence to api.py
May 28, 2020
a43ba74
Add presence to sync.json
May 28, 2020
5a690bf
Add presence schema
May 28, 2020
2b1fa90
Add presence events to SyncResponse
May 28, 2020
f3b0483
Add empty presence list in tests
May 28, 2020
314dce7
Fix circular import
May 28, 2020
af4b46a
Handle presence events in base client and async client
May 28, 2020
c300baf
Add last_active_ago and currently_active to MatrixUser
May 28, 2020
f7fb7b6
Add PresenceEvent
May 28, 2020
e7cd4af
Remove unnecessary MatrixUserPresence enum
May 29, 2020
76189eb
Add get/set presence response/error response
May 29, 2020
9de351a
Add get/set presence to async_client.py
May 29, 2020
4c36850
Make mypy happy and extend test
May 29, 2020
21c0a84
Add example presence event to sync.json
May 29, 2020
393027e
Add presence section to sync schema
May 29, 2020
d5ae862
Fix dict in presence event
May 29, 2020
7a42a6b
Add test for sync presence event in async_client_test.py
May 29, 2020
8f6c099
Increase test coverage
May 29, 2020
153ebeb
Fix indentation and clarify docstring
May 29, 2020
1650aea
Add support for set_presence to api
May 29, 2020
b94a659
Add presence field to AsyncClient to prevent overriding a previous se…
May 29, 2020
ec89aac
Fix typo api sync docstring
May 29, 2020
cb8db8a
Add presence callbacks
May 29, 2020
5f2370e
Add optional user_id to get_presence and PresenceGetResponse
May 29, 2020
8380ddb
Make AsyncClient presence field private
May 29, 2020
c88de9f
Add test for presence callback
May 29, 2020
876773c
Add test for presence callback in client_test.py
May 29, 2020
4a42576
Handle presence response after manual get_presence
May 30, 2020
a5ba113
Default to False if currently_active is not set
May 30, 2020
592efc8
Move set_presence argument from client creation to sync/sync_forever
May 30, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion nio/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ def sync(
timeout: Optional[int] = None,
filter: _FilterT = None,
full_state: Optional[bool] = None,
set_presence: Optional[str] = None,
):
# type: (...) -> Tuple[str, str]
"""Synchronise the client's state with the latest state on the server.
Expand All @@ -408,13 +409,20 @@ def sync(
to.
timeout (int): The maximum time to wait, in milliseconds, before
returning this request.
sync_filter (Union[None, str, Dict[Any, Any]):
filter (Union[None, str, Dict[Any, Any]):
A filter ID or dict that should be used for this sync request.
full_state (bool, optional): Controls whether to include the full
state for all rooms the user is a member of. If this is set to
true, then all state events will be returned, even if since is
non-empty. The timeline will still be limited by the since
parameter.
set_presence (str, optinal): Controls whether the client is automatically
marked as online by polling this API. If this parameter is omitted
then the client is automatically marked as online when it uses this API.
Otherwise if the parameter is set to "offline" then the client is not
marked as being online when it uses this API. When set to "unavailable",
the client is marked as being idle.
One of: ["offline", "online", "unavailable"]
"""
query_parameters = {"access_token": access_token}

Expand All @@ -427,6 +435,9 @@ def sync(
if timeout is not None:
query_parameters["timeout"] = str(timeout)

if set_presence:
query_parameters["set_presence"] = set_presence

if isinstance(filter, dict):
filter_json = json.dumps(filter, separators=(",", ":"))
query_parameters["filter"] = filter_json
Expand Down Expand Up @@ -1483,6 +1494,48 @@ def profile_set_avatar(access_token, user_id, avatar_url):
Api.to_json(content)
)

@staticmethod
def get_presence(access_token: str, user_id: str) -> Tuple[str, str]:
"""Get the given user's presence state.

Returns the HTTP method and HTTP path for the request.

Args:
access_token (str): The access token to be used with the request.
user_id (str): User id whose presence state to get.
"""
query_parameters = {"access_token": access_token}
path = "presence/{user_id}/status".format(user_id=user_id)

return (
"GET",
Api._build_path(path, query_parameters),
)

@staticmethod
def set_presence(access_token: str, user_id: str, presence: str, status_msg: str = None):
"""This API sets the given user's presence state.

Returns the HTTP method, HTTP path and data for the request.

Args:
access_token (str): The access token to be used with the request.
user_id (str): User id whose presence state to get.
presence (str): The new presence state.
status_msg (str, optional): The status message to attach to this state.
"""
query_parameters = {"access_token": access_token}
content = {"presence": presence}
if status_msg:
content["status_msg"] = status_msg
path = "presence/{user_id}/status".format(user_id=user_id)

return (
"PUT",
Api._build_path(path, query_parameters),
Api.to_json(content)
)

@staticmethod
def whoami(access_token):
# type (str) -> Tuple[str, str]
Expand Down
100 changes: 98 additions & 2 deletions nio/client/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@
ProfileSetAvatarError,
ProfileSetDisplayNameResponse,
ProfileSetDisplayNameError,
PresenceGetResponse,
PresenceGetError,
PresenceSetResponse,
PresenceSetError,
Response,
RoomBanError,
RoomBanResponse,
Expand Down Expand Up @@ -363,6 +367,8 @@ def __init__(
self.ssl = ssl
self.proxy = proxy

self._presence: Optional[str] = None

self.synced = AsyncioEvent()
self.response_callbacks: List[ResponseCb] = []

Expand Down Expand Up @@ -559,6 +565,21 @@ async def _handle_joined_rooms(self, response: SyncType) -> None:
if self.store:
self.store.save_encrypted_rooms(encrypted_rooms)

async def _handle_presence_events(self, response: SyncType):
devNan0 marked this conversation as resolved.
Show resolved Hide resolved
for event in response.presence_events:
for room_id in self.rooms.keys():
if event.user_id not in self.rooms[room_id].users:
continue

self.rooms[room_id].users[event.user_id].presence = event.presence
self.rooms[room_id].users[event.user_id].last_active_ago = event.last_active_ago
self.rooms[room_id].users[event.user_id].currently_active = event.currently_active
self.rooms[room_id].users[event.user_id].status_msg = event.status_msg

for cb in self.presence_callbacks:
if cb.filter is None or isinstance(event, cb.filter):
await asyncio.coroutine(cb.func)(event)

async def _handle_expired_verifications(self):
expired_verifications = self.olm.clear_verifications()

Expand All @@ -584,6 +605,8 @@ async def _handle_sync(self, response: SyncType) -> None:

await self._handle_joined_rooms(response)

await self._handle_presence_events(response)

if self.olm:
await self._handle_expired_verifications()
self._handle_olm_events(response)
Expand Down Expand Up @@ -877,6 +900,7 @@ async def sync(
sync_filter: _FilterT = None,
since: Optional[str] = None,
full_state: Optional[bool] = None,
set_presence: Optional[str] = None,
) -> Union[SyncResponse, SyncError]:
"""Synchronise the client's state with the latest state on the server.

Expand Down Expand Up @@ -904,18 +928,22 @@ async def sync(
since (str, optional): A token specifying a point in time where to
continue the sync from. Defaults to the last sync token we
received from the server using this API call.
set_presence (str, optional): The presence state.
One of: ["online", "offline", "unavailable"]

Returns either a `SyncResponse` if the request was successful or
a `SyncError` if there was an error with the request.
"""

sync_token = since or self.next_batch
presence = set_presence or self._presence
method, path = Api.sync(
self.access_token,
since=sync_token or self.loaded_sync_token,
timeout=timeout or None,
filter=sync_filter,
full_state=full_state,
set_presence=presence,
)

response = await self._send(
Expand Down Expand Up @@ -974,6 +1002,7 @@ async def sync_forever(
full_state: Optional[bool] = None,
loop_sleep_time: Optional[int] = None,
first_sync_filter: _FilterT = None,
set_presence: Optional[str] = None,
):
"""Continuously sync with the configured homeserver.

Expand Down Expand Up @@ -1021,6 +1050,9 @@ async def sync_forever(
is used.
To have no filtering for the first sync regardless of
`sync_filter`'s value, pass `{}`.

set_presence (str, optional): The presence state.
One of: ["online", "offline", "unavailable"]
"""

first_sync = True
Expand All @@ -1036,13 +1068,15 @@ async def sync_forever(
# before the other requests, this helps to ensure that after one
# fired synced event the state is indeed fully synced.
if first_sync:
sync_response = await self.sync(use_timeout, use_filter, since, full_state)
presence = set_presence or self._presence
sync_response = await self.sync(use_timeout, use_filter, since, full_state, presence)
await self.run_response_callbacks([sync_response])
else:
presence = set_presence or self._presence
tasks = [
asyncio.ensure_future(coro)
for coro in (
self.sync(use_timeout, use_filter, since, full_state),
self.sync(use_timeout, use_filter, since, full_state, presence),
self.send_to_device_messages(),
)
]
Expand Down Expand Up @@ -2578,6 +2612,68 @@ async def get_profile(

return await self._send(ProfileGetResponse, method, path,)

@client_session
async def get_presence(self, user_id: str) -> Union[PresenceGetResponse, PresenceGetError]:
"""Get a user's presence state.

This queries the presence state of a user from the server.

Calls receive_response() to update the client state if necessary.

Returns either a `PresenceGetResponse` if the request was
successful or a `PresenceGetError` if there was an error
with the request.

Args:
user_id (str): User id of the user to get the presence state for.
"""

method, path = Api.get_presence(
self.access_token,
user_id
)

return await self._send(
PresenceGetResponse, method, path
)

@client_session
async def set_presence(
self,
presence: str,
status_msg: str = None
) -> Union[PresenceSetResponse, PresenceSetError]:
"""Set our user's presence state.

This tells the server to set presence state of the currently logged
in user to the supplied string.

Calls receive_response() to update the client state if necessary.

Returns either a `PresenceSetResponse` if the request was
successful or a `PresenceSetError` if there was an error
with the request.

Args:
presence (str): The new presence state. One of: ["online", "offline", "unavailable"]
status_msg (str, optional): The status message to attach to this state.
"""

method, path, data = Api.set_presence(
self.access_token,
self.user_id,
presence,
status_msg
)

resp = await self._send(
PresenceSetResponse, method, path, data
)
if isinstance(resp, PresenceSetResponse):
self._presence = presence

return resp

@client_session
async def get_displayname(
self, user_id: Optional[str] = None
Expand Down
52 changes: 52 additions & 0 deletions nio/client/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
ToDeviceEvent,
RoomKeyRequest,
RoomKeyRequestCancellation,
PresenceEvent,
)
from ..exceptions import LocalProtocolError, MembersSyncError, EncryptionError
from ..log import logger_group
Expand All @@ -68,6 +69,7 @@
SyncResponse,
SyncType,
ToDeviceResponse,
PresenceGetResponse,
)
from ..rooms import MatrixInvitedRoom, MatrixRoom

Expand Down Expand Up @@ -215,6 +217,7 @@ def __init__(
self.event_callbacks: List[ClientCallback] = []
self.ephemeral_callbacks: List[ClientCallback] = []
self.to_device_callbacks: List[ClientCallback] = []
self.presence_callbacks: List[ClientCallback] = []

@property
def logged_in(self) -> bool:
Expand Down Expand Up @@ -765,6 +768,21 @@ def _handle_joined_rooms(self, response: SyncType):
if self.store:
self.store.save_encrypted_rooms(encrypted_rooms)

def _handle_presence_events(self, response: SyncType):
for event in response.presence_events:
for room_id in self.rooms.keys():
if event.user_id not in self.rooms[room_id].users:
continue

self.rooms[room_id].users[event.user_id].presence = event.presence
self.rooms[room_id].users[event.user_id].last_active_ago = event.last_active_ago
self.rooms[room_id].users[event.user_id].currently_active = event.currently_active
self.rooms[room_id].users[event.user_id].status_msg = event.status_msg

for cb in self.presence_callbacks:
if cb.filter is None or isinstance(event, cb.filter):
cb.func(event)

def _handle_expired_verifications(self):
expired_verifications = self.olm.clear_verifications()

Expand Down Expand Up @@ -818,6 +836,8 @@ def _handle_sync(

self._handle_joined_rooms(response)

self._handle_presence_events(response)

if self.olm:
self._handle_expired_verifications()
self._handle_olm_events(response)
Expand Down Expand Up @@ -943,6 +963,17 @@ def _handle_room_forget_response(self, response: RoomForgetResponse):
elif response.room_id in self.invited_rooms:
del self.invited_rooms[response.room_id]

def _handle_presence_response(self, response: PresenceGetResponse):
if response.user_id:
for room_id in self.rooms.keys():
if response.user_id not in self.rooms[room_id].users:
continue

self.rooms[room_id].users[response.user_id].presence = response.presence
self.rooms[room_id].users[response.user_id].last_active_ago = response.last_active_ago
self.rooms[room_id].users[response.user_id].currently_active = response.currently_active or False
self.rooms[room_id].users[response.user_id].status_msg = response.status_msg

def receive_response(
self, response: Response
) -> Union[None, Coroutine[Any, Any, None]]:
Expand Down Expand Up @@ -992,6 +1023,8 @@ def receive_response(
response.event = self.decrypt_event(response.event)
except EncryptionError:
pass
elif isinstance(response, PresenceGetResponse):
self._handle_presence_response(response)
elif isinstance(response, ErrorResponse):
if response.soft_logout:
self.access_token = ""
Expand Down Expand Up @@ -1188,6 +1221,25 @@ def add_to_device_callback(
cb = ClientCallback(callback, filter)
self.to_device_callbacks.append(cb)

def add_presence_callback(
self,
callback: Callable[[PresenceEvent], None],
filter: Union[Type, Tuple[Type]],
):
"""Add a callback that will be executed on presence events.

Args:
callback (Callable[[PresenceEvent], None]): A function that will be
called if the event type in the filter argument is found in a
the presence part of the sync response.
filter (Union[Type, Tuple[Type]]): The event type or a tuple
containing multiple types for which the function
will be called.

"""
cb = ClientCallback(callback, filter)
self.presence_callbacks.append(cb)

@store_loaded
def create_key_verification(self, device: OlmDevice) -> ToDeviceMessage:
"""Start a new key verification process with the given device.
Expand Down
1 change: 1 addition & 0 deletions nio/events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
from .account_data import *
from .invite_events import *
from .misc import *
from .presence import *
Loading