Skip to content

Commit

Permalink
Guild member events
Browse files Browse the repository at this point in the history
  • Loading branch information
gehongyan committed Jan 5, 2025
1 parent 7619b15 commit 99ed4ed
Show file tree
Hide file tree
Showing 10 changed files with 405 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;

namespace QQBot.API.Gateway;

internal class AudioOrLiveChannelMemberEvent
{
[JsonPropertyName("guild_id")]
public required ulong GuildId { get; init; }

[JsonPropertyName("channel_id")]
public required ulong ChannelId { get; init; }

[JsonPropertyName("channel_type")]
public AudioOrLiveChannelType ChannelType { get; init; }

[JsonPropertyName("user_id")]
public required ulong UserId { get; init; }
}
7 changes: 7 additions & 0 deletions src/QQBot.Net.WebSocket/API/Gateway/AudioOrLiveChannelType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace QQBot.API.Gateway;

internal enum AudioOrLiveChannelType
{
Audio = 2,
Live = 5
}
9 changes: 9 additions & 0 deletions src/QQBot.Net.WebSocket/API/Gateway/GuildMemberEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;

namespace QQBot.API.Gateway;

internal class GuildMemberEvent : MemberWithGuildId
{
[JsonPropertyName("op_user_id")]
public required ulong OperatorUserId { get; init; }
}
10 changes: 9 additions & 1 deletion src/QQBot.Net.WebSocket/Entities/Guilds/SocketGuild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class SocketGuild : SocketEntity<ulong>, IGuild, IUpdateable
public bool IsOwner { get; private set; }

/// <inheritdoc />
public int MemberCount { get; private set; }
public int MemberCount { get; internal set; }

/// <inheritdoc />
public int MaxMembers { get; private set; }
Expand Down Expand Up @@ -256,6 +256,14 @@ internal SocketGuildMember AddOrUpdateUser(API.User userModel, API.Member? membe
return member;
}

internal SocketGuildMember? RemoveUser(ulong id)
{
if (!_members.TryRemove(id, out SocketGuildMember? member))
return null;
member.GlobalUser.RemoveRef(Client);
return member;
}

/// <summary>
/// 获取此频道内的用户。
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/QQBot.Net.WebSocket/Entities/Users/SocketGlobalUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,6 @@ internal void RemoveRef(QQBotSocketClient client)
}

private string DebuggerDisplay => $"Unknown ({Id} Global)";

