Skip to content

Commit

Permalink
implement more sync event handling
Browse files Browse the repository at this point in the history
  • Loading branch information
rschili committed Dec 28, 2024
1 parent 37a3f83 commit b4653e9
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 22 deletions.
150 changes: 129 additions & 21 deletions src/Narrensicher.Matrix/MatrixTextClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Narrensicher.Matrix.Http;
using System.Reflection.Metadata;
using System.Collections.Concurrent;
using System.Text.Json;

namespace Narrensicher.Matrix;

Expand Down Expand Up @@ -90,12 +91,12 @@ private async Task HandleSyncResponseAsync(SyncResponse response)
{
if (response.AccountData != null && response.AccountData.Events != null)
{
await HandleAccountDataReceivedAsync(null, response.AccountData.Events).ConfigureAwait(false);
HandleAccountDataReceived(null, response.AccountData.Events);
}

if (response.Presence != null && response.Presence.Events != null)
{
await HandlePresenceReceivedAsync(response.Presence.Events).ConfigureAwait(false);
HandlePresenceReceived(response.Presence.Events);
}

if (response.Rooms != null)
Expand All @@ -112,16 +113,16 @@ private async Task HandleSyncResponseAsync(SyncResponse response)
}

if(pair.Value.Summary != null)
await HandleRoomSummaryReceivedAsync(roomId, pair.Value.Summary).ConfigureAwait(false);
HandleRoomSummaryReceived(roomId, pair.Value.Summary);

if(pair.Value.AccountData != null && pair.Value.AccountData.Events != null)
await HandleAccountDataReceivedAsync(roomId, pair.Value.AccountData.Events).ConfigureAwait(false);
HandleAccountDataReceived(roomId, pair.Value.AccountData.Events);

if(pair.Value.Ephemeral != null && pair.Value.Ephemeral.Events != null)
await HandleEphemeralReceivedAsync(roomId, pair.Value.Ephemeral.Events).ConfigureAwait(false);
HandleEphemeralReceived(roomId, pair.Value.Ephemeral.Events);

if(pair.Value.State != null && pair.Value.State.Events != null)
await HandleStateReceivedAsync(roomId, pair.Value.State.Events).ConfigureAwait(false);
HandleStateReceived(roomId, pair.Value.State.Events);

if(pair.Value.Timeline != null && pair.Value.Timeline.Events != null)
{
Expand Down Expand Up @@ -157,22 +158,21 @@ private async Task HandleSyncResponseAsync(SyncResponse response)
}
}

private ValueTask HandleRoomSummaryReceivedAsync(MatrixId roomId, RoomSummary? summary)
private void HandleRoomSummaryReceived(MatrixId roomId, RoomSummary? summary)
{
ArgumentNullException.ThrowIfNull(roomId, nameof(roomId));
ArgumentNullException.ThrowIfNull(summary, nameof(summary));

var users = summary?.Heroes?.Select(s => UserId.TryParse(s, out MatrixId? userId) ? userId : null)
?.Where(id => id != null)?.Select(id => id!)
?.Select(id => GetOrAddUser(id))?.ToList();
?.Select(GetOrAddUser)?.ToList();

var room = _rooms.GetOrAdd(roomId.Full,
(_) => new Room(roomId));
var room = GetOrAddRoom(roomId);

if(users == null || users.Count == 0)
{
Logger.LogWarning("Received room summary for room {RoomId} with no users.", roomId.Full);
return ValueTask.CompletedTask;
return;
}

lock(room)
Expand All @@ -185,25 +185,129 @@ private ValueTask HandleRoomSummaryReceivedAsync(MatrixId roomId, RoomSummary? s
}
}
}

return ValueTask.CompletedTask;
}

private async Task HandlePresenceReceivedAsync(List<MatrixEvent> events)
private void HandlePresenceReceived(List<MatrixEvent> events)
{
throw new NotImplementedException();
ArgumentNullException.ThrowIfNull(events, nameof(events));
foreach(var ev in events)
{
if(ev.Type != "m.presence")
{
Logger.LogWarning("Received event of type {Type} in presence events.", ev.Type);
continue;
}

if(ev.Content == null)
{
Logger.LogWarning("Received presence event with no content.");
continue;
}

var userId = UserId.TryParse(ev.Sender, out MatrixId? id) ? id : null;
if(userId == null)
{
Logger.LogWarning("Received presence event with invalid sender ID: {Sender}", ev.Sender);
continue;
}
var user = GetOrAddUser(userId);

var parsedPresence = JsonSerializer.Deserialize<PresenceEvent>((JsonElement)ev.Content);
if(parsedPresence == null)
{
Logger.LogWarning("Received presence event with no valid content.");
continue;
}

lock(user)
{
if(parsedPresence.CurrentlyActive != null)
user.CurrentlyActive = parsedPresence.CurrentlyActive;

if(parsedPresence.AvatarUrl != null)
user.AvatarUrl = parsedPresence.AvatarUrl;

if(parsedPresence.DisplayName != null)
user.DisplayName = parsedPresence.DisplayName;

if(parsedPresence.CurrentlyActive != null)
user.CurrentlyActive = parsedPresence.CurrentlyActive;

user.Presence = parsedPresence.Presence;

if(parsedPresence.StatusMsg != null)
user.StatusMessage = parsedPresence.StatusMsg;
}
}
}


