diff --git a/src/QQBot.Net.Core/Entities/Threads/IPost.cs b/src/QQBot.Net.Core/Entities/Threads/IPost.cs new file mode 100644 index 0000000..515c094 --- /dev/null +++ b/src/QQBot.Net.Core/Entities/Threads/IPost.cs @@ -0,0 +1,42 @@ +namespace QQBot; + +/// +/// 表示一个通用的论坛主题评论。 +/// +public interface IPost : IEntity +{ + /// + /// 获取此主题评论所属的频道。 + /// + IGuild Guild { get; } + + /// + /// 获取此主题评论所属的论坛子频道。 + /// + IForumChannel Channel { get; } + + /// + /// 获取此主题评论的作者用户的 ID。 + /// + ulong AuthorId { get; } + + /// + /// 获取此主题评论所属的主题的 ID。 + /// + string ThreadId { get; } + + /// + /// 获取此主题的原始内容。 + /// + string RawContent { get; } + + /// + /// 获取此主题评论的富文本内容。 + /// + RichText Content { get; } + + /// + /// 获取此主题评论的创建时间。 + /// + DateTimeOffset CreatedAt { get; } +} diff --git a/src/QQBot.Net.Core/Entities/Threads/IReply.cs b/src/QQBot.Net.Core/Entities/Threads/IReply.cs new file mode 100644 index 0000000..4299d85 --- /dev/null +++ b/src/QQBot.Net.Core/Entities/Threads/IReply.cs @@ -0,0 +1,47 @@ +namespace QQBot; + +/// +/// 表示一个通用的论坛主题评论回复。 +/// +public interface IReply : IEntity +{ + /// + /// 获取此主题评论回复所属的频道。 + /// + IGuild Guild { get; } + + /// + /// 获取此主题评论回复所属的论坛子频道。 + /// + IForumChannel Channel { get; } + + /// + /// 获取此主题评论回复的作者用户的 ID。 + /// + ulong AuthorId { get; } + + /// + /// 获取此主题评论回复所属的主题的 ID。 + /// + string ThreadId { get; } + + /// + /// 获取此主题评论回复所属的主题评论的 ID。 + /// + string PostId { get; } + + /// + /// 获取此主题的原始内容。 + /// + string RawContent { get; } + + /// + /// 获取此主题评论回复的富文本内容。 + /// + RichText Content { get; } + + /// + /// 获取此主题评论回复的创建时间。 + /// + DateTimeOffset CreatedAt { get; } +} diff --git a/src/QQBot.Net.Core/Entities/Threads/IThread.cs b/src/QQBot.Net.Core/Entities/Threads/IThread.cs index 791d194..dbbffae 100644 --- a/src/QQBot.Net.Core/Entities/Threads/IThread.cs +++ b/src/QQBot.Net.Core/Entities/Threads/IThread.cs @@ -11,7 +11,7 @@ public interface IThread : IEntity, IUpdateable, IDeletable IGuild Guild { get; } /// - /// 获取次主题所属的论坛子频道。 + /// 获取此主题所属的论坛子频道。 /// IForumChannel Channel { get; } diff --git a/src/QQBot.Net.Rest/API/Common/PostInfo.cs b/src/QQBot.Net.Rest/API/Common/PostInfo.cs new file mode 100644 index 0000000..e3c0323 --- /dev/null +++ b/src/QQBot.Net.Rest/API/Common/PostInfo.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace QQBot.API; + +internal class PostInfo +{ + [JsonPropertyName("thread_id")] + public required string ThreadId { get; init; } + + [JsonPropertyName("post_id")] + public required string PostId { get; init; } + + [JsonPropertyName("content")] + public required string Content { get; init; } + + [JsonPropertyName("date_time")] + public required DateTimeOffset DateTime { get; init; } +} diff --git a/src/QQBot.Net.Rest/API/Common/ReplyInfo.cs b/src/QQBot.Net.Rest/API/Common/ReplyInfo.cs new file mode 100644 index 0000000..812bf5b --- /dev/null +++ b/src/QQBot.Net.Rest/API/Common/ReplyInfo.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace QQBot.API; + +internal class ReplyInfo +{ + [JsonPropertyName("thread_id")] + public required string ThreadId { get; init; } + + [JsonPropertyName("post_id")] + public required string PostId { get; init; } + + [JsonPropertyName("reply_id")] + public required string ReplyId { get; init; } + + [JsonPropertyName("content")] + public required string Content { get; init; } + + [JsonPropertyName("date_time")] + public required DateTimeOffset DateTime { get; init; } +} diff --git a/src/QQBot.Net.WebSocket/API/Gateway/AudioOrLiveChannelMemberEvent.cs b/src/QQBot.Net.WebSocket/API/Gateway/AudioOrLiveChannelMemberEvent.cs index 550ebc5..e544022 100644 --- a/src/QQBot.Net.WebSocket/API/Gateway/AudioOrLiveChannelMemberEvent.cs +++ b/src/QQBot.Net.WebSocket/API/Gateway/AudioOrLiveChannelMemberEvent.cs @@ -15,4 +15,4 @@ internal class AudioOrLiveChannelMemberEvent [JsonPropertyName("user_id")] public required ulong UserId { get; init; } -} \ No newline at end of file +} diff --git a/src/QQBot.Net.WebSocket/API/Gateway/AuditType.cs b/src/QQBot.Net.WebSocket/API/Gateway/AuditType.cs new file mode 100644 index 0000000..dbbefd1 --- /dev/null +++ b/src/QQBot.Net.WebSocket/API/Gateway/AuditType.cs @@ -0,0 +1,8 @@ +namespace QQBot.API.Gateway; + +internal enum AuditType +{ + Thread = 1, + Post = 2, + Reply = 3 +} diff --git a/src/QQBot.Net.WebSocket/API/Gateway/ForumPostEvent.cs b/src/QQBot.Net.WebSocket/API/Gateway/ForumPostEvent.cs new file mode 100644 index 0000000..2177732 --- /dev/null +++ b/src/QQBot.Net.WebSocket/API/Gateway/ForumPostEvent.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace QQBot.API.Gateway; + +internal class ForumPostEvent +{ + [JsonPropertyName("guild_id")] + public required ulong GuildId { get; init; } + + [JsonPropertyName("channel_id")] + public required ulong ChannelId { get; init; } + + [JsonPropertyName("author_id")] + public required ulong AuthorId { get; init; } + + [JsonPropertyName("post_info")] + public required PostInfo PostInfo { get; set; } +} diff --git a/src/QQBot.Net.WebSocket/API/Gateway/ForumPublishAuditResultEvent.cs b/src/QQBot.Net.WebSocket/API/Gateway/ForumPublishAuditResultEvent.cs new file mode 100644 index 0000000..648ffdf --- /dev/null +++ b/src/QQBot.Net.WebSocket/API/Gateway/ForumPublishAuditResultEvent.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; +using QQBot.Net.Converters; + +namespace QQBot.API.Gateway; + +internal class ForumPublishAuditResultEvent +{ + [JsonPropertyName("guild_id")] + public required ulong GuildId { get; init; } + + [JsonPropertyName("channel_id")] + public required ulong ChannelId { get; init; } + + [JsonPropertyName("author_id")] + public required ulong AuthorId { get; init; } + + [JsonPropertyName("thread_id")] + public required string? ThreadId { get; init; } + + [JsonPropertyName("post_id")] + public required string? PostId { get; init; } + + [JsonPropertyName("reply_id")] + public required string? ReplyId { get; init; } + + [JsonPropertyName("type")] + public AuditType AuditType { get; init; } + + [JsonPropertyName("result")] + [NumberBooleanConverter] + public bool Failed { get; init; } + + [JsonPropertyName("err_msg")] + public string? ErrorMessage { get; init; } +} diff --git a/src/QQBot.Net.WebSocket/API/Gateway/ForumReplyEvent.cs b/src/QQBot.Net.WebSocket/API/Gateway/ForumReplyEvent.cs new file mode 100644 index 0000000..32a7baf --- /dev/null +++ b/src/QQBot.Net.WebSocket/API/Gateway/ForumReplyEvent.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace QQBot.API.Gateway; + +internal class ForumReplyEvent +{ + [JsonPropertyName("guild_id")] + public required ulong GuildId { get; init; } + + [JsonPropertyName("channel_id")] + public required ulong ChannelId { get; init; } + + [JsonPropertyName("author_id")] + public required ulong AuthorId { get; init; } + + [JsonPropertyName("reply_info")] + public required ReplyInfo ReplyInfo { get; set; } +} diff --git a/src/QQBot.Net.WebSocket/API/Gateway/ForumThreadEvent.cs b/src/QQBot.Net.WebSocket/API/Gateway/ForumThreadEvent.cs new file mode 100644 index 0000000..3439184 --- /dev/null +++ b/src/QQBot.Net.WebSocket/API/Gateway/ForumThreadEvent.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace QQBot.API.Gateway; + +internal class ForumThreadEvent +{ + [JsonPropertyName("guild_id")] + public required ulong GuildId { get; init; } + + [JsonPropertyName("channel_id")] + public required ulong ChannelId { get; init; } + + [JsonPropertyName("author_id")] + public required ulong AuthorId { get; init; } + + [JsonPropertyName("thread_info")] + public required ThreadInfo ThreadInfo { get; set; } +} diff --git a/src/QQBot.Net.WebSocket/API/Gateway/GroupBotEvent.cs b/src/QQBot.Net.WebSocket/API/Gateway/GroupBotEvent.cs new file mode 100644 index 0000000..0a85f97 --- /dev/null +++ b/src/QQBot.Net.WebSocket/API/Gateway/GroupBotEvent.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace QQBot.API.Gateway; + +internal class GroupBotEvent +{ + [JsonPropertyName("group_openid")] + public required Guid GroupOpenid { get; init; } + + [JsonPropertyName("op_member_openid")] + public required string OpMemberOpenId { get; init; } + + [JsonPropertyName("timestamp")] + public required int Timestamp { get; init; } +} diff --git a/src/QQBot.Net.WebSocket/API/Gateway/UserBotEvent.cs b/src/QQBot.Net.WebSocket/API/Gateway/UserBotEvent.cs new file mode 100644 index 0000000..c7b38be --- /dev/null +++ b/src/QQBot.Net.WebSocket/API/Gateway/UserBotEvent.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace QQBot.API.Gateway; + +internal class UserBotEvent +{ + [JsonPropertyName("openid")] + public required Guid OpenId { get; init; } + + [JsonPropertyName("timestamp")] + public required int Timestamp { get; init; } +} diff --git a/src/QQBot.Net.WebSocket/Entities/Channels/SocketUserChannel.cs b/src/QQBot.Net.WebSocket/Entities/Channels/SocketUserChannel.cs index c3be592..2dfb7c0 100644 --- a/src/QQBot.Net.WebSocket/Entities/Channels/SocketUserChannel.cs +++ b/src/QQBot.Net.WebSocket/Entities/Channels/SocketUserChannel.cs @@ -13,20 +13,20 @@ public class SocketUserChannel : SocketChannel, IUserChannel, ISocketPrivateChan public new Guid Id { get; } /// - public SocketUser Recipient { get; } + public SocketUser? Recipient { get; } /// public IReadOnlyCollection CachedMessages => []; /// - internal SocketUserChannel(QQBotSocketClient client, Guid id, SocketUser recipient) + internal SocketUserChannel(QQBotSocketClient client, Guid id, SocketUser? recipient) : base(client, id.ToIdString()) { Id = id; Recipient = recipient; } - internal static SocketUserChannel Create(QQBotSocketClient client, ClientState state, Guid id, SocketUser recipient) + internal static SocketUserChannel Create(QQBotSocketClient client, ClientState state, Guid id, SocketUser? recipient) { SocketUserChannel channel = new(client, id, recipient); return channel; @@ -51,7 +51,7 @@ public Task SendMessageAsync(string? content = null, IMarkdown? ma /// protected override SocketUser? GetUserInternal(string id) { - if (id == Recipient.Id) return Recipient; + if (id == Recipient?.Id) return Recipient; return id == Client.CurrentUser?.Id.ToIdString() ? Client.CurrentUser : null; } @@ -60,14 +60,14 @@ public Task SendMessageAsync(string? content = null, IMarkdown? ma #region ISocketPrivateChannel /// - IReadOnlyCollection ISocketPrivateChannel.Recipients => [Recipient]; + IReadOnlyCollection ISocketPrivateChannel.Recipients => Recipient is not null ? [Recipient] : []; #endregion #region IPrivateChannel /// - IReadOnlyCollection IPrivateChannel.Recipients => [Recipient]; + IReadOnlyCollection IPrivateChannel.Recipients => Recipient is not null ? [Recipient] : []; #endregion diff --git a/src/QQBot.Net.WebSocket/Entities/Threads/SocketPost.cs b/src/QQBot.Net.WebSocket/Entities/Threads/SocketPost.cs new file mode 100644 index 0000000..371f566 --- /dev/null +++ b/src/QQBot.Net.WebSocket/Entities/Threads/SocketPost.cs @@ -0,0 +1,72 @@ +using System.Diagnostics; +using QQBot.API; +using QQBot.Rest; + +namespace QQBot.WebSocket; + +/// +/// 表示一个基于网关的论坛主题评论。 +/// +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public class SocketPost : SocketEntity, IPost +{ + /// + public SocketGuild Guild { get; } + + /// + public SocketForumChannel Channel { get; } + + /// + public ulong AuthorId { get; } + + /// + public string ThreadId { get; } + + /// + public string RawContent { get; private set; } + + /// + public RichText Content { get; private set; } + + /// + public DateTimeOffset CreatedAt { get; private set; } + + /// + private SocketPost(QQBotSocketClient client, string id, string threadId, SocketForumChannel channel, ulong authorId) + : base(client, id) + { + Guild = channel.Guild; + Channel = channel; + AuthorId = authorId; + ThreadId = threadId; + RawContent = string.Empty; + Content = RichText.Empty; + CreatedAt = DateTimeOffset.Now; + } + + internal static SocketPost Create(QQBotSocketClient client, + SocketForumChannel channel, ulong authorId, API.PostInfo model) + { + SocketPost post = new(client, model.PostId, model.ThreadId, channel, authorId); + post.Update(model); + return post; + } + + private void Update(API.PostInfo model) + { + RawContent = model.Content; + Content = ForumHelper.ParseContent(model.Content); + CreatedAt = model.DateTime; + } + + private string DebuggerDisplay => $"{Content} ({Id}, {Content.DebuggerDisplay})"; + + /// + public override string ToString() => RawContent; + + /// + IGuild IPost.Guild => Guild; + + /// + IForumChannel IPost.Channel => Channel; +} diff --git a/src/QQBot.Net.WebSocket/Entities/Threads/SocketReply.cs b/src/QQBot.Net.WebSocket/Entities/Threads/SocketReply.cs new file mode 100644 index 0000000..804ad52 --- /dev/null +++ b/src/QQBot.Net.WebSocket/Entities/Threads/SocketReply.cs @@ -0,0 +1,106 @@ +using System.Diagnostics; +using QQBot.Rest; + +namespace QQBot.WebSocket; + +/// +/// 表示一个基于网关的论坛主题评论回复。 +/// +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public class SocketReply : SocketEntity, IReply +{ + /// + public SocketGuild Guild { get; } + + /// + public SocketForumChannel Channel { get; } + + /// + public ulong AuthorId { get; } + + /// + public string ThreadId { get; } + + /// + public string PostId { get; } + + /// + public string RawContent { get; private set; } + + /// + public RichText Content { get; private set; } + + /// + public DateTimeOffset CreatedAt { get; private set; } + + /// + private SocketReply(QQBotSocketClient client, string id, string threadId, string postId, SocketForumChannel channel, ulong authorId) + : base(client, id) + { + Guild = channel.Guild; + Channel = channel; + AuthorId = authorId; + ThreadId = threadId; + PostId = postId; + RawContent = string.Empty; + Content = RichText.Empty; + CreatedAt = DateTimeOffset.Now; + } + + internal static SocketReply Create(QQBotSocketClient client, + SocketForumChannel channel, ulong authorId, API.ReplyInfo model) + { + SocketReply Reply = new(client, model.ReplyId, model.ThreadId, model.PostId, channel, authorId); + Reply.Update(model); + return Reply; + } + + private void Update(API.ReplyInfo model) + { + RawContent = model.Content; + Content = ForumHelper.ParseContent(model.Content); + CreatedAt = model.DateTime; + } + + private string DebuggerDisplay => $"{Content} ({Id}, {Content.DebuggerDisplay})"; + + /// + public override string ToString() => RawContent; + + /// + IGuild IReply.Guild => Guild; + + /// + IForumChannel IReply.Channel => Channel; +} + +// internal class ForumPublishAuditResultEvent +// { +// [JsonPropertyName("guild_id")] +// public required ulong GuildId { get; init; } +// +// [JsonPropertyName("channel_id")] +// public required ulong ChannelId { get; init; } +// +// [JsonPropertyName("author_id")] +// public required ulong AuthorId { get; init; } +// +// [JsonPropertyName("thread_id")] +// public required string? ThreadId { get; init; } +// +// [JsonPropertyName("post_id")] +// public required string? PostId { get; init; } +// +// [JsonPropertyName("reply_id")] +// public required string? ReplyId { get; init; } +// +// [JsonPropertyName("type")] +// public AuditType AuditType { get; init; } +// +// [JsonPropertyName("result")] +// [NumberBooleanConverter] +// public int Failed { get; init; } +// +// [JsonPropertyName("err_msg")] +// public string? ErrorMessage { get; init; } +// } diff --git a/src/QQBot.Net.WebSocket/Entities/Threads/SocketThread.cs b/src/QQBot.Net.WebSocket/Entities/Threads/SocketThread.cs new file mode 100644 index 0000000..96582a6 --- /dev/null +++ b/src/QQBot.Net.WebSocket/Entities/Threads/SocketThread.cs @@ -0,0 +1,83 @@ +using System.Diagnostics; +using QQBot.Rest; + +namespace QQBot.WebSocket; + +/// +/// 表示一个基于网关的论坛主题。 +/// +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public class SocketThread : SocketEntity, IThread +{ + /// + public SocketGuild Guild { get; } + + /// + public SocketForumChannel Channel { get; } + + /// + public ulong AuthorId { get; } + + /// + public string Title { get; private set; } + + /// + public string RawContent { get; private set; } + + /// + public RichText Content { get; private set; } + + /// + public DateTimeOffset CreatedAt { get; private set; } + + /// + private SocketThread(QQBotSocketClient client, string id, SocketForumChannel channel, ulong authorId) + : base(client, id) + { + Guild = channel.Guild; + Channel = channel; + AuthorId = authorId; + Title = string.Empty; + RawContent = string.Empty; + Content = RichText.Empty; + CreatedAt = DateTimeOffset.Now; + } + + internal static SocketThread Create(QQBotSocketClient client, + SocketForumChannel channel, ulong authorId, API.ThreadInfo model) + { + SocketThread thread = new(client, model.ThreadId, channel, authorId); + thread.Update(model); + return thread; + } + + internal void Update(API.ThreadInfo model) + { + Title = model.Title; + RawContent = model.Content; + Content = ForumHelper.ParseContent(model.Content); + CreatedAt = model.DateTime; + } + + /// + public async Task UpdateAsync(RequestOptions? options = null) + { + API.Thread model = await ChannelHelper.GetThreadAsync(Channel, Client, Id, options).ConfigureAwait(false); + Update(model.ThreadInfo); + } + + /// + public Task DeleteAsync(RequestOptions? options = null) => + ChannelHelper.DeleteThreadAsync(Channel, Client, Id, options); + + private string DebuggerDisplay => $"{Title} ({Id}, {Content.DebuggerDisplay})"; + + /// + public override string ToString() => RawContent; + + /// + IGuild IThread.Guild => Guild; + + /// + IForumChannel IThread.Channel => Channel; +} diff --git a/src/QQBot.Net.WebSocket/QQBot.Net.WebSocket.csproj.DotSettings b/src/QQBot.Net.WebSocket/QQBot.Net.WebSocket.csproj.DotSettings index 656aaac..9e6838e 100644 --- a/src/QQBot.Net.WebSocket/QQBot.Net.WebSocket.csproj.DotSettings +++ b/src/QQBot.Net.WebSocket/QQBot.Net.WebSocket.csproj.DotSettings @@ -4,4 +4,5 @@ True True True + True True \ No newline at end of file diff --git a/src/QQBot.Net.WebSocket/QQBotSocketClient.Events.cs b/src/QQBot.Net.WebSocket/QQBotSocketClient.Events.cs index 22c4778..f136a01 100644 --- a/src/QQBot.Net.WebSocket/QQBotSocketClient.Events.cs +++ b/src/QQBot.Net.WebSocket/QQBotSocketClient.Events.cs @@ -293,4 +293,294 @@ public event Func, SocketGuildChannel, Task> #endregion + #region Forums + + /// + /// 当论坛主题被创建时引发。 + /// + /// + /// 事件参数: + /// + /// 参数是新创建的论坛主题。 + /// + /// + public event Func ForumThreadCreated + { + add => _forumThreadCreatedEvent.Add(value); + remove => _forumThreadCreatedEvent.Remove(value); + } + + internal readonly AsyncEvent> _forumThreadCreatedEvent = new(); + + /// + /// 当论坛主题被修改时引发。 + /// + /// + /// 事件参数: + /// + /// 参数是修改后的论坛主题。 + /// + /// + public event Func ForumThreadUpdated + { + add => _forumThreadUpdatedEvent.Add(value); + remove => _forumThreadUpdatedEvent.Remove(value); + } + + internal readonly AsyncEvent> _forumThreadUpdatedEvent = new(); + + /// + /// 当论坛主题被删除时引发。 + /// + /// + /// 事件参数: + /// + /// 参数是被删除的论坛主题。 + /// + /// + public event Func ForumThreadDeleted + { + add => _forumThreadDeletedEvent.Add(value); + remove => _forumThreadDeletedEvent.Remove(value); + } + + internal readonly AsyncEvent> _forumThreadDeletedEvent = new(); + + /// + /// 当论坛主题评论被创建时引发。 + /// + /// + /// 事件参数: + /// + /// 参数是新创建的论坛主题评论。 + /// + /// + public event Func ForumPostCreated + { + add => _forumPostCreatedEvent.Add(value); + remove => _forumPostCreatedEvent.Remove(value); + } + + internal readonly AsyncEvent> _forumPostCreatedEvent = new(); + + /// + /// 当论坛主题评论被删除时引发。 + /// + /// + /// 事件参数: + /// + /// 参数是被删除的论坛主题评论。 + /// + /// + public event Func ForumPostDeleted + { + add => _forumPostDeletedEvent.Add(value); + remove => _forumPostDeletedEvent.Remove(value); + } + + internal readonly AsyncEvent> _forumPostDeletedEvent = new(); + + /// + /// 当论坛主题评论回复被创建时引发。 + /// + /// + /// 事件参数: + /// + /// 参数是新创建的论坛主题评论回复。 + /// + /// + public event Func ForumReplyCreated + { + add => _forumReplyCreatedEvent.Add(value); + remove => _forumReplyCreatedEvent.Remove(value); + } + + internal readonly AsyncEvent> _forumReplyCreatedEvent = new(); + + /// + /// 当论坛主题评论回复被删除时引发。 + /// + /// + /// 事件参数: + /// + /// 参数是被删除的论坛主题评论回复。 + /// + /// + public event Func ForumReplyDeleted + { + add => _forumReplyDeletedEvent.Add(value); + remove => _forumReplyDeletedEvent.Remove(value); + } + + internal readonly AsyncEvent> _forumReplyDeletedEvent = new(); + + #endregion + + #region Groups + + /// + /// 当群组添加当前用户时引发。 + /// + /// + /// 事件参数: + /// + /// 参数是添加当前用户的群组。 + /// + /// 参数是添加当前用户的群组的用户。如果缓存中存在此用户实体,那么该结构内包含该 + /// 群组用户;否则,包含 用户 ID。 + /// 如果网关没有提供实体的详细信息,由于目前无法通过 API 获取此用户实体,因此 + /// 总会返回 null。 + /// + /// + /// + public event Func, Task> JoinedGroup + { + add => _joinedGroupEvent.Add(value); + remove => _joinedGroupEvent.Remove(value); + } + + internal readonly AsyncEvent, Task>> _joinedGroupEvent = new(); + + /// + /// 当群组移除当前用户时引发。 + /// + /// + /// 事件参数: + /// + /// 参数是移除当前用户的群组。 + /// + /// 参数是添加当前用户的群组的用户。如果缓存中存在此用户实体,那么该结构内包含该 + /// 群组用户;否则,包含 用户 ID。 + /// 如果网关没有提供实体的详细信息,由于目前无法通过 API 获取此用户实体,因此 + /// 总会返回 null。 + /// + /// + /// + public event Func, Task> LeftGroup + { + add => _leftGroupEvent.Add(value); + remove => _leftGroupEvent.Remove(value); + } + + internal readonly AsyncEvent, Task>> _leftGroupEvent = new(); + + /// + /// 当群组接受当前用户的主动消息时引发。 + /// + /// + /// 事件参数: + /// + /// 参数是接受当前用户主动消息的群组。 + /// + /// 参数是添加当前用户的群组的用户。如果缓存中存在此用户实体,那么该结构内包含该 + /// 群组用户;否则,包含 用户 ID。 + /// 如果网关没有提供实体的详细信息,由于目前无法通过 API 获取此用户实体,因此 + /// 总会返回 null。 + /// + /// + /// + public event Func, Task> GroupActiveMessageAllowed + { + add => _groupActiveMessageAllowedEvent.Add(value); + remove => _groupActiveMessageAllowedEvent.Remove(value); + } + + internal readonly AsyncEvent, Task>> _groupActiveMessageAllowedEvent = new(); + + /// + /// 当群组接受当前用户的主动消息时引发。 + /// + /// + /// 事件参数: + /// + /// 参数是接受当前用户主动消息的群组。 + /// + /// 参数是添加当前用户的群组的用户。如果缓存中存在此用户实体,那么该结构内包含该 + /// 群组用户;否则,包含 用户 ID。 + /// 如果网关没有提供实体的详细信息,由于目前无法通过 API 获取此用户实体,因此 + /// 总会返回 null。 + /// + /// + /// + public event Func, Task> GroupActiveMessageRejected + { + add => _groupActiveMessageRejectedEvent.Add(value); + remove => _groupActiveMessageRejectedEvent.Remove(value); + } + + internal readonly AsyncEvent, Task>> _groupActiveMessageRejectedEvent = new(); + + #endregion + + #region Users + + /// + /// 当用户添加当前用户时引发。 + /// + /// + /// 事件参数: + /// + /// 参数是添加当前用户的用户频道。 + /// + /// + public event Func UserAdded + { + add => _userAddedEvent.Add(value); + remove => _userAddedEvent.Remove(value); + } + + internal readonly AsyncEvent> _userAddedEvent = new(); + + /// + /// 当用户移除当前用户时引发。 + /// + /// + /// 事件参数: + /// + /// 参数是移除当前用户的用户频道。 + /// + /// + public event Func UserRemoved + { + add => _userRemovedEvent.Add(value); + remove => _userRemovedEvent.Remove(value); + } + + internal readonly AsyncEvent> _userRemovedEvent = new(); + + /// + /// 当用户接受当前用户的主动消息时引发。 + /// + /// + /// 事件参数: + /// + /// 参数是接受当前用户主动消息的用户。 + /// + /// + public event Func UserActiveMessageAllowed + { + add => _userActiveMessageAllowedEvent.Add(value); + remove => _userActiveMessageAllowedEvent.Remove(value); + } + + internal readonly AsyncEvent> _userActiveMessageAllowedEvent = new(); + + /// + /// 当用户接受当前用户的主动消息时引发。 + /// + /// + /// 事件参数: + /// + /// 参数是接受当前用户主动消息的用户。 + /// + /// + public event Func UserActiveMessageRejected + { + add => _userActiveMessageRejectedEvent.Add(value); + remove => _userActiveMessageRejectedEvent.Remove(value); + } + + internal readonly AsyncEvent> _userActiveMessageRejectedEvent = new(); + + #endregion } diff --git a/src/QQBot.Net.WebSocket/QQBotSocketClient.Messages.cs b/src/QQBot.Net.WebSocket/QQBotSocketClient.Messages.cs index 9990a9b..d55cd7b 100644 --- a/src/QQBot.Net.WebSocket/QQBotSocketClient.Messages.cs +++ b/src/QQBot.Net.WebSocket/QQBotSocketClient.Messages.cs @@ -492,6 +492,229 @@ private async Task HandleAudioOrLiveChannelMemberExitAsync(object? payload) #endregion + #region Forums + + private async Task HandleForumThreadCreatedAsync(object? payload) + { + if (DeserializePayload(payload) is not { } data) return; + if (GetGuild(data.GuildId) is not { } guild) + { + await UnknownGuildAsync(nameof(ForumThreadCreated), data.GuildId, payload).ConfigureAwait(false); + return; + } + if (guild.GetForumChannel(data.ChannelId) is not { } channel) + { + await UnknownChannelAsync(nameof(ForumThreadCreated), data.ChannelId, payload).ConfigureAwait(false); + return; + } + SocketThread thread = SocketThread.Create(this, channel, data.AuthorId, data.ThreadInfo); + await TimedInvokeAsync(_forumThreadCreatedEvent, nameof(ForumThreadCreated), thread).ConfigureAwait(false); + } + + private async Task HandleForumThreadUpdatedAsync(object? payload) + { + if (DeserializePayload(payload) is not { } data) return; + if (GetGuild(data.GuildId) is not { } guild) + { + await UnknownGuildAsync(nameof(ForumThreadUpdated), data.GuildId, payload).ConfigureAwait(false); + return; + } + if (guild.GetForumChannel(data.ChannelId) is not { } channel) + { + await UnknownChannelAsync(nameof(ForumThreadUpdated), data.ChannelId, payload).ConfigureAwait(false); + return; + } + SocketThread thread = SocketThread.Create(this, channel, data.AuthorId, data.ThreadInfo); + await TimedInvokeAsync(_forumThreadUpdatedEvent, nameof(ForumThreadUpdated), thread).ConfigureAwait(false); + } + + private async Task HandleForumThreadDeletedAsync(object? payload) + { + if (DeserializePayload(payload) is not { } data) return; + if (GetGuild(data.GuildId) is not { } guild) + { + await UnknownGuildAsync(nameof(ForumThreadDeleted), data.GuildId, payload).ConfigureAwait(false); + return; + } + if (guild.GetForumChannel(data.ChannelId) is not { } channel) + { + await UnknownChannelAsync(nameof(ForumThreadDeleted), data.ChannelId, payload).ConfigureAwait(false); + return; + } + SocketThread thread = SocketThread.Create(this, channel, data.AuthorId, data.ThreadInfo); + await TimedInvokeAsync(_forumThreadDeletedEvent, nameof(ForumThreadDeleted), thread).ConfigureAwait(false); + } + + private async Task HandleForumPostCreatedAsync(object? payload) + { + if (DeserializePayload(payload) is not { } data) return; + if (GetGuild(data.GuildId) is not { } guild) + { + await UnknownGuildAsync(nameof(ForumPostCreated), data.GuildId, payload).ConfigureAwait(false); + return; + } + if (guild.GetForumChannel(data.ChannelId) is not { } channel) + { + await UnknownChannelAsync(nameof(ForumPostCreated), data.ChannelId, payload).ConfigureAwait(false); + return; + } + SocketPost post = SocketPost.Create(this, channel, data.AuthorId, data.PostInfo); + await TimedInvokeAsync(_forumPostCreatedEvent, nameof(ForumPostCreated), post).ConfigureAwait(false); + } + + private async Task HandleForumPostDeletedAsync(object? payload) + { + if (DeserializePayload(payload) is not { } data) return; + if (GetGuild(data.GuildId) is not { } guild) + { + await UnknownGuildAsync(nameof(ForumPostDeleted), data.GuildId, payload).ConfigureAwait(false); + return; + } + if (guild.GetForumChannel(data.ChannelId) is not { } channel) + { + await UnknownChannelAsync(nameof(ForumPostDeleted), data.ChannelId, payload).ConfigureAwait(false); + return; + } + SocketPost post = SocketPost.Create(this, channel, data.AuthorId, data.PostInfo); + await TimedInvokeAsync(_forumPostDeletedEvent, nameof(ForumPostDeleted), post).ConfigureAwait(false); + } + + private async Task HandleForumReplyCreatedAsync(object? payload) + { + if (DeserializePayload(payload) is not { } data) return; + if (GetGuild(data.GuildId) is not { } guild) + { + await UnknownGuildAsync(nameof(ForumReplyCreated), data.GuildId, payload).ConfigureAwait(false); + return; + } + if (guild.GetForumChannel(data.ChannelId) is not { } channel) + { + await UnknownChannelAsync(nameof(ForumReplyCreated), data.ChannelId, payload).ConfigureAwait(false); + return; + } + SocketReply reply = SocketReply.Create(this, channel, data.AuthorId, data.ReplyInfo); + await TimedInvokeAsync(_forumReplyCreatedEvent, nameof(ForumReplyCreated), reply).ConfigureAwait(false); + } + + private async Task HandleForumReplyDeletedAsync(object? payload) + { + if (DeserializePayload(payload) is not { } data) return; + if (GetGuild(data.GuildId) is not { } guild) + { + await UnknownGuildAsync(nameof(ForumReplyDeleted), data.GuildId, payload).ConfigureAwait(false); + return; + } + if (guild.GetForumChannel(data.ChannelId) is not { } channel) + { + await UnknownChannelAsync(nameof(ForumReplyDeleted), data.ChannelId, payload).ConfigureAwait(false); + return; + } + SocketReply reply = SocketReply.Create(this, channel, data.AuthorId, data.ReplyInfo); + await TimedInvokeAsync(_forumReplyDeletedEvent, nameof(ForumReplyDeleted), reply).ConfigureAwait(false); + } + + #endregion + + #region Groups + + private async Task HandleGroupRobotAddedAsync(object? payload) + { + if (DeserializePayload(payload) is not { } data) return; + if (GetOrCreateGroupChannel(State, data.GroupOpenid) is not { } channel) + { + await UnknownChannelAsync(nameof(JoinedGroup), data.GroupOpenid, payload).ConfigureAwait(false); + return; + } + Cacheable user = new(null, data.OpMemberOpenId, false, () => Task.FromResult(null)); + await TimedInvokeAsync(_joinedGroupEvent, nameof(JoinedGroup), channel, user).ConfigureAwait(false); + } + + private async Task HandleGroupRobotRemovedAsync(object? payload) + { + if (DeserializePayload(payload) is not { } data) return; + if (GetOrCreateGroupChannel(State, data.GroupOpenid) is not { } channel) + { + await UnknownChannelAsync(nameof(LeftGroup), data.GroupOpenid, payload).ConfigureAwait(false); + return; + } + Cacheable user = new(null, data.OpMemberOpenId, false, () => Task.FromResult(null)); + await TimedInvokeAsync(_leftGroupEvent, nameof(LeftGroup), channel, user).ConfigureAwait(false); + } + + private async Task HandleGroupMessageRejectedAsync(object? payload) + { + if (DeserializePayload(payload) is not { } data) return; + if (GetOrCreateGroupChannel(State, data.GroupOpenid) is not { } channel) + { + await UnknownChannelAsync(nameof(GroupActiveMessageRejected), data.GroupOpenid, payload).ConfigureAwait(false); + return; + } + Cacheable user = new(null, data.OpMemberOpenId, false, () => Task.FromResult(null)); + await TimedInvokeAsync(_groupActiveMessageRejectedEvent, nameof(GroupActiveMessageRejected), channel, user).ConfigureAwait(false); + } + + private async Task HandleGroupMessageReceivedAsync(object? payload) + { + if (DeserializePayload(payload) is not { } data) return; + if (GetOrCreateGroupChannel(State, data.GroupOpenid) is not { } channel) + { + await UnknownChannelAsync(nameof(GroupActiveMessageAllowed), data.GroupOpenid, payload).ConfigureAwait(false); + return; + } + Cacheable user = new(null, data.OpMemberOpenId, false, () => Task.FromResult(null)); + await TimedInvokeAsync(_groupActiveMessageAllowedEvent, nameof(GroupActiveMessageAllowed), channel, user).ConfigureAwait(false); + } + + #endregion + + #region Users + + private async Task HandleFriendAddedAsync(object? payload) + { + if (DeserializePayload(payload) is not { } data) return; + if (GetOrCreateUserChannel(State, data.OpenId) is not { } channel) + { + await UnknownChannelAsync(nameof(UserAdded), data.OpenId, payload).ConfigureAwait(false); + return; + } + await TimedInvokeAsync(_userAddedEvent, nameof(UserAdded), channel).ConfigureAwait(false); + } + + private async Task HandleFriendRemovedAsync(object? payload) + { + if (DeserializePayload(payload) is not { } data) return; + if (GetOrCreateUserChannel(State, data.OpenId) is not { } channel) + { + await UnknownChannelAsync(nameof(UserRemoved), data.OpenId, payload).ConfigureAwait(false); + return; + } + await TimedInvokeAsync(_userRemovedEvent, nameof(UserRemoved), channel).ConfigureAwait(false); + } + + private async Task HandleUserMessageRejectedAsync(object? payload) + { + if (DeserializePayload(payload) is not { } data) return; + if (GetOrCreateUserChannel(State, data.OpenId) is not { } channel) + { + await UnknownChannelAsync(nameof(UserRemoved), data.OpenId, payload).ConfigureAwait(false); + return; + } + await TimedInvokeAsync(_userActiveMessageRejectedEvent, nameof(UserActiveMessageRejected), channel).ConfigureAwait(false); + } + + private async Task HandleUserMessageReceivedAsync(object? payload) + { + if (DeserializePayload(payload) is not { } data) return; + if (GetOrCreateUserChannel(State, data.OpenId) is not { } channel) + { + await UnknownChannelAsync(nameof(UserRemoved), data.OpenId, payload).ConfigureAwait(false); + return; + } + await TimedInvokeAsync(_userActiveMessageAllowedEvent, nameof(UserActiveMessageAllowed), channel).ConfigureAwait(false); + } + + #endregion + #region Raising Events private async Task GuildAvailableAsync(SocketGuild guild) @@ -501,7 +724,7 @@ private async Task GuildAvailableAsync(SocketGuild guild) await TimedInvokeAsync(_guildAvailableEvent, nameof(GuildAvailable), guild).ConfigureAwait(false); } - internal async Task GuildUnavailableAsync(SocketGuild guild) + private async Task GuildUnavailableAsync(SocketGuild guild) { if (!guild.IsConnected) return; guild.IsConnected = false; @@ -514,6 +737,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 UnknownChannelAsync(string dispatch, Guid channelId, object? payload) => + await LogGatewayErrorAsync(dispatch, $"Unknown Group ChannelId: {channelId.ToIdString()}.", payload).ConfigureAwait(false); + private async Task UnknownUserAsync(string dispatch, object? payload) => await LogGatewayErrorAsync(dispatch, $"No User in payload.", payload).ConfigureAwait(false); diff --git a/src/QQBot.Net.WebSocket/QQBotSocketClient.cs b/src/QQBot.Net.WebSocket/QQBotSocketClient.cs index bc5de3f..64919e8 100644 --- a/src/QQBot.Net.WebSocket/QQBotSocketClient.cs +++ b/src/QQBot.Net.WebSocket/QQBotSocketClient.cs @@ -4,7 +4,6 @@ using System.Text.Json.Serialization; using QQBot.API; using QQBot.API.Gateway; -using QQBot.API.Rest; using QQBot.Logging; using QQBot.Net.Queue; using QQBot.Net.WebSockets; @@ -273,7 +272,7 @@ internal SocketUserChannel AddUserChannel(ClientState state, Guid id, SocketUser return channel; } - internal SocketUserChannel GetOrCreateUserChannel(ClientState state, Guid id, SocketUser recipient) => + internal SocketUserChannel GetOrCreateUserChannel(ClientState state, Guid id, SocketUser? recipient = null) => state.GetOrAddUserChannel(id, _ => SocketUserChannel.Create(this, state, id, recipient)); internal SocketDMChannel GetOrCreateDMChannel(ClientState state, ulong id, SocketGuildUser recipient) => @@ -615,33 +614,33 @@ internal async Task ProcessGatewayEventAsync(int sequence, string type, object p #endregion - // #region Forums - // - // case "FORUM_THREAD_CREATE": - // await HandleForumThreadCreatedAsync(payload).ConfigureAwait(false); - // break; - // case "FORUM_THREAD_UPDATE": - // await HandleForumThreadUpdatedAsync(payload).ConfigureAwait(false); - // break; - // case "FORUM_THREAD_DELETE": - // await HandleForumThreadDeletedAsync(payload).ConfigureAwait(false); - // break; - // case "FORUM_POST_CREATE": - // await HandleForumPostCreatedAsync(payload).ConfigureAwait(false); - // break; - // case "FORUM_POST_DELETE": - // await HandleForumPostDeletedAsync(payload).ConfigureAwait(false); - // break; - // case "FORUM_REPLY_CREATE": - // await HandleForumReplyCreatedAsync(payload).ConfigureAwait(false); - // break; - // case "FORUM_REPLY_DELETE": - // await HandleForumReplyDeletedAsync(payload).ConfigureAwait(false); - // break; + #region Forums + + case "FORUM_THREAD_CREATE": + await HandleForumThreadCreatedAsync(payload).ConfigureAwait(false); + break; + case "FORUM_THREAD_UPDATE": + await HandleForumThreadUpdatedAsync(payload).ConfigureAwait(false); + break; + case "FORUM_THREAD_DELETE": + await HandleForumThreadDeletedAsync(payload).ConfigureAwait(false); + break; + case "FORUM_POST_CREATE": + await HandleForumPostCreatedAsync(payload).ConfigureAwait(false); + break; + case "FORUM_POST_DELETE": + await HandleForumPostDeletedAsync(payload).ConfigureAwait(false); + break; + case "FORUM_REPLY_CREATE": + await HandleForumReplyCreatedAsync(payload).ConfigureAwait(false); + break; + case "FORUM_REPLY_DELETE": + await HandleForumReplyDeletedAsync(payload).ConfigureAwait(false); + break; // case "FORUM_PUBLISH_AUDIT_RESULT": // await HandleForumPublishAuditResultAsync(payload).ConfigureAwait(false); // break; - // + // case "OPEN_FORUM_THREAD_CREATE": // await HandleOpenForumThreadCreatedAsync(payload).ConfigureAwait(false); // break; @@ -663,37 +662,42 @@ internal async Task ProcessGatewayEventAsync(int sequence, string type, object p // case "OPEN_FORUM_REPLY_DELETE": // await HandleOpenForumReplyDeletedAsync(payload).ConfigureAwait(false); // break; - // - // #endregion - // - // #region Groups - // - // case "GROUP_ADD_ROBOT": - // await HandleGroupRobotAddedAsync(payload).ConfigureAwait(false); - // break; - // case "GROUP_DEL_ROBOT": - // await HandleGroupRobotRemovedAsync(payload).ConfigureAwait(false); - // break; - // case "GROUP_MSG_REJECT": - // await HandleGroupMessageRejectedAsync(payload).ConfigureAwait(false); - // break; - // case "GROUP_MSG_RECEIVE": - // await HandleGroupMessageReceivedAsync(payload).ConfigureAwait(false); - // break; - // case "FRIEND_ADD": - // await HandleFriendAddedAsync(payload).ConfigureAwait(false); - // break; - // case "FRIEND_DEL": - // await HandleFriendRemovedAsync(payload).ConfigureAwait(false); - // break; - // case "C2C_MSG_REJECT": - // await HandleUserMessageRejectedAsync(payload).ConfigureAwait(false); - // break; - // case "C2C_MSG_RECEIVE": - // await HandleUserMessageReceivedAsync(payload).ConfigureAwait(false); - // break; - // - // #endregion + + #endregion + + #region Groups + + case "GROUP_ADD_ROBOT": + await HandleGroupRobotAddedAsync(payload).ConfigureAwait(false); + break; + case "GROUP_DEL_ROBOT": + await HandleGroupRobotRemovedAsync(payload).ConfigureAwait(false); + break; + case "GROUP_MSG_REJECT": + await HandleGroupMessageRejectedAsync(payload).ConfigureAwait(false); + break; + case "GROUP_MSG_RECEIVE": + await HandleGroupMessageReceivedAsync(payload).ConfigureAwait(false); + break; + + #endregion + + #region Users + + case "FRIEND_ADD": + await HandleFriendAddedAsync(payload).ConfigureAwait(false); + break; + case "FRIEND_DEL": + await HandleFriendRemovedAsync(payload).ConfigureAwait(false); + break; + case "C2C_MSG_REJECT": + await HandleUserMessageRejectedAsync(payload).ConfigureAwait(false); + break; + case "C2C_MSG_RECEIVE": + await HandleUserMessageReceivedAsync(payload).ConfigureAwait(false); + break; + + #endregion default: if (!SuppressUnknownDispatchWarnings)