-
-
Notifications
You must be signed in to change notification settings - Fork 41
Add basic implementation for the presence endpoints. #137
Conversation
@mujx I get an error 'UserIdParam should ensure a UserId' during the test and i have no idea why this happen. Any idea? |
I forgot |
5900a73
to
eb5c515
Compare
I don't think this is ready for review because there are some test failures. The sync error is not related to the deadlocks. |
@mujx Fixed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good 👍
On a side note I don't think it's necessary to have three files for the presence endpoints.
presence TEXT NOT NULL, | ||
status_msg TEXT, | ||
updated_at TIMESTAMP NOT NULL DEFAULT now(), | ||
UNIQUE (id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You already have id as the primary key.
@@ -93,3 +93,27 @@ CREATE TABLE filters ( | |||
content TEXT NOT NULL, | |||
UNIQUE (id, user_id) | |||
); | |||
|
|||
CREATE TABLE presence_status ( | |||
id TEXT NOT NULL PRIMARY KEY, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NOT NULL
is implied. Also rename it to user_id
since it's not obvious unless you read the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If i rename this to use_id, i can't use Identifable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use #[primary_key(user_id)]
. Check out the changelog in diesel.
CREATE TABLE presence_list ( | ||
user_id TEXT NOT NULL, | ||
observed_user_id TEXT NOT NULL, | ||
UNIQUE (user_id, observed_user_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use them as composite primary key with PRIMARY KEY(user_id, observed_user_id)
UNIQUE (user_id, observed_user_id) | ||
); | ||
|
||
CREATE TABLE presence_stream ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing with presence_info
.
nit: Would it make more sense to rename it to presence_events
because it's the same thing with the events
table.
pub updated_at: SystemTime, | ||
} | ||
|
||
fn to_string(state: PresenceState) -> String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have opened a PR on ruma-events for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.map_err(ApiError::from)? | ||
}; | ||
|
||
let profiles: Vec<Profile> = profiles::table |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can move this into profiles too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think of adding profiles to the presence_stream table reduce requests and clean up the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you post here the schema you are proposing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CREATE TABLE presence_events (
ordering BIGSERIAL NOT NULL,
event_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
presence TEXT NOT NULL,
avatar_url TEXT,
displayname TEXT,
created_at TIMESTAMP NOT NULL DEFAULT now(),
UNIQUE (ordering)
);
|
||
let url = request.url.clone().into_generic_url(); | ||
let query_pairs = url.query_pairs().into_owned(); | ||
|
||
let mut filter = None; | ||
let mut since = None; | ||
let mut full_state = false; | ||
let mut set_presence = PresenceState::Offline; | ||
let mut set_presence = None; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default seems to be offline
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spec is inconsequence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem. You can keep it offline though to reduce the diff because it's not really necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed here, let's default to offline rather than making it an Option
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked deeper into synapse and they use Online as default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They updated it in the head version.
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. One of: ["offline"]
@@ -107,7 +113,7 @@ mod tests { | |||
filter: None, | |||
since: None, | |||
full_state: false, | |||
set_presence: PresenceState::Offline, | |||
set_presence: None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure why you changed those.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, let's switch back to offline.
.unwrap() | ||
.as_array() | ||
.unwrap(); | ||
assert_eq!(array.len(), 2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe show here the contents of the events.
} | ||
|
||
/// Return `PresenceEvent`'s for given `UserId`. | ||
pub fn find_events(connection: &PgConnection, user_id: &UserId, since: Option<i64>) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe find_events_per_user
? Because find_events
seems too generic.
Adding server behavior of presence in different PR request. |
@mujx I'm done with refactoring the requests. I'm open an issue due to the not resolved request. |
@@ -96,6 +96,7 @@ impl Test { | |||
domain: "ruma.test".to_string(), | |||
macaroon_secret_key: "YymznQHmKdN9B4f7iBalJB1tWEDy9LdaFSQJEtB3R5w=".into(), | |||
postgres_url: DATABASE_URL.to_string(), | |||
update_interval_presence: 3000000, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not really convinced that it's necessary to put this as a config variable. It seems like something that should be the same among the homeservers. @jimmycuadra thoughts?
@@ -117,6 +123,46 @@ impl User { | |||
Err(error) => Err(ApiError::from(error)), | |||
} | |||
} | |||
|
|||
pub fn find_missing_user_and_check_existence( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing docs (Run make ci
locally to avoid these errors).
nit: You can put all the arguments in the same line.
I don't think the naming is correct here. You provide a list of users and you want to know which are valid but you also do the error handling. Also you have hardcoded the error message and it would be the wrong one for the dropped users. You will need a test for that.
You can rename the function to find_missing_users
which returns a vector with the missing ones (if any). Then you check if the returned vector is not empty and you throw the appropriate error. Use !missing_users.is_empty()
like clippy suggests.
@@ -126,4 +130,17 @@ impl Profile { | |||
Err(err) => Err(ApiError::from(err)), | |||
} | |||
} | |||
|
|||
pub fn find_for_presence_list_by_uid( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing docs. Also the name is a little confusing.
You can split this into a function PresenceList::get_observed_user_list(UserId) or get_observed_users
for the first query and another function Profile::get_profiles(Vec<UserId>)
to retrieve the profiles.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is Diesel
limited. You can not do something like this Diesel, without sending multiple request to database. I don't like my solution, but it is better than sending to request.
Performance!!!
let mut presence_state: PresenceState = status.presence.parse().expect("Something wrong with the database!"); | ||
let now = SystemTime::now(); | ||
let last_active_ago = PresenceStatus::calculate_last_active_ago(status.updated_at, now)?; | ||
if last_active_ago > timeout && presence_state == PresenceState::Online { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same lines with presence_status
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed this part incorrect behavior.
.map_err(ApiError::from) | ||
} | ||
|
||
pub fn find_for_presence_list_by_uid( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing docs
.filter(presence_list::user_id.eq(user_id)) | ||
.select(presence_list::observed_user_id); | ||
|
||
if let Some(since) = since { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here you have essentially the same code in the if else
blocks. You can refactor this into a function and pass since
as an Option
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is Diesel
limited. You can not do something like this Diesel, without sending multiple request to database. I don't like my solution, but it is better than sending to request.
b26f225
to
11fec19
Compare
event_id TEXT PRIMARY KEY, | ||
user_id TEXT NOT NULL, | ||
presence TEXT NOT NULL, | ||
avatar_url TEXT, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems unnecessary. We already have tables for profiles.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done because the code is much cleaner. I don't have to build a complex code to combine these data. I could revert this easy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem with this is synchronization. You already have an avatar and display name when you save the presence event. Then you update your profile info but the presence event that you need to return has the old info. Updating the events with the new profile info seems awkward.
There are other events so that the client can learn about profile changes. So unless I'm missing something I don't see the point of sending/save them. Also synapse doesn't seem to include them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also I'm not so sure about the presence_events
table. Having a full history of these events doesn't have a use case (synapse deletes old entries) because we only need the latest ones. I was thinking about moving that info into presence_list
and then only updating those entries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like to split this into multiple request. We have to add queue to clean up and update events. I try to move it into presence_status, but i make it complicated to work with ordering. Saving the profile in presence_list
. Help to speed up the critical endpoint sync
, see synapse. Normally user doesn't change their profile often.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 for the link, that cleared things a bit. My main objection is about having the displayname
and avatar_url
into two separate tables. If you want these fields you just use the profiles table. You already have the user_id
so it would be fast enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will return to this ruma/ruma@fcb617a#diff-bb665a0481f7bd2c8bedef7b2dd9f702L117. If you are okay with it.
let connection = DB::from_request(request)?; | ||
|
||
let status = PresenceStatus::find_by_uid(&connection, &user_id)?; | ||
let status: PresenceStatus = match status { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just do match find_by_uid
here.
}; | ||
|
||
let presence_state: PresenceState = status.presence.parse() | ||
.expect("Something wrong with the database!"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really a database problem. Probably error while parsing presence state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't happened anytime.
} | ||
|
||
/// Calculate the difference between two SystemTimes in milliseconds. | ||
pub fn calculate_last_active_ago(since: SystemTime, now: SystemTime) -> Result<u64, ApiError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably be renamed to calculate_time_difference
.
57125a4
to
eb6e591
Compare
What's the status here? Are the items on the todo list in the PR description still to be done as part of this PR, or are those going to be in another PR? |
@mujx Can you look into two latest commits? I will update tests to the new schema, before i squash or resolve issues. |
Rebase on the current master. Also please try not to include any more changes in this PR. Generally prefer smaller consice changes that are easy to review and merge (for example the sync stuff could have been updated in a separate PR). |
My next PR will be smaller. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In models/precence_events.rs
we always use the latest events (max
ordering etc). This is natural because those events give us the most recent
info about the user's precence status which is what we want. Accessing older
entries for a user wouldn't make any sense because they contain outdated
information.
Given that these precence_events
don't have any historical
value we can remove the table and just keep the latest info inside the
presence_status entries (which essentialy is the same table). I have made some changes (as a proof of concept) on @farodin91 's branch to show my point. Should we do something like that or keep the current logic?
@@ -8,3 +8,7 @@ DROP TABLE room_memberships; | |||
DROP TABLE rooms; | |||
DROP TABLE users; | |||
DROP TABLE room_tags; | |||
DROP TABLE filters; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be related to #140?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so.
use bodyparser; | ||
use iron::status::Status; | ||
use iron::{Chain, Handler, IronResult, IronError, Plugin, Request, Response}; | ||
use ruma_identifiers::{UserId}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use ruma_identifiers::UserId;
|
||
let put_presence_status_request = match request.get::<bodyparser::Struct<PutPresenceStatusRequest>>() { | ||
Ok(Some(request)) => request, | ||
Ok(None) | Err(_) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok(None) | Err(_) => Err(ApiError::bad_json(None))?,
@@ -0,0 +1,172 @@ | |||
//! Storage and querying of presence status. | |||
|
|||
#[cfg(test)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need this line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need Duration
only for the test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh right didn't see that.
if user.id != user_id { | ||
let rooms = RoomMembership::find_shared_rooms_by_uid(&connection, &user.id, &user_id)?; | ||
if rooms.is_empty() { | ||
return Err(IronError::from( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Err(ApiError::unauthorized(
format!("...", user_id)
))?;
to make the line a little shorter.
@@ -126,4 +148,18 @@ impl Profile { | |||
Err(err) => Err(ApiError::from(err)), | |||
} | |||
} | |||
|
|||
/// Return `Profile`s for given `UserId` and his `PresenceList` entries. | |||
pub fn find_profiles_by_presence_list( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be a generic get_profiles(connection: &PgConnection, users: Vec<UserId>)
so it's not specific to the presence list.
let mut presence_state = PresenceState::Unavailable; | ||
let mut status_msg = None; | ||
|
||
match PresenceStatus::find_by_uid(connection, user_id)? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need this. You already call this inside upsert
.
Profile::create(connection, &new_profile) | ||
} | ||
} | ||
PresenceStatus::update_by_uid_and_status(connection, homeserver_domain, &user_id)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Call PresenceStatus::upsert
directly here.
Profile::create(connection, &new_profile) | ||
} | ||
} | ||
PresenceStatus::update_by_uid_and_status(connection, homeserver_domain, &user_id)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Call PresenceStatus::upsert
directly here.
.as_array() | ||
.unwrap(); | ||
let mut events = array.into_iter(); | ||
assert_eq!(events.len(), 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly this one entry is his own presence, because this user doesn't observe anyone else. Is this the correct behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes
@mujx That's look good. Could you push it in my branch? I'll give you access. |
Or open an PR i'll merge it. |
No need for write access. You can just add |
# State: - Add Endpoints - Fix migrations of table filters - Add only existing users (presence_list) - Support presence in sync with `since` - Support sync `set_presence` - Change some sync response types to event_type collections - Add config for custom presence timeout - Clean up `RoomMembership` by moving a user existence test to `User` - Update Status.md - Update `ruma-events` to 0.3.0 - Add check before getting `status` endpoint. (Alice and Bob must be in a same Room.) - Add check before updating `list` endpoint. (Alice and Bob must be in a same Room.) - Sending a `m.presence` event again after changing `avatar_url` or `displayname` # Fixes - ruma#39 - ruma#40 - ruma#41 - ruma#42
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found a bug on my previous patch and some other minor things.
edit: TIL the diesel timestamp is actually microseconds since January 1st 2000 not UNIX time .
)?; | ||
if rooms.is_empty() { | ||
Err(ApiError::unauthorized( | ||
format!("You are not authorized to get the presence status for th given user_id: {}.", user_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
th given
=> the given
let connection = DB::from_request(request)?; | ||
|
||
if user.id != user_id { | ||
let rooms = RoomMembership::find_common_joined_rooms( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename to find_common_rooms
because you already pass "join"
as a parameter. Or remove the membership parameter and keep the name.
Profile::create(connection, &new_profile) | ||
} | ||
} | ||
PresenceStatus::upsert(connection, homeserver_domain, &user_id, None, None)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be set to online by default?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No.
a
m.presence
presence status update is sent, again containing the new values of thedisplayname
andavatar_url
keys, in addition to the required presence key containing the current presence state of the user.
} | ||
|
||
/// Return `RoomId`'s for given `UserId`'s. | ||
pub fn find_common_joined_rooms( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be renamed to find_common_rooms
.
|
||
let now = time::get_time(); | ||
let last_update = time::Timespec::new(status.updated_at.0, 0); | ||
let last_active_ago: time::Duration = last_update - now; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be the other way around. i.e now - last_update
. Actually there is a bug here. When a new presence status entry is created in the database the default PgTimestamp format is used which is in microseconds and only when we update it we start using seconds. I would suggest to keep the default microseconds format for PgTimestamp and just convert to secs etc.
We also need a test to verify that the last_active_ago
parameter in the response is correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use every one time. Server or Database time.
}; | ||
|
||
// The precision is in seconds. | ||
thread::sleep(Duration::from_secs(2)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably don't need those if we keep the microseconds timestamp.
self.event_id = event_id.clone(); | ||
|
||
// Use seconds instead of microseconds (default for PgTimestamp) | ||
self.updated_at = PgTimestamp(time::get_time().sec); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be added automatically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm only using datetimes generated by the server.
) -> Result<Vec<PresenceStatus>, ApiError> { | ||
match since { | ||
Some(since) => { | ||
let time = PgTimestamp(since.sec); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need microseconds here
} | ||
|
||
/// Return `RoomId`'s for given `RoomId`'s and `UserId`. | ||
pub fn get_common_rooms( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe rename it to filter_rooms_by_state
?. You only have one user so the naming is a little off.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great job 👍
@@ -52,6 +54,20 @@ pub struct PresenceStatus { | |||
pub updated_at: PgTimestamp, | |||
} | |||
|
|||
/// Return now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe Return current time in milliseconds
?
@@ -52,6 +54,20 @@ pub struct PresenceStatus { | |||
pub updated_at: PgTimestamp, | |||
} | |||
|
|||
/// Return now | |||
pub fn get_now() -> i64 { | |||
// Use seconds instead of microseconds (default for PgTimestamp) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can remove this comment.
@jimmycuadra Any progress? |
Sorry I've left this sitting! I'll review it within a couple days. |
State:
since
set_presence
RoomMembership
by moving a user existence test toUser
ruma-events
to 0.3.0status
endpoint. (Alice and Bob must be in a same Room.)list
endpoint. (Alice and Bob must be in a same Room.)m.presence
event again after changingavatar_url
ordisplayname
ToDo
last_active_ago
andcurrently_active
to work as the spec says.Fixes