private async Task HandleEphemeralReceivedAsync(MatrixId roomId, List<MatrixEvent> events)
private void HandleEphemeralReceived(MatrixId roomId, List<MatrixEvent> events)
{
throw new NotImplementedException();
ArgumentNullException.ThrowIfNull(roomId, nameof(roomId));
ArgumentNullException.ThrowIfNull(events, nameof(events));
foreach(var e in events.Where(ev => ev.Type != "m.typing" && ev.Type != "m.receipt"))
{
// Just track these for now. We are not interested in typing and receipt events
Logger.LogWarning("Received unknown Ephemeral event type in room {RoomId}: {Type}.", roomId.Full, e.Type);
}
}


private async Task HandleStateReceivedAsync(MatrixId roomId, List<ClientEventWithoutRoomID> events)
private void HandleStateReceived(MatrixId roomId, List<ClientEventWithoutRoomID> events)
{
throw new NotImplementedException();
ArgumentNullException.ThrowIfNull(roomId, nameof(roomId));
ArgumentNullException.ThrowIfNull(events, nameof(events));
var room = GetOrAddRoom(roomId);

foreach(var e in events)
{
if(e.Content == null)
{
Logger.LogWarning("Received state event with no content in room {RoomId}. Type {Type}", roomId.Full, e.Type);
continue;
}
switch(e.Type)
{
case "m.room.member":
JsonSerializer.Deserialize<RoomMemberEvent>((JsonElement)e.Content);
// TODO
break;
case "m.room.name":
var nameEvent = JsonSerializer.Deserialize<RoomNameEvent>((JsonElement)e.Content);
if(nameEvent == null)
{
Logger.LogWarning("Received m.room.name event deserialize returned null in room {RoomId}.", roomId.Full);
break;
}

lock(room)
{
room.DisplayName = nameEvent.Name;
}
break;
case "m.room.canonical_alias":
var parsed = JsonSerializer.Deserialize<CanonicalAliasEvent>((JsonElement)e.Content);
RoomAlias.TryParse(parsed?.Alias, out MatrixId? alias);
var altAliases = parsed?.AltAliases?.Select(a => RoomAlias.TryParse(a, out MatrixId? id) ? id : null).Where(id => id != null).Select(id => id!).ToList();
lock(room)
{
room.CanonicalAlias = alias;

Check failure on line 295 in src/Narrensicher.Matrix/MatrixTextClient.cs

View workflow job for this annotation

GitHub Actions / build

'Room' does not contain a definition for 'CanonicalAlias' and no accessible extension method 'CanonicalAlias' accepting a first argument of type 'Room' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 295 in src/Narrensicher.Matrix/MatrixTextClient.cs

View workflow job for this annotation

GitHub Actions / build

'Room' does not contain a definition for 'CanonicalAlias' and no accessible extension method 'CanonicalAlias' accepting a first argument of type 'Room' could be found (are you missing a using directive or an assembly reference?)
if(altAliases != null)
room.AltAliases = room.AltAliases?.Union(altAliases).ToList() ?? altAliases;

Check failure on line 297 in src/Narrensicher.Matrix/MatrixTextClient.cs

View workflow job for this annotation

GitHub Actions / build

'Room' does not contain a definition for 'AltAliases' and no accessible extension method 'AltAliases' accepting a first argument of type 'Room' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 297 in src/Narrensicher.Matrix/MatrixTextClient.cs

View workflow job for this annotation

GitHub Actions / build

'Room' does not contain a definition for 'AltAliases' and no accessible extension method 'AltAliases' accepting a first argument of type 'Room' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 297 in src/Narrensicher.Matrix/MatrixTextClient.cs

View workflow job for this annotation

GitHub Actions / build

'Room' does not contain a definition for 'AltAliases' and no accessible extension method 'AltAliases' accepting a first argument of type 'Room' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 297 in src/Narrensicher.Matrix/MatrixTextClient.cs

View workflow job for this annotation

GitHub Actions / build

'Room' does not contain a definition for 'AltAliases' and no accessible extension method 'AltAliases' accepting a first argument of type 'Room' could be found (are you missing a using directive or an assembly reference?)
}
break;
case "m.room.power_levels":
case "m.room.join_rules":
case "m.room.topic":
case "m.room.avatar":
// We don't care about these
break;
default:
Logger.LogWarning("Received unknown state event type in room {RoomId}: {Type}.", roomId.Full, e.Type);
break;
}
}
}