internal SocketGlobalUser Clone() => (SocketGlobalUser)MemberwiseClone();
}
7 changes: 7 additions & 0 deletions src/QQBot.Net.WebSocket/Entities/Users/SocketGuildMember.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ public Task RemoveRolesAsync(IEnumerable<IRole> roles, RequestOptions? options =
private string DebuggerDisplay =>
$"{Nickname ?? Username} ({(Nickname is not null ? $"{Username}, " : string.Empty)}{Id}{(IsBot ?? false ? ", Bot" : "")}, GuildMember)";

internal SocketGuildMember Clone()
{
SocketGuildMember clone = (SocketGuildMember)MemberwiseClone();
clone._globalUser = GlobalUser.Clone();
return clone;
}

#region IGuild

/// <inheritdoc />
Expand Down
6 changes: 4 additions & 2 deletions src/QQBot.Net.WebSocket/Entities/Users/SocketGuildUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ namespace QQBot.WebSocket;
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class SocketGuildUser : SocketUser, IGuildUser
{
internal SocketGlobalUser _globalUser;

/// <inheritdoc />
internal override SocketGlobalUser GlobalUser { get; }
internal override SocketGlobalUser GlobalUser => _globalUser;

/// <inheritdoc />
public override string? Avatar
Expand Down Expand Up @@ -38,7 +40,7 @@ public override string? Avatar
internal SocketGuildUser(QQBotSocketClient client, SocketGlobalUser globalUser)
: base(client, globalUser.Id)
{
GlobalUser = globalUser;
_globalUser = globalUser;
Username = string.Empty;
}

Expand Down
129 changes: 128 additions & 1 deletion src/QQBot.Net.WebSocket/QQBotSocketClient.Events.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ public event Func<SocketGuildChannel, Cacheable<SocketGuildMember, ulong>, Task>
/// <item> <see cref="QQBot.WebSocket.SocketGuildChannel"/> 参数是更新前的子频道。 </item>
/// <item> <see cref="QQBot.WebSocket.SocketGuildChannel"/> 参数是更新后的子频道。 </item>
/// <item>
/// <see cref="QQBot.Cacheable{TEntity,TId}"/> 参数是删除此子频道的用户。如果缓存中存在此用户实体,那么该结构内包含该
/// <see cref="QQBot.Cacheable{TEntity,TId}"/> 参数是更新此子频道的用户。如果缓存中存在此用户实体,那么该结构内包含该
/// <see cref="QQBot.WebSocket.SocketGuildMember"/> 频道用户;否则,包含 <see cref="System.UInt64"/> 用户 ID,以供按需下载实体。
/// </item>
/// </list>
Expand All @@ -166,4 +166,131 @@ public event Func<SocketGuildChannel, SocketGuildChannel, Cacheable<SocketGuildM
internal readonly AsyncEvent<Func<SocketGuildChannel, SocketGuildChannel, Cacheable<SocketGuildMember, ulong>, Task>> _channelUpdatedEvent = new();

#endregion

#region Guild Members

/// <summary>
/// 当用户加入频道时引发。
/// </summary>
/// <remarks>
/// 事件参数:
/// <list type="number">
/// <item> <see cref="QQBot.WebSocket.SocketGuildMember"/> 参数是加入频道的频道用户。 </item>
/// <item>
/// <see cref="QQBot.Cacheable{TEntity,TId}"/> 参数是进行此操作的子频道的用户。如果缓存中存在此用户实体,那么该结构内包含该
/// <see cref="QQBot.WebSocket.SocketGuildMember"/> 频道用户;否则,包含 <see cref="System.UInt64"/> 用户 ID,以供按需下载实体。
/// </item>
/// </list>
/// </remarks>
public event Func<SocketGuildMember, Cacheable<SocketGuildMember, ulong>, Task> UserJoined
{
add => _userJoinedEvent.Add(value);
remove => _userJoinedEvent.Remove(value);
}

internal readonly AsyncEvent<Func<SocketGuildMember, Cacheable<SocketGuildMember, ulong>, Task>> _userJoinedEvent = new();

/// <summary>
/// 当用户离开频道时引发。
/// </summary>
/// <remarks>
/// <note type="warning">
/// 有消息称,那么此事件不会在其成员数量超过 2000 人的频道内被触发。
/// </note>
/// <br />
/// 事件参数:
/// <list type="number">
/// <item> <see cref="QQBot.WebSocket.SocketGuild"/> 参数是用户离开的频道。 </item>
/// <item> <see cref="QQBot.WebSocket.SocketGuildUser"/> 参数是离开频道的频道用户。 </item>
/// <item>
/// <see cref="QQBot.Cacheable{TEntity,TId}"/> 参数是进行此操作的子频道的用户。如果缓存中存在此用户实体,那么该结构内包含该
/// <see cref="QQBot.WebSocket.SocketGuildMember"/> 频道用户;否则,包含 <see cref="System.UInt64"/> 用户 ID,以供按需下载实体。
/// </item>
/// </list>
/// </remarks>
public event Func<SocketGuild, SocketGuildUser, Cacheable<SocketGuildMember, ulong>, Task> UserLeft
{
add => _userLeftEvent.Add(value);
remove => _userLeftEvent.Remove(value);
}

internal readonly AsyncEvent<Func<SocketGuild, SocketGuildUser, Cacheable<SocketGuildMember, ulong>, Task>> _userLeftEvent = new();

/// <summary>
/// 当频道用户信息被更新时引发。
/// </summary>
/// <remarks>
/// 事件参数:
/// <list type="number">
/// <item>
/// <see cref="QQBot.Cacheable{TEntity,TId}"/> 参数是可缓存用户被更新前的状态。如果缓存中存在此用户实体,那么该结构内包含该
/// <see cref="QQBot.WebSocket.SocketUser"/> 用户被更新前的状态;否则,包含 <see cref="System.UInt64"/> 用户 ID。
/// <br />
/// <note type="important">
/// 用户被更新前的状态无法通过 <see cref="QQBot.Cacheable{TEntity,TId}.DownloadAsync"/> 方法下载。
/// </note>
/// </item>
/// <item> <see cref="QQBot.WebSocket.SocketGuildUser"/> 参数是更新后的频道用户。 </item>
/// <item>
/// <see cref="QQBot.Cacheable{TEntity,TId}"/> 参数是进行此操作的子频道的用户。如果缓存中存在此用户实体,那么该结构内包含该
/// <see cref="QQBot.WebSocket.SocketGuildMember"/> 频道用户;否则,包含 <see cref="System.UInt64"/> 用户 ID,以供按需下载实体。
/// </item>
/// </list>
/// </remarks>
public event Func<Cacheable<SocketGuildMember, ulong>, SocketGuildMember, Cacheable<SocketGuildMember, ulong>, Task> GuildMemberUpdated
{
add => _guildMemberUpdatedEvent.Add(value);
remove => _guildMemberUpdatedEvent.Remove(value);
}

internal readonly AsyncEvent<Func<Cacheable<SocketGuildMember, ulong>, SocketGuildMember, Cacheable<SocketGuildMember, ulong>, Task>> _guildMemberUpdatedEvent = new();

#endregion

#region Voices

/// <summary>
/// 当频道用户连接到语音子频道或直播子频道时引发。
/// </summary>
/// <remarks>
/// 事件参数:
/// <list type="number">
/// <item>
/// <see cref="QQBot.Cacheable{TEntity,TId}"/> 参数是连接到语音子频道或直播子频道的可缓存频道用户。如果缓存中存在此频道用户实体,那么该结构内包含该
/// <see cref="QQBot.WebSocket.SocketGuildMember"/> 频道用户;否则,包含 <see cref="System.UInt64"/> 用户 ID,以供按需下载实体。
/// </item>
/// <item> <see cref="QQBot.WebSocket.SocketGuildChannel"/> 参数是用户连接到的语音子频道或直播子频道。 </item>
/// </list>
/// </remarks>
public event Func<Cacheable<SocketGuildMember, ulong>, SocketGuildChannel, Task> UserConnected
{
add => _userConnectedEvent.Add(value);
remove => _userConnectedEvent.Remove(value);
}

internal readonly AsyncEvent<Func<Cacheable<SocketGuildMember, ulong>, SocketGuildChannel, Task>> _userConnectedEvent = new();

/// <summary>
/// 当频道用户从语音子频道或直播子频道断开连接时引发。
/// </summary>
/// <remarks>
/// 事件参数:
/// <list type="number">
/// <item>
/// <see cref="QQBot.Cacheable{TEntity,TId}"/> 参数是从语音子频道或直播子频道断开连接的可缓存频道用户。如果缓存中存在此频道用户实体,那么该结构内包含该
/// <see cref="QQBot.WebSocket.SocketGuildMember"/> 频道用户;否则,包含 <see cref="System.UInt64"/> 用户 ID,以供按需下载实体。
/// </item>
/// <item> <see cref="QQBot.WebSocket.SocketGuildChannel"/> 参数是用户断开连接的语音子频道或直播子频道。 </item>
/// </list>
/// </remarks>
public event Func<Cacheable<SocketGuildMember, ulong>, SocketGuildChannel, Task> UserDisconnected
{
add => _userDisconnectedEvent.Add(value);
remove => _userDisconnectedEvent.Remove(value);
}

internal readonly AsyncEvent<Func<Cacheable<SocketGuildMember, ulong>, SocketGuildChannel, Task>> _userDisconnectedEvent = new();

#endregion

}
123 changes: 123 additions & 0 deletions src/QQBot.Net.WebSocket/QQBotSocketClient.Messages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,126 @@ private async Task HandleChannelDeletedAsync(object? payload)

#endregion

#region Guild Members

private async Task HandleGuildMemberAddedAsync(object? payload)
{
if (DeserializePayload<GuildMemberEvent>(payload) is not { } data) return;
if (GetGuild(data.GuildId) is not { } guild)
{
await UnknownGuildAsync(nameof(UserJoined), data.GuildId, payload).ConfigureAwait(false);
return;
}
if (data.User is null)
{
await UnknownUserAsync(nameof(UserJoined), payload).ConfigureAwait(false);
return;
}

SocketGuildMember user = guild.AddOrUpdateUser(data.User, data);
guild.MemberCount++;
SocketGuildMember? operatorUser = guild.GetUser(data.OperatorUserId);
Cacheable<SocketGuildMember, ulong> cacheableOperatorUser = GetCacheableSocketGuildMember(operatorUser, data.OperatorUserId, guild);
await TimedInvokeAsync(_userJoinedEvent, nameof(UserJoined), user, cacheableOperatorUser).ConfigureAwait(false);
}

private async Task HandleGuildMemberRemovedAsync(object? payload)
{
if (DeserializePayload<GuildMemberEvent>(payload) is not { } data) return;
if (GetGuild(data.GuildId) is not { } guild)
{
await UnknownGuildAsync(nameof(UserLeft), data.GuildId, payload).ConfigureAwait(false);
return;
}
if (data.User is null)
{
await UnknownUserAsync(nameof(UserJoined), payload).ConfigureAwait(false);
return;
}
SocketGuildUser user = guild.RemoveUser(data.User.Id)
?? State.GetGuildUser(data.User.Id)
?? State.GetOrAddGuildUser(data.User.Id, _ => SocketGuildUser.Create(this, State, data.User));
guild.MemberCount--;

SocketGuildMember? operatorUser = guild.GetUser(data.OperatorUserId);
Cacheable<SocketGuildMember, ulong> cacheableOperatorUser = GetCacheableSocketGuildMember(operatorUser, data.OperatorUserId, guild);
await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user, cacheableOperatorUser).ConfigureAwait(false);
}