private async Task HandleTimelineReceivedAsync(MatrixId roomId, List<ClientEventWithoutRoomID> events, List<MatrixTextMessage> messages)

Check warning on line 313 in src/Narrensicher.Matrix/MatrixTextClient.cs

View workflow job for this annotation

GitHub Actions / build

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 313 in src/Narrensicher.Matrix/MatrixTextClient.cs

View workflow job for this annotation

GitHub Actions / build

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
Expand All @@ -216,14 +320,18 @@ private User GetOrAddUser(MatrixId id)
return _users.GetOrAdd(id.Full, (_) => new User(id));
}

private ValueTask HandleAccountDataReceivedAsync(MatrixId? roomId, List<MatrixEvent> accountData)
private Room GetOrAddRoom(MatrixId id)
{
return _rooms.GetOrAdd(id.Full, (_) => new Room(id));
}

private void HandleAccountDataReceived(MatrixId? roomId, List<MatrixEvent> accountData)
{
// Sadly, we don't have any account data events we're interested in
foreach(var ev in accountData)
{
Logger.LogDebug("Received account data event: {Event} in Room {Room}", ev.Type, roomId?.Full ?? "(global)");
}
return ValueTask.CompletedTask;
}
}

84 changes: 83 additions & 1 deletion src/Narrensicher.Matrix/Models/Events.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class MatrixEvent
public class ClientEventWithoutRoomID
{
[JsonPropertyName("content")]
public JsonElement? Content { get; set; }
public required JsonElement? Content { get; set; }

[JsonPropertyName("type")]
public required string Type { get; set; }
Expand All @@ -33,6 +33,9 @@ public class ClientEventWithoutRoomID
[JsonPropertyName("origin_server_ts")]
public required long OriginServerTs { get; set; }

/// <summary>
/// Only set if it's a state event.
/// </summary>
[JsonPropertyName("state_key")]
public string? StateKey { get; set; }

Expand All @@ -43,6 +46,27 @@ public class ClientEventWithoutRoomID
public Dictionary<string, JsonElement>? AdditionalProps { get; set; }
}

public class ClientEvent : ClientEventWithoutRoomID
{
[JsonPropertyName("room_id")]
public required string RoomId { get; set; }
}

public class CanonicalAliasEvent
{
[JsonPropertyName("alias")]
public string? Alias { get; set; }

[JsonPropertyName("alt_aliases")]
public List<string>? AltAliases { get; set; }
}

public class RoomNameEvent
{
[JsonPropertyName("name")]
public required string Name { get; set; }
}

public enum Presence
{
[JsonPropertyName("offline")]
Expand All @@ -53,6 +77,64 @@ public enum Presence
Unavailable
}

public class PresenceEvent
{
[JsonPropertyName("avatar_url")]
public string? AvatarUrl { get; set; }

[JsonPropertyName("currently_active")]
public bool? CurrentlyActive { get; set; }

[JsonPropertyName("displayname")]
public string? DisplayName { get; set; }

[JsonPropertyName("last_active_ago")]
public long? LastActiveAgo { get; set; } // milliseconds

[JsonPropertyName("presence")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public Presence Presence { get; set; }

[JsonPropertyName("status_msg")]
public string? StatusMsg { get; set; }
}

public enum Membership
{
[JsonPropertyName("invite")]
Invite,
[JsonPropertyName("join")]
Join,
[JsonPropertyName("knock")]
Knock,
[JsonPropertyName("leave")]
Leave,
[JsonPropertyName("ban")]
Ban
}

public class RoomMemberEvent
{
[JsonPropertyName("avatar_url")]
public string? AvatarUrl { get; set; }

[JsonPropertyName("displayname")]
public string? DisplayName { get; set; }

[JsonPropertyName("membership")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public required Membership Membership { get; set; }

[JsonPropertyName("reason")]
public string? Reason { get; set; }

/// <summary>
/// Specifies if this is a direct message chat.
/// </summary>
[JsonPropertyName("is_direct")]
public bool? IsDirect { get; set; }
}

public class UnsignedData
{
/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/Narrensicher.Matrix/Models/Room.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public Room(MatrixId roomId)

// ConcurrentDictionary is expensive, we only use it for the global things. Inside the room we use ImmutableDictionary instead as there is less data and less movement
public ImmutableDictionary<string, RoomUser> Users { get; internal set; } = ImmutableDictionary<string, RoomUser>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase);
public string? DisplayName { get; internal set; }
}

public class RoomUser
Expand Down
5 changes: 5 additions & 0 deletions src/Narrensicher.Matrix/Models/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ public User(MatrixId userId)
}

public MatrixId UserId { get; }
public bool? CurrentlyActive { get; internal set; }
public string? AvatarUrl { get; internal set; }
public string? DisplayName { get; internal set; }
public Presence? Presence { get; internal set; } // Initially null if we have not received presence information
public string? StatusMessage { get; internal set; }
}

0 comments on commit b4653e9

Please sign in to comment.