private async Task HandleGuildMemberUpdatedAsync(object? payload)
{
if (DeserializePayload<GuildMemberEvent>(payload) is not { } data) return;
if (GetGuild(data.GuildId) is not { } guild)
{
await UnknownGuildAsync(nameof(GuildMemberUpdated), data.GuildId, payload).ConfigureAwait(false);
return;
}
if (data.User is null)
{
await UnknownUserAsync(nameof(GuildMemberUpdated), payload).ConfigureAwait(false);
return;
}

SocketGuildMember? operatorUser = guild.GetUser(data.OperatorUserId);
Cacheable<SocketGuildMember, ulong> cacheableOperatorUser = GetCacheableSocketGuildMember(operatorUser, data.OperatorUserId, guild);

if (guild.GetUser(data.User.Id) is { } user)
{
SocketGuildMember before = user.Clone();
user.Update(State, data.User, data);
Cacheable<SocketGuildMember, ulong> cacheableBefore = new(before, user.Id, true, () => Task.FromResult<SocketGuildMember?>(null));
await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user, cacheableOperatorUser).ConfigureAwait(false);
}
else
{
user = guild.AddOrUpdateUser(data.User, data);
Cacheable<SocketGuildMember, ulong> cacheableBefore = new(null, user.Id, false, () => Task.FromResult<SocketGuildMember?>(null));
await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user, cacheableOperatorUser).ConfigureAwait(false);
}
}

#endregion

#region Voices

private async Task HandleAudioOrLiveChannelMemberEnterAsync(object? payload)
{
if (DeserializePayload<AudioOrLiveChannelMemberEvent>(payload) is not { } data) return;
if (GetGuild(data.GuildId) is not { } guild)
{
await UnknownGuildAsync(nameof(UserConnected), data.GuildId, payload).ConfigureAwait(false);
return;
}
if (guild.GetChannel(data.ChannelId) is not { } channel)
{
await UnknownChannelAsync(nameof(UserConnected), data.ChannelId, payload).ConfigureAwait(false);
return;
}
SocketGuildMember? user = guild.GetUser(data.UserId);
Cacheable<SocketGuildMember, ulong> cacheableUser = GetCacheableSocketGuildMember(user, data.UserId, guild);
await TimedInvokeAsync(_userConnectedEvent, nameof(UserConnected), cacheableUser, channel).ConfigureAwait(false);
}

private async Task HandleAudioOrLiveChannelMemberExitAsync(object? payload)
{
if (DeserializePayload<AudioOrLiveChannelMemberEvent>(payload) is not { } data) return;
if (GetGuild(data.GuildId) is not { } guild)
{
await UnknownGuildAsync(nameof(UserDisconnected), data.GuildId, payload).ConfigureAwait(false);
return;
}
if (guild.GetChannel(data.ChannelId) is not { } channel)
{
await UnknownChannelAsync(nameof(UserDisconnected), data.ChannelId, payload).ConfigureAwait(false);
return;
}
SocketGuildMember? user = guild.GetUser(data.UserId);
Cacheable<SocketGuildMember, ulong> cacheableUser = GetCacheableSocketGuildMember(user, data.UserId, guild);
await TimedInvokeAsync(_userDisconnectedEvent, nameof(UserDisconnected), cacheableUser, channel).ConfigureAwait(false);
}

#endregion

#region Raising Events

private async Task GuildAvailableAsync(SocketGuild guild)
Expand All @@ -394,6 +514,9 @@ private async Task UnknownGuildAsync(string dispatch, ulong guildId, object? pay
private async Task UnknownChannelAsync(string dispatch, ulong channelId, object? payload) =>
await LogGatewayErrorAsync(dispatch, $"Unknown ChannelId: {channelId}.", payload).ConfigureAwait(false);

private async Task UnknownUserAsync(string dispatch, object? payload) =>
await LogGatewayErrorAsync(dispatch, $"No User in payload.", payload).ConfigureAwait(false);

private async Task LogGatewayErrorAsync(string dispatch, string message, object? payload) =>
await _gatewayLogger.WarningAsync($"{message} Event: {dispatch}. Payload: {SerializePayload(payload)}").ConfigureAwait(false);

Expand Down
Loading

0 comments on commit 99ed4ed

Please sign in to comment.