├── .gitignore ├── LICENSE ├── QQChannelBot ├── Bot │ ├── API │ │ ├── APIList.cs │ │ ├── WebSocket.cs │ │ ├── 公告.cs │ │ ├── 子频道.cs │ │ ├── 子频道权限.cs │ │ ├── 成员.cs │ │ ├── 接口权限.cs │ │ ├── 日程.cs │ │ ├── 消息.cs │ │ ├── 用户.cs │ │ ├── 禁言.cs │ │ ├── 私信.cs │ │ ├── 音频.cs │ │ ├── 频道.cs │ │ └── 频道身份组.cs │ ├── BotClient.cs │ ├── BotCommandRegister.cs │ ├── BotHttpClient.cs │ ├── BotWebSocket.cs │ ├── Command.cs │ ├── Identity.cs │ ├── InfoSDK.cs │ ├── Log.cs │ ├── MessageCenter.cs │ ├── Sender.cs │ ├── SocketEvent │ │ ├── Intent.cs │ │ └── Opcode.cs │ └── StatusCode │ │ └── StatusCodes.cs ├── Models │ ├── APIPermissions.cs │ ├── Announces.cs │ ├── AudioControl.cs │ ├── Channel.cs │ ├── ChannelPermissions.cs │ ├── Emoji.cs │ ├── Guild.cs │ ├── JsonConverters.cs │ ├── Member.cs │ ├── Message.cs │ ├── MessageReaction.cs │ ├── Mute.cs │ ├── Role.cs │ ├── Schedule.cs │ ├── User.cs │ └── WebSocketLimit.cs ├── MsgHelper │ ├── MsgArk23.cs │ ├── MsgArk24.cs │ ├── MsgArk34.cs │ ├── MsgArk37.cs │ ├── MsgDescImg.cs │ ├── MsgEmbed.cs │ ├── MsgImage.cs │ ├── MsgTag.cs │ └── MsgText.cs ├── QQChannelBot.csproj ├── Tools │ ├── Benchmarks.cs │ ├── JsonHelper.cs │ ├── StringTools.cs │ └── Unicoder.cs └── logo.png └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | bin/ 3 | obj/ 4 | *.sln -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Antecer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /QQChannelBot/Bot/API/WebSocket.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using System.Text.Json; 3 | using QQChannelBot.Models; 4 | 5 | namespace QQChannelBot.Bot 6 | { 7 | public partial class BotClient 8 | { 9 | /// 10 | /// 获取通用 WSS 接入点 11 | /// 12 | /// 一个用于连接 websocket 的地址 13 | public async Task GetWssUrl() 14 | { 15 | HttpResponseMessage? respone = await HttpSendAsync($"{ApiOrigin}/gateway"); 16 | JsonElement? res = respone == null ? null : await respone.Content.ReadFromJsonAsync(); 17 | return res?.GetProperty("url").GetString(); 18 | } 19 | /// 20 | /// 获取带分片 WSS 接入点 21 | /// 22 | /// 详情查阅: QQ机器人文档 23 | /// 24 | /// 25 | /// 一个用于连接 websocket 的地址。
同时返回建议的分片数,以及目前连接数使用情况。
26 | public async Task GetWssUrlWithShared() 27 | { 28 | HttpResponseMessage? respone = await HttpSendAsync($"{ApiOrigin}/gateway/bot"); 29 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/API/公告.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using QQChannelBot.Models; 3 | 4 | namespace QQChannelBot.Bot 5 | { 6 | public partial class BotClient 7 | { 8 | /// 9 | /// 创建频道全局公告 10 | /// 11 | /// 频道Id 12 | /// 消息所在子频道Id 13 | /// 消息Id 14 | /// 15 | /// 16 | public async Task CreateAnnouncesGlobalAsync(string guild_id, string channel_id, string message_id, Sender? sender = null) 17 | { 18 | BotAPI api = APIList.创建频道公告; 19 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id), api.Method, JsonContent.Create(new { channel_id, message_id }), sender); 20 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 21 | } 22 | /// 23 | /// 删除频道全局公告 24 | /// 25 | /// 频道Id 26 | /// 用于创建公告的消息Id 27 | /// 28 | /// 29 | public async Task DeleteAnnouncesGlobalAsync(string guild_id, string message_id = "all", Sender? sender = null) 30 | { 31 | BotAPI api = APIList.删除频道公告; 32 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id).Replace("{message_id}", message_id), api.Method, null, sender); 33 | return respone?.IsSuccessStatusCode ?? false; 34 | } 35 | /// 36 | /// 创建子频道公告 37 | /// 38 | /// 子频道Id 39 | /// 消息Id 40 | /// 41 | /// 42 | public async Task CreateAnnouncesAsync(string channel_id, string message_id, Sender? sender = null) 43 | { 44 | BotAPI api = APIList.创建子频道公告; 45 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id), api.Method, channel_id == null ? null : JsonContent.Create(new { message_id }), sender); 46 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 47 | } 48 | /// 49 | /// 删除子频道公告 50 | /// 51 | /// 子频道Id 52 | /// 用于创建公告的消息Id 53 | /// 54 | /// 55 | public async Task DeleteAnnouncesAsync(string channel_id, string message_id = "all", Sender? sender = null) 56 | { 57 | BotAPI api = APIList.删除子频道公告; 58 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id).Replace("{message_id}", message_id), api.Method, null, sender); 59 | return respone?.IsSuccessStatusCode ?? false; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/API/子频道.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using QQChannelBot.Models; 3 | 4 | namespace QQChannelBot.Bot 5 | { 6 | public partial class BotClient 7 | { 8 | /// 9 | /// 获取子频道详情 10 | /// 11 | /// 子频道Id 12 | /// 13 | /// Channel? 14 | public async Task GetChannelAsync(string channel_id, Sender? sender = null) 15 | { 16 | BotAPI api = APIList.获取子频道详情; 17 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id), api.Method, null, sender); 18 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 19 | } 20 | /// 21 | /// 获取频道下的子频道列表 22 | /// 23 | /// 频道Id 24 | /// 筛选子频道类型 25 | /// 筛选子频道子类型 26 | /// 27 | /// 28 | public async Task?> GetChannelsAsync(string guild_id, ChannelType? channelType = null, ChannelSubType? channelSubType = null, Sender? sender = null) 29 | { 30 | BotAPI api = APIList.获取子频道列表; 31 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id), api.Method, null, sender); 32 | List? channels = respone == null ? null : await respone.Content.ReadFromJsonAsync?>(); 33 | if (channels != null) 34 | { 35 | if (channelType != null) channels = channels.Where(channel => channel.Type == channelType).ToList(); 36 | if (channelSubType != null) channels = channels.Where(channel => channel.SubType == channelSubType).ToList(); 37 | } 38 | return channels; 39 | } 40 | /// 41 | /// 创建子频道(仅私域可用) 42 | /// 43 | /// 用于创建子频道的对象(需提前填充必要字段) 44 | /// 45 | /// 46 | public async Task CreateChannelAsync(Channel channel, Sender? sender = null) 47 | { 48 | BotAPI api = APIList.创建子频道; 49 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", channel.GuildId), api.Method, JsonContent.Create(channel), sender); 50 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 51 | } 52 | /// 53 | /// 修改子频道(仅私域可用) 54 | /// 55 | /// 修改属性后的子频道对象 56 | /// 57 | /// 58 | public async Task EditChannelAsync(Channel channel, Sender? sender = null) 59 | { 60 | BotAPI api = APIList.修改子频道; 61 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel.Id), api.Method, JsonContent.Create(channel), sender); 62 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 63 | } 64 | /// 65 | /// 删除指定子频道(仅私域可用) 66 | /// 67 | /// 要删除的子频道Id 68 | /// 69 | /// 70 | public async Task DeleteChannelAsync(string channel_id, Sender? sender = null) 71 | { 72 | BotAPI api = APIList.删除子频道; 73 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id), api.Method, null, sender); 74 | return respone?.IsSuccessStatusCode ?? false; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/API/子频道权限.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using QQChannelBot.Models; 3 | 4 | namespace QQChannelBot.Bot 5 | { 6 | public partial class BotClient 7 | { 8 | /// 9 | /// 获取用户在指定子频道的权限 10 | /// 11 | /// 子频道Id 12 | /// 用户Id 13 | /// 14 | /// 15 | public async Task GetChannelPermissionsAsync(string channel_id, string user_id, Sender? sender = null) 16 | { 17 | BotAPI api = APIList.获取子频道用户权限; 18 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id).Replace("{user_id}", user_id), api.Method, null, sender); 19 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 20 | } 21 | /// 22 | /// 修改用户在指定子频道的权限 23 | /// 24 | /// 子频道Id 25 | /// 用户Id 26 | /// 添加的权限 27 | /// 删除的权限 28 | /// 29 | /// 30 | public async Task EditChannelPermissionsAsync(string channel_id, string user_id, string add = "0", string remove = "0", Sender? sender = null) 31 | { 32 | BotAPI api = APIList.修改子频道用户权限; 33 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id).Replace("{user_id}", user_id), api.Method, JsonContent.Create(new 34 | { 35 | add, 36 | remove 37 | }), sender); 38 | return respone?.IsSuccessStatusCode ?? false; 39 | } 40 | /// 41 | /// 获取指定身份组在指定子频道的权限 42 | /// 43 | /// 子频道Id 44 | /// 身份组Id 45 | /// 46 | /// 47 | public async Task GetMemberChannelPermissionsAsync(string channel_id, string role_id, Sender? sender = null) 48 | { 49 | BotAPI api = APIList.获取子频道身份组权限; 50 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id).Replace("{role_id}", role_id), api.Method, null, sender); 51 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 52 | } 53 | /// 54 | /// 修改指定身份组在指定子频道的权限 55 | /// 56 | /// 子频道Id 57 | /// 身份组Id 58 | /// 添加的权限 59 | /// 删除的权限 60 | /// 61 | /// 62 | public async Task EditMemberChannelPermissionsAsync(string channel_id, string role_id, string add = "0", string remove = "0", Sender? sender = null) 63 | { 64 | BotAPI api = APIList.修改子频道身份组权限; 65 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id).Replace("{role_id}", role_id), api.Method, JsonContent.Create(new 66 | { 67 | add, 68 | remove 69 | }), sender); 70 | return respone?.IsSuccessStatusCode ?? false; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/API/成员.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using QQChannelBot.Models; 3 | 4 | namespace QQChannelBot.Bot 5 | { 6 | public partial class BotClient 7 | { 8 | /// 9 | /// 获取频道成员列表(仅私域可用) 10 | /// 11 | /// guild_id - 频道Id
12 | /// limit - 分页大小1-1000(默认值10)
13 | /// after - 上次回包中最后一个Member的用户ID,首次请求填0 14 | ///
15 | ///
16 | /// 频道Id 17 | /// 分页大小1-1000(默认值10) 18 | /// 上次回包中最后一个Member的用户ID,首次请求填"0" 19 | /// 20 | /// 21 | public async Task?> GetGuildMembersAsync(string guild_id, int limit = 10, string? after = null, Sender? sender = null) 22 | { 23 | BotAPI api = APIList.获取频道成员列表; 24 | HttpResponseMessage? respone = await HttpSendAsync($"{api.Path.Replace("{guild_id}", guild_id)}?limit={limit}&after={after ?? "0"}", api.Method, null, sender); 25 | return respone == null ? null : await respone.Content.ReadFromJsonAsync?>(); 26 | } 27 | /// 28 | /// 获取频道成员详情 29 | /// 30 | /// 频道Id 31 | /// 成员用户Id 32 | /// 33 | /// 34 | public async Task GetMemberAsync(string guild_id, string user_id, Sender? sender = null) 35 | { 36 | BotAPI api = APIList.获取成员详情; 37 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id).Replace("{user_id}", user_id), api.Method, null, sender); 38 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 39 | } 40 | /// 41 | /// 删除指定频道成员(仅私域可用) 42 | /// 43 | /// 频道Id 44 | /// 用户Id 45 | /// 46 | /// 47 | public async Task DeleteGuildMemberAsync(string guild_id, string user_id, Sender? sender = null) 48 | { 49 | BotAPI api = APIList.删除频道成员; 50 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id).Replace("{user_id}", user_id), api.Method, null, sender); 51 | return respone?.IsSuccessStatusCode ?? false; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/API/接口权限.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using QQChannelBot.Models; 3 | 4 | namespace QQChannelBot.Bot 5 | { 6 | public partial class BotClient 7 | { 8 | /// 9 | /// 获取频道可用权限列表 10 | /// 11 | /// 获取机器人在频道 guild_id 内可以使用的权限列表 12 | /// 13 | /// 14 | /// 频道Id 15 | /// 16 | /// 17 | public async Task?> GetGuildPermissionsAsync(string guild_id, Sender? sender = null) 18 | { 19 | BotAPI api = APIList.获取频道可用权限列表; 20 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id), api.Method, null, sender); 21 | APIPermissions? Permissions = respone == null ? null : await respone.Content.ReadFromJsonAsync(); 22 | return Permissions?.List; 23 | } 24 | /// 25 | /// 创建频道 API 接口权限授权链接 26 | /// 27 | /// 频道Id 28 | /// 子频道Id 29 | /// 权限需求标识对象 30 | /// 机器人申请对应的 API 接口权限后可以使用功能的描述 31 | /// 32 | /// 33 | public async Task SendPermissionDemandAsync(string guild_id, string channel_id, APIPermissionDemandIdentify api_identify, string desc = "", Sender? sender = null) 34 | { 35 | BotAPI api = APIList.创建频道接口授权链接; 36 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id), api.Method, JsonContent.Create(new 37 | { 38 | channel_id, 39 | api_identify, 40 | desc 41 | }), sender); 42 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/API/日程.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using QQChannelBot.Models; 3 | 4 | namespace QQChannelBot.Bot 5 | { 6 | public partial class BotClient 7 | { 8 | /// 9 | /// 获取频道日程列表 10 | /// 11 | /// 获取某个日程子频道里中当天的日程列表;
12 | /// 若带了参数 since,则返回结束时间在 since 之后的日程列表;
13 | /// 若未带参数 since,则默认返回当天的日程列表。 14 | ///
15 | ///
16 | /// 日程子频道Id 17 | /// 筛选日程开始时间(默认为当日全天) 18 | /// 19 | /// List<Schedule>? 20 | public async Task?> GetSchedulesAsync(string channel_id, DateTime? since = null, Sender? sender = null) 21 | { 22 | BotAPI api = APIList.获取频道日程列表; 23 | string param = since == null ? "" : $"?since={new DateTimeOffset(since.Value).ToUnixTimeMilliseconds()}"; 24 | HttpResponseMessage? respone = await HttpSendAsync($"{api.Path.Replace("{channel_id}", channel_id)}{param}", api.Method, null, sender); 25 | return respone == null ? null : await respone.Content.ReadFromJsonAsync?>(); 26 | } 27 | /// 28 | /// 获取日程详情 29 | /// 30 | /// 日程子频道Id 31 | /// 日程Id 32 | /// 33 | /// 目标 Schedule 对象 34 | public async Task GetScheduleAsync(string channel_id, string schedule_id, Sender? sender = null) 35 | { 36 | BotAPI api = APIList.获取日程详情; 37 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id).Replace("{schedule_id}", schedule_id), api.Method, null, sender); 38 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 39 | } 40 | /// 41 | /// 创建日程 42 | /// 43 | /// 要求操作人具有"管理频道"的权限,如果是机器人,则需要将机器人设置为管理员。
44 | /// 创建成功后,返回创建成功的日程对象。
45 | /// 日程开始时间必须大于当前时间。 46 | ///
47 | ///
48 | /// 日程子频道Id 49 | /// 新的日程对象,不需要带Id 50 | /// 51 | /// 新创建的 Schedule 对象 52 | public async Task CreateScheduleAsync(string channel_id, Schedule schedule, Sender? sender = null) 53 | { 54 | BotAPI api = APIList.创建日程; 55 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id), api.Method, JsonContent.Create(new { schedule }), sender); 56 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 57 | } 58 | /// 59 | /// 修改日程 60 | /// 61 | /// 要求操作人具有"管理频道"的权限,如果是机器人,则需要将机器人设置为管理员。
62 | /// 修改成功后,返回修改后的日程对象。 63 | ///
64 | ///
65 | /// 日程子频道Id 66 | /// 修改后的日程对象 67 | /// 68 | /// 修改后的 Schedule 对象 69 | public async Task EditScheduleAsync(string channel_id, Schedule schedule, Sender? sender = null) 70 | { 71 | BotAPI api = APIList.修改日程; 72 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id).Replace("{schedule_id}", schedule.Id), api.Method, JsonContent.Create(new { schedule }), sender); 73 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 74 | } 75 | /// 76 | /// 删除日程 77 | /// 78 | /// 要求操作人具有"管理频道"的权限,如果是机器人,则需要将机器人设置为管理员。 79 | /// 80 | /// 81 | /// 日程子频道Id 82 | /// 日程对象 83 | /// 这里是为了获取日程Id,为了防错设计为传递日程对象 84 | /// 85 | /// HTTP 状态码 204 86 | public async Task DeleteScheduleAsync(string channel_id, Schedule schedule, Sender? sender = null) 87 | { 88 | BotAPI api = APIList.删除日程; 89 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id).Replace("{schedule_id}", schedule.Id), api.Method, null, sender); 90 | return respone?.IsSuccessStatusCode ?? false; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/API/消息.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using QQChannelBot.Models; 3 | 4 | namespace QQChannelBot.Bot 5 | { 6 | public partial class BotClient 7 | { 8 | /// 9 | /// 获取指定消息 10 | /// 11 | /// 子频道Id 12 | /// 消息Id 13 | /// 14 | /// 15 | public async Task GetMessageAsync(string channel_id, string message_id, Sender? sender = null) 16 | { 17 | BotAPI api = APIList.获取指定消息; 18 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id).Replace("{message_id}", message_id), api.Method, null, sender); 19 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 20 | } 21 | /// 22 | /// 获取消息列表(2022年1月29日暂未开通) 23 | /// 24 | /// 作为坐标的消息(需要消息Id和子频道Id) 25 | /// 分页大小(1-20) 26 | /// 拉取类型(默认拉取最新消息) 27 | /// 28 | /// 29 | public async Task?> GetMessagesAsync(Message message, int limit = 20, GetMsgTypesEnum? typesEnum = GetMsgTypesEnum.latest, Sender? sender = null) 30 | { 31 | BotAPI api = APIList.获取消息列表; 32 | string type = typesEnum == null ? "" : $"&type={typesEnum}"; 33 | HttpResponseMessage? respone = await HttpSendAsync($"{api.Path.Replace("{channel_id}", message.ChannelId)}?limit={limit}&id={message.Id}{type}", api.Method, null, sender); 34 | return respone == null ? null : await respone.Content.ReadFromJsonAsync?>(); 35 | } 36 | /// 37 | /// 发送消息 38 | /// 39 | /// 要求操作人在该子频道具有"发送消息"的权限
40 | /// 发送成功之后,会触发一个创建消息的事件
41 | /// 被动回复消息有效期为 5 分钟
42 | /// 主动推送消息每个子频道限 2 条/天
43 | /// 发送消息接口要求机器人接口需要链接到websocket gateway 上保持在线状态 44 | ///
45 | ///
46 | /// 子频道Id 47 | /// 消息对象 48 | /// 49 | /// 50 | public async Task SendMessageAsync(string channel_id, MessageToCreate message, Sender? sender = null) 51 | { 52 | BotAPI api = APIList.发送消息; 53 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id), api.Method, JsonContent.Create(message), sender); 54 | Message? result = respone == null ? null : await respone.Content.ReadFromJsonAsync(); 55 | return LastMessage(result, true); 56 | } 57 | /// 58 | /// 撤回消息 59 | /// 60 | /// 子频道Id 61 | /// 消息Id 62 | /// 63 | /// 64 | public async Task DeleteMessageAsync(string channel_id, string message_id, Sender? sender = null) 65 | { 66 | BotAPI api = APIList.撤回消息; 67 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id).Replace("{message_id}", message_id), api.Method, null, sender); 68 | return respone?.IsSuccessStatusCode ?? false; 69 | } 70 | /// 71 | /// 撤回目标用户在当前子频道发出的最后一条消息 72 | /// 73 | /// 需要传入指令发出者的消息对象
74 | /// 用于检索指令发出者所在频道信息 75 | ///
76 | ///
77 | /// 78 | /// 被撤回消息的目标用户信息
79 | /// 需要:message.GuildId、message.ChannelId、message.Author.Id 80 | /// 81 | /// 82 | /// 83 | public async Task DeleteLastMessageAsync(Message? masterMessage, Sender? sender = null) 84 | { 85 | Message? lastMessage = LastMessage(masterMessage); 86 | return lastMessage == null ? null : await DeleteMessageAsync(lastMessage.ChannelId, lastMessage.Id, sender); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/API/用户.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using QQChannelBot.Models; 3 | 4 | namespace QQChannelBot.Bot 5 | { 6 | public partial class BotClient 7 | { 8 | /// 9 | /// 获取当前用户(机器人)信息 10 | /// 此API无需任何权限 11 | /// 12 | /// 13 | /// 当前用户对象 14 | public async Task GetMeAsync(Sender? sender = null) 15 | { 16 | BotAPI api = APIList.获取用户详情; 17 | HttpResponseMessage? respone = await HttpSendAsync(api.Path, api.Method, null, sender); 18 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 19 | } 20 | /// 21 | /// 获取当前用户(机器人)已加入频道列表 22 | /// 此API无需任何权限 23 | /// 24 | /// 频道Id(作为拉取下一次列表的分页坐标使用) 25 | /// 数据拉取方向(true-向前查找 | false-向后查找) 26 | /// 数据分页(默认每次拉取100条) 27 | /// 28 | /// 29 | public async Task?> GetMeGuildsAsync(string? guild_id = null, bool route = false, int limit = 100, Sender? sender = null) 30 | { 31 | BotAPI api = APIList.获取用户频道列表; 32 | guild_id = string.IsNullOrWhiteSpace(guild_id) ? "" : $"&{(route ? "before" : "after")}={guild_id}"; 33 | HttpResponseMessage? respone = await HttpSendAsync($"{api.Path}?limit={limit}{guild_id}", api.Method, null, sender); 34 | return respone == null ? null : await respone.Content.ReadFromJsonAsync?>(); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /QQChannelBot/Bot/API/禁言.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using QQChannelBot.Models; 3 | 4 | namespace QQChannelBot.Bot 5 | { 6 | public partial class BotClient 7 | { 8 | /// 9 | /// 频道全局禁言 10 | /// 11 | /// muteTime - 禁言时间:
12 | /// mute_end_timestamp 禁言到期时间戳,绝对时间戳,单位:秒
13 | /// mute_seconds 禁言多少秒 14 | ///
15 | ///
16 | /// 频道Id 17 | /// 禁言模式 18 | /// 19 | /// 20 | public async Task MuteGuildAsync(string guild_id, MuteTime muteTime, Sender? sender = null) 21 | { 22 | BotAPI api = APIList.禁言全员; 23 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id), api.Method, JsonContent.Create(muteTime), sender); 24 | return respone?.IsSuccessStatusCode ?? false; 25 | } 26 | /// 27 | /// 频道指定成员禁言 28 | /// 29 | /// muteTime - 禁言时间:
30 | /// mute_end_timestamp 禁言到期时间戳,绝对时间戳,单位:秒
31 | /// mute_seconds 禁言多少秒 32 | ///
33 | ///
34 | /// 频道Id 35 | /// 成员Id 36 | /// 禁言时间 37 | /// 38 | /// 39 | public async Task MuteMemberAsync(string guild_id, string user_id, MuteTime muteTime, Sender? sender = null) 40 | { 41 | BotAPI api = APIList.禁言指定成员; 42 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id).Replace("{user_id}", user_id), api.Method, JsonContent.Create(muteTime), sender); 43 | return respone?.IsSuccessStatusCode ?? false; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/API/私信.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using QQChannelBot.Models; 3 | 4 | namespace QQChannelBot.Bot 5 | { 6 | public partial class BotClient 7 | { 8 | /// 9 | /// 创建私信会话 10 | /// 11 | /// 接收者Id 12 | /// 源频道Id 13 | /// 14 | /// 15 | public async Task CreateDMSAsync(string recipient_id, string source_guild_id, Sender sender) 16 | { 17 | BotAPI api = APIList.创建私信会话; 18 | HttpResponseMessage? respone = await HttpSendAsync(api.Path, api.Method, JsonContent.Create(new { recipient_id, source_guild_id }), sender); 19 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 20 | } 21 | /// 22 | /// 发送私信 23 | /// 用于发送私信消息,前提是已经创建了私信会话。 24 | /// 25 | /// 私信频道Id 26 | /// 消息对象 27 | /// 28 | /// 29 | public async Task SendPMAsync(string guild_id, MessageToCreate message, Sender? sender = null) 30 | { 31 | BotAPI api = APIList.发送私信; 32 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id), api.Method, JsonContent.Create(message), sender); 33 | Message? result = respone == null ? null : await respone.Content.ReadFromJsonAsync(); 34 | return LastMessage(result, true); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/API/音频.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using QQChannelBot.Models; 3 | 4 | namespace QQChannelBot.Bot 5 | { 6 | public partial class BotClient 7 | { 8 | /// 9 | /// 音频控制 10 | /// 11 | /// 子频道Id 12 | /// 音频对象 13 | /// 14 | /// 15 | public async Task AudioControlAsync(string channel_id, AudioControl audioControl, Sender? sender = null) 16 | { 17 | BotAPI api = APIList.音频控制; 18 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{channel_id}", channel_id), api.Method, JsonContent.Create(audioControl), sender); 19 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/API/频道.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using QQChannelBot.Models; 3 | 4 | namespace QQChannelBot.Bot 5 | { 6 | public partial class BotClient 7 | { 8 | /// 9 | /// 获取频道详情 10 | /// 11 | /// 频道Id 12 | /// 13 | /// Guild? 14 | public async Task GetGuildAsync(string guild_id, Sender? sender = null) 15 | { 16 | BotAPI api = APIList.获取频道详情; 17 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id), api.Method, null, sender); 18 | return respone == null ? null : await respone.Content.ReadFromJsonAsync(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/API/频道身份组.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using QQChannelBot.Models; 3 | 4 | namespace QQChannelBot.Bot 5 | { 6 | public partial class BotClient 7 | { 8 | /// 9 | /// 获取频道身份组列表 10 | /// 11 | /// 频道Id 12 | /// 13 | /// 14 | public async Task?> GetRolesAsync(string guild_id, Sender? sender = null) 15 | { 16 | BotAPI api = APIList.获取频道身份组列表; 17 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id), api.Method, null, sender); 18 | var result = respone == null ? null : await respone.Content.ReadFromJsonAsync(); 19 | return result?.Roles; 20 | } 21 | /// 22 | /// 创建频道身份组 23 | /// 24 | /// 频道Id 25 | /// 携带需要设置的字段内容 26 | /// 标识需要设置哪些字段,若不填则根据Info自动推测 27 | /// 28 | /// 29 | public async Task CreateRoleAsync(string guild_id, Info info, Filter? filter = null, Sender? sender = null) 30 | { 31 | BotAPI api = APIList.创建频道身份组; 32 | filter ??= new Filter(!string.IsNullOrWhiteSpace(info.Name), info.Color != null, info.Hoist ?? false); 33 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id), api.Method, JsonContent.Create(new { filter, info }), sender); 34 | var result = respone == null ? null : await respone.Content.ReadFromJsonAsync(); 35 | return result?.Role; 36 | 37 | } 38 | /// 39 | /// 修改频道身份组 40 | /// 41 | /// 频道Id 42 | /// 角色Id 43 | /// 携带需要修改的字段内容 44 | /// 标识需要设置哪些字段,若不填则根据Info自动推测 45 | /// 46 | /// 47 | public async Task EditRoleAsync(string guild_id, string role_id, Info info, Filter? filter = null, Sender? sender = null) 48 | { 49 | BotAPI api = APIList.修改频道身份组; 50 | filter ??= new Filter(!string.IsNullOrWhiteSpace(info.Name), info.Color != null, info.Hoist ?? false); 51 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id).Replace("{role_id}", role_id), api.Method, JsonContent.Create(new { filter, info }), sender); 52 | var result = respone == null ? null : await respone.Content.ReadFromJsonAsync(); 53 | return result?.Role; 54 | } 55 | /// 56 | /// 删除频道身份组 57 | /// HTTP状态码 204 表示成功 58 | /// 59 | /// 频道Id 60 | /// 身份Id 61 | /// 62 | /// 63 | public async Task DeleteRoleAsync(string guild_id, string role_id, Sender? sender = null) 64 | { 65 | BotAPI api = APIList.删除频道身份组; 66 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id).Replace("{role_id}", role_id), api.Method, null, sender); 67 | return respone?.IsSuccessStatusCode ?? false; 68 | } 69 | /// 70 | /// 增加频道身份组成员 71 | /// 72 | /// 需要使用的 token 对应的用户具备增加身份组成员权限。如果是机器人,要求被添加为管理员。
73 | /// 如果要增加的身份组ID是(5-子频道管理员),需要增加 channel_id 来指定具体是哪个子频道。 74 | ///
75 | ///
76 | /// 频道Id 77 | /// 用户Id 78 | /// 身份组Id 79 | /// 子频道Id 80 | /// 81 | /// 82 | public async Task AddRoleMemberAsync(string guild_id, string user_id, string role_id, string? channel_id = null, Sender? sender = null) 83 | { 84 | BotAPI api = APIList.添加频道身份组成员; 85 | HttpContent? httpContent = channel_id == null ? null : JsonContent.Create(new { channel = new Channel { Id = channel_id } }); 86 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id).Replace("{user_id}", user_id).Replace("{role_id}", role_id), api.Method, httpContent, sender); 87 | return respone?.IsSuccessStatusCode ?? false; 88 | } 89 | /// 90 | /// 删除频道身份组成员 91 | /// 92 | /// 需要使用的 token 对应的用户具备删除身份组成员权限。如果是机器人,要求被添加为管理员。
93 | /// 如果要删除的身份组ID是(5-子频道管理员),需要设置 channel_id 来指定具体是哪个子频道。
94 | /// 详情查阅 QQ机器人文档 95 | ///
96 | ///
97 | /// 频道Id 98 | /// 要加入身份组的用户Id 99 | /// 身份组Id 100 | /// 子频道Id 101 | /// 102 | /// 103 | public async Task DeleteRoleMemberAsync(string guild_id, string user_id, string role_id, string? channel_id = null, Sender? sender = null) 104 | { 105 | BotAPI api = APIList.删除频道身份组成员; 106 | HttpContent? httpContent = channel_id == null ? null : JsonContent.Create(new { channel = new Channel { Id = channel_id } }); 107 | HttpResponseMessage? respone = await HttpSendAsync(api.Path.Replace("{guild_id}", guild_id).Replace("{user_id}", user_id).Replace("{role_id}", role_id), api.Method, httpContent, sender); 108 | return respone?.IsSuccessStatusCode ?? false; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/BotClient.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Net.WebSockets; 3 | using System.Text.Json; 4 | using System.Text.RegularExpressions; 5 | using QQChannelBot.Bot.SocketEvent; 6 | using QQChannelBot.Bot.StatusCode; 7 | using QQChannelBot.Models; 8 | 9 | namespace QQChannelBot.Bot 10 | { 11 | /// 12 | /// 机器人对象 13 | /// 14 | /// 可选属性配置表:
15 | /// BotAccessInfo - 机器人鉴权登陆信息,见
16 | /// SadboxGuildId - 指定用于调试机器人的沙箱频道(DebugBot=true时有效)
17 | /// DebugBot - 指定机器人运行的模式[true:测试; false:正式];默认值=false
18 | /// Info - 机器人的 信息(在机器人鉴权通过后更新);默认值=null
19 | /// Members - 自动记录机器人在各频道内的身份组信息
20 | /// ReportApiError - 向前端消息发出者报告API错误[true:报告;false:静默];默认值=false
21 | /// SandBox - 机器人调用API的模式[true:沙箱;false:正式];默认值=false
22 | /// ApiOrigin - (只读) 获取机器人当前使用的ApiUrl
23 | /// Intents - 订阅频道事件,详见:;默认值=(GUILDS|GUILD_MEMBERS|AT_MESSAGES|GUILD_MESSAGE_REACTIONS)
24 | /// Guilds - 机器人已加入的频道列表 25 | ///
26 | ///
27 | public partial class BotClient 28 | { 29 | /// 30 | /// 记录机器人启动时间 31 | /// 32 | public static DateTime StartTime { get; private set; } = DateTime.Now; 33 | 34 | #region 可以监听的固定事件列表 35 | /// 36 | /// WebSocketClient连接后触发 37 | /// 等同于 WebSocket.OnOpen 事件 38 | /// 39 | public event Action? OnWebSocketConnected; 40 | /// 41 | /// WebSocketClient关闭后触发 42 | /// 43 | public event Action? OnWebSocketClosed; 44 | /// 45 | /// WebSocketClient发送数据前触发 46 | /// 47 | public event Action? OnWebSoketSending; 48 | /// 49 | /// WebSocketClient收到数据后触发 50 | /// 51 | public event Action? OnWebSocketReceived; 52 | /// 53 | /// 收到服务端推送的消息时触发 54 | /// 55 | public event Action? OnDispatch; 56 | /// 57 | /// 客户端发送心跳或收到服务端推送心跳时触发 58 | /// 59 | public event Action? OnHeartbeat; 60 | /// 61 | /// 客户端发送鉴权时触发 62 | /// 63 | public event Action? OnIdentify; 64 | /// 65 | /// 客户端恢复连接时触发 66 | /// 67 | public event Action? OnResume; 68 | /// 69 | /// 服务端通知客户端重新连接时触发 70 | /// 71 | public event Action? OnReconnect; 72 | /// 73 | /// 当identify或resume的时候,参数错误的时候触发 74 | /// 75 | public event Action? OnInvalidSession; 76 | /// 77 | /// 当客户端与网关建立ws连接的时候触发 78 | /// 79 | public event Action? OnHello; 80 | /// 81 | /// 客户端发送心跳被服务端接收后触发 82 | /// 83 | public event Action? OnHeartbeatACK; 84 | /// 85 | /// 鉴权连接成功后触发 86 | /// 注:此时获取的User对象只有3个属性 {id,username,bot} 87 | /// 88 | public event Action? OnReady; 89 | /// 90 | /// 恢复连接成功后触发 91 | /// 92 | public event Action? OnResumed; 93 | /// 94 | /// 频道信息变更后触发 95 | /// 96 | /// 机器人加入频道, 频道资料变更, 机器人退出频道
97 | /// BotClient - 机器人对象
98 | /// Guild - 频道对象
99 | /// string - 事件类型(GUILD_ADD | GUILD_UPDATE | GUILD_REMOVE) 100 | ///
101 | ///
102 | public event Action? OnGuildMsg; 103 | /// 104 | /// 子频道被修改后触发 105 | /// 106 | /// 子频道被创建, 子频道资料变更, 子频道被删除
107 | /// BotClient - 机器人对象
108 | /// Channel - 频道对象(没有分组Id和排序位置属性)
109 | /// string - 事件类型(CHANNEL_CREATE|CHANNEL_UPDATE|CHANNEL_DELETE) 110 | ///
111 | ///
112 | public event Action? OnChannelMsg; 113 | /// 114 | /// 成员信息变更后触发 115 | /// 116 | /// 成员加入, 资料变更, 移除成员
117 | /// BotClient - 机器人对象
118 | /// MemberWithGuildID - 成员对象
119 | /// string - 事件类型(GUILD_MEMBER_ADD | GUILD_MEMBER_UPDATE | GUILD_MEMBER_REMOVE) 120 | ///
121 | ///
122 | public event Action? OnGuildMemberMsg; 123 | /// 124 | /// 修改表情表态后触发 125 | /// 126 | /// 添加表情表态, 删除表情表态
127 | /// BotClient - 机器人对象
128 | /// MessageReaction - 表情表态对象
129 | /// string - 事件类型(MESSAGE_REACTION_ADD|MESSAGE_REACTION_REMOVE) 130 | ///
131 | ///
132 | public event Action? OnMessageReaction; 133 | /// 134 | /// 消息审核出结果后触发 135 | /// 136 | /// 消息审核通过才有MessageId属性
137 | /// BotClient - 机器人对象
138 | /// MessageAudited - 消息审核对象
139 | /// MessageAudited.IsPassed - 消息审核是否通过 140 | ///
141 | ///
142 | public event Action? OnMessageAudit; 143 | /// 144 | /// 音频状态变更后触发 145 | /// 146 | public event Action? OnAudioMsg; 147 | /// 148 | /// 频道内有人发消息就触发 (包含 @机器人 消息) 149 | /// 150 | /// Sender - 发件人对象
151 | /// 包含消息类别(公开,AT机器人,AT全员,私聊) 152 | ///
153 | ///
154 | public event Action? OnMsgCreate; 155 | /// 156 | /// API调用出错时触发 157 | /// 158 | /// Sender - 发件人对象(仅当调用API的主体为Sender时才有)
159 | /// List<string> - API错误信息{接口地址,请求方式,异常代码,异常原因}
160 | /// FreezeTime - 对访问出错的接口,暂停使用的时间 161 | ///
162 | ///
163 | public event Action? OnApiError; 164 | /// 165 | /// 消息过滤器 166 | /// 当返回值为True时,该消息将被拦截并丢弃 167 | /// 168 | public Func? MessageFilter; 169 | #endregion 170 | 171 | /// 172 | /// 鉴权信息 173 | /// 可在这里查询 QQ机器人开发设置 174 | /// 175 | public Identity BotAccessInfo { get; set; } 176 | /// 177 | /// 向前端指令发出者报告API错误 178 | /// 注:该属性作为 的默认值使用 179 | /// 180 | public bool ReportApiError { get; set; } 181 | /// 182 | /// 机器人用户信息 183 | /// 184 | public User Info { get; private set; } = new User(); 185 | /// 186 | /// 缓存机器人已加入的频道 187 | /// 188 | public ConcurrentDictionary Guilds { get; private set; } = new(); 189 | /// 190 | /// 保存机器人在各频道内的角色信息 191 | /// 192 | /// string - GUILD_ID
193 | /// Member - 角色信息 194 | ///
195 | ///
196 | public ConcurrentDictionary Members { get; private set; } = new(); 197 | /// 198 | /// 缓存消息 199 | /// 200 | /// string - UserId
201 | /// List<Message> - 用户消息列表 202 | ///
203 | ///
204 | private ConcurrentDictionary> StackMessage { get; set; } = new(); 205 | /// 206 | /// 存取最后一次发送的消息 207 | /// 注:目的是为了用于撤回消息(自动删除已过5分钟的记录) 208 | /// 209 | /// 需要存储的msg,或者用于检索同频道的msg 210 | /// fase-出栈;true-入栈 211 | private Message? LastMessage(Message? msg, bool push = false) 212 | { 213 | if (msg == null) return null; 214 | DateTime outTime = DateTime.Now.AddMinutes(-10); 215 | string userId = msg.Author.Id; 216 | StackMessage.TryGetValue(userId, out var messages); 217 | if (push) 218 | { 219 | messages?.RemoveAll(m => m.CreateTime < outTime); 220 | messages ??= new(); 221 | messages.Add(msg); 222 | StackMessage[userId] = messages; 223 | } 224 | else 225 | { 226 | int stackIndex = messages?.FindLastIndex(m => m.GuildId.Equals(msg.GuildId) && m.ChannelId.Equals(msg.ChannelId)) ?? -1; 227 | if (stackIndex >= 0) 228 | { 229 | msg = messages![stackIndex]; 230 | messages.RemoveAt(stackIndex); 231 | } 232 | else msg = null; 233 | } 234 | return msg; 235 | } 236 | #region Http客户端配置 237 | /// 238 | /// 正式环境 239 | /// 240 | private static string ReleaseApi => "https://api.sgroup.qq.com"; 241 | /// 242 | /// 沙箱环境 243 | /// 244 | /// 沙箱环境只会收到测试频道的事件,且调用openapi仅能操作测试频道 245 | /// 246 | /// 247 | private static string SandboxApi => "https://sandbox.api.sgroup.qq.com"; 248 | /// 249 | /// 机器人接口域名 250 | /// 251 | public string ApiOrigin { get; init; } 252 | /// 253 | /// 集中处理机器人的HTTP请求 254 | /// 255 | /// 请求网址 256 | /// 请求类型(默认GET) 257 | /// 请求数据 258 | /// 指示本次请求由谁发起的 259 | /// 260 | public async Task HttpSendAsync(string url, HttpMethod? method = null, HttpContent? content = null, Sender? sender = null) 261 | { 262 | method ??= HttpMethod.Get; 263 | HttpRequestMessage request = new() { RequestUri = new(new(ApiOrigin), url), Content = content, Method = method }; 264 | request.Headers.Authorization = new("Bot", $"{BotAccessInfo.BotAppId}.{BotAccessInfo.BotToken}"); 265 | // 捕获Http请求错误 266 | return await BotHttpClient.SendAsync(request, async (response, freezeTime) => 267 | { 268 | ApiErrorInfo errInfo = new(url.TrimStart(ApiOrigin), method.Method, response.StatusCode.GetHashCode(), "此错误类型未收录!", freezeTime); 269 | if (response.Content.Headers.ContentType?.MediaType == "application/json") 270 | { 271 | ApiStatus? err = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync()); 272 | if (err?.Code != null) errInfo.Code = err.Code; 273 | if (err?.Message != null) errInfo.Detail = err.Message; 274 | } 275 | if (StatusCodes.OpenapiCode.TryGetValue(errInfo.Code, out string? value)) errInfo.Detail = value; 276 | string senderMaster = (sender?.Bot.Guilds.TryGetValue(sender.GuildId, out var guild) == true ? guild.Name : null) ?? sender?.Author.UserName ?? ""; 277 | Log.Error($"[接口访问失败]{senderMaster} 代码:{errInfo.Code},详情:{errInfo.Detail}"); 278 | 279 | OnApiError?.Invoke(sender, errInfo); 280 | if (sender != null) 281 | { 282 | if (sender.ReportError != true) Log.Info($"[接口访问失败] 机器人配置ReportError!=true,取消推送给发件人"); 283 | else if (sender.Message.CreateTime.AddMinutes(5) < DateTime.Now) Log.Info($"[接口访问失败] 被动可回复时间已超时,取消推送给发件人"); 284 | else 285 | { 286 | _ = Task.Run(async delegate 287 | { 288 | sender.ReportError = false; 289 | await sender.ReplyAsync(string.Join('\n', 290 | "❌接口访问失败", 291 | "接口地址:" + errInfo.Path, 292 | "请求方式:" + errInfo.Method, 293 | "异常代码:" + errInfo.Code, 294 | "异常详情:" + errInfo.Detail, 295 | errInfo.FreezeTime.EndTime > DateTime.Now ? $"接口冻结:暂停使用此接口 {errInfo.FreezeTime.AddTime.Minutes}分{errInfo.FreezeTime.AddTime.Seconds}秒\n解冻时间:{errInfo.FreezeTime.EndTime:yyyy-MM-dd HH:mm:ss}" : null 296 | )); 297 | }); 298 | } 299 | } 300 | else Log.Info($"[接口访问失败] 访问接口的主体不是Sender,取消推送给发件人"); 301 | }); 302 | } 303 | #endregion 304 | 305 | #region Socket客户端配置 306 | /// 307 | /// Socket客户端 308 | /// 309 | private ClientWebSocket WebSocketClient { get; set; } = new(); 310 | /// 311 | /// Socket客户端收到的最新的消息的s,如果是第一次连接,传null 312 | /// 313 | private int WebSocketLastSeq { get; set; } = 1; 314 | /// 315 | /// 此次连接所需要接收的事件 316 | /// 具体可参考 事件订阅 317 | /// 318 | public Intent Intents { get; set; } = SocketEvent.Intents.Public; 319 | /// 320 | /// 会话分片信息 321 | /// 322 | private WebSocketLimit? GateLimit { get; set; } 323 | /// 324 | /// 会话分片id 325 | /// 326 | /// 分片是按照频道id进行哈希的,同一个频道的信息会固定从同一个链接推送。
327 | /// 详见 Shard机制 328 | ///
329 | ///
330 | public int ShardId { get; set; } = 0; 331 | /// 332 | /// Socket客户端存储的SessionId 333 | /// 334 | private string? WebSoketSessionId { get; set; } 335 | /// 336 | /// 断线重连状态标志 337 | /// 338 | private bool IsResume { get; set; } = false; 339 | /// 340 | /// Socket心跳定时器 341 | /// 342 | private System.Timers.Timer HeartBeatTimer { get; set; } = new(); 343 | #endregion 344 | 345 | /// 346 | /// QQ频道机器人 347 | /// 348 | /// 机器人鉴权信息 349 | /// 使用沙箱API 350 | /// 向前端用户反馈API错误 351 | public BotClient(Identity identity, bool sandBoxApi = false, bool reportApiError = false) 352 | { 353 | BotAccessInfo = identity; 354 | ApiOrigin = sandBoxApi ? SandboxApi : ReleaseApi; 355 | ReportApiError = reportApiError; 356 | // 绑定定时器回调,用于定时发送心跳包 357 | HeartBeatTimer.Elapsed += async (sender, e) => await ExcuteCommand(JsonDocument.Parse("{\"op\":" + (int)Opcode.Heartbeat + "}").RootElement); 358 | } 359 | 360 | /// 361 | /// 关闭机器人并释放所有占用的资源 362 | /// 363 | public void Close() 364 | { 365 | WebSocketClient.Abort(); 366 | } 367 | /// 368 | /// 启动机器人 369 | /// RetryCount 连接服务器失败后的重试次数 370 | /// 371 | /// 连接服务器失败后的重试次数 372 | public async void Start(int RetryCount = 3) 373 | { 374 | StartTime = DateTime.Now; 375 | await ConnectAsync(RetryCount); 376 | } 377 | /// 378 | /// 上帝ID 379 | /// 仅用于测试,方便机器人开发者验证功能 380 | /// 381 | public string GodId { get; set; } = "15524401336961673551"; 382 | } 383 | } -------------------------------------------------------------------------------- /QQChannelBot/Bot/BotCommandRegister.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace QQChannelBot.Bot 4 | { 5 | public partial class BotClient 6 | { 7 | /// 8 | /// 自定义指令前缀 9 | /// 10 | /// 当机器人识别到消息的头部包含指令前缀时触发指令识别功能
11 | /// 默认值:"/" 12 | ///
13 | ///
14 | public string CommandPrefix { get; set; } = "/"; 15 | /// 16 | /// 缓存动态注册的消息指令事件 17 | /// 18 | private ConcurrentDictionary Commands { get; } = new(); 19 | /// 20 | /// 添加消息指令 21 | /// 22 | /// 注1:指令匹配忽略消息前的 @机器人 标签,并移除所有前导和尾随空白字符。
23 | /// 注2:被指令命中的消息不会再触发 OnAtMessage 和 OnMsgCreate 事件 24 | ///
25 | ///
26 | /// 指令对象 27 | /// 指令名称重复的处理办法true:替换, false:忽略 28 | /// 29 | public BotClient AddCommand(Command command, bool overwrite = false) 30 | { 31 | string cmdName = command.Name; 32 | if (Commands.ContainsKey(cmdName)) 33 | { 34 | if (overwrite) 35 | { 36 | Log.Warn($"[CommandManager] 指令 {cmdName} 已存在,已替换新注册的功能!"); 37 | Commands[cmdName] = command; 38 | } 39 | else Log.Warn($"[CommandManager] 指令 {cmdName} 已存在,已忽略新功能的注册!"); 40 | } 41 | else 42 | { 43 | Log.Info($"[CommandManager] 指令 {cmdName} 已注册."); 44 | Commands[cmdName] = command; 45 | } 46 | return this; 47 | } 48 | /// 49 | /// 删除消息指令 50 | /// 51 | /// 指令名称 52 | /// 53 | public BotClient DelCommand(string cmdName) 54 | { 55 | if (Commands.ContainsKey(cmdName)) 56 | { 57 | if (Commands.Remove(cmdName, out _)) Log.Info($"[CommandManager] 指令 {cmdName} 已删除."); 58 | else Log.Warn($"[CommandManager] 指令 {cmdName} 删除失败."); 59 | } 60 | else Log.Warn($"[CommandManager] 指令 {cmdName} 不存在!"); 61 | return this; 62 | } 63 | /// 64 | /// 获取所有已注册的指令 65 | /// 66 | public List GetCommands => Commands.Values.ToList(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/BotHttpClient.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http.Headers; 3 | using System.Text.Json; 4 | using System.Text.RegularExpressions; 5 | using QQChannelBot.Bot.StatusCode; 6 | using QQChannelBot.Tools; 7 | 8 | namespace QQChannelBot.Bot 9 | { 10 | /// 11 | /// 时间冻结类 12 | /// 13 | public class FreezeTime 14 | { 15 | /// 16 | /// 结束时间 17 | /// 18 | public DateTime EndTime { get; set; } = DateTime.MinValue; 19 | /// 20 | /// 附加时间 21 | /// 22 | public TimeSpan AddTime { get; set; } = TimeSpan.Zero; 23 | } 24 | /// 25 | /// 经过封装的HttpClient 26 | /// 内置了请求日志功能 27 | /// 28 | public static class BotHttpClient 29 | { 30 | /// 31 | /// URL访问失败的默认冻结时间 32 | /// 33 | public static TimeSpan FreezeAddTime { get; set; } = TimeSpan.FromSeconds(30); 34 | /// 35 | /// URL访问失败的最高冻结时间 36 | /// 37 | public static TimeSpan FreezeMaxTime { get; set; } = TimeSpan.FromHours(1); 38 | /// 39 | /// 临时冻结无权限访问的URL 40 | /// 41 | /// value.Item1 - 解封时间(DateTime) 42 | /// value.Item2 - 再次封禁增加的时间(TimeSpan) 43 | /// 44 | /// 45 | private static readonly Dictionary FreezeUrl = new(); 46 | /// 47 | /// Http客户端 48 | /// 这里设置禁止重定向:AllowAutoRedirect = false 49 | /// 这里设置超时时间为15s 50 | /// 51 | public static HttpClient HttpClient { get; } = new(new HttpLoggingHandler(new HttpClientHandler() { AllowAutoRedirect = false })) { Timeout = TimeSpan.FromSeconds(15) }; 52 | 53 | /// 54 | /// 发起HTTP异步请求 55 | /// 56 | /// 请求消息 57 | /// 请求失败的回调函数 58 | /// 59 | public static async Task SendAsync(HttpRequestMessage request, Action? action = null) 60 | { 61 | string reqUrl = request.RequestUri!.ToString(); 62 | if (FreezeUrl.TryGetValue(reqUrl, out FreezeTime? freezeTime) && freezeTime.EndTime > DateTime.Now) 63 | { 64 | Log.Warn($"[HttpSend] 目标接口处于冻结状态,暂时无法访问:{reqUrl}"); 65 | return null; 66 | } 67 | HttpResponseMessage response = await HttpClient.SendAsync(request).ConfigureAwait(false); 68 | if (response.IsSuccessStatusCode) 69 | { 70 | if (FreezeUrl.ContainsKey(reqUrl)) FreezeUrl.Remove(reqUrl); 71 | return response; 72 | } 73 | if (response.Content.Headers.ContentType?.MediaType == "application/json") 74 | { 75 | string responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 76 | ApiStatus? err = JsonSerializer.Deserialize(responseContent); 77 | if (err?.Code >= 400) 78 | { 79 | if (FreezeUrl.TryGetValue(reqUrl, out freezeTime)) 80 | { 81 | freezeTime.AddTime *= 2; 82 | if (freezeTime.AddTime > FreezeMaxTime) freezeTime.AddTime = FreezeMaxTime; 83 | freezeTime.EndTime = DateTime.Now + freezeTime.AddTime; 84 | } 85 | else 86 | { 87 | freezeTime = new FreezeTime() { AddTime = TimeSpan.FromSeconds(5) }; 88 | // 重点打击11264和11265错误,无接口访问权限;轻微处理其它错误 89 | freezeTime.EndTime = DateTime.Now + ((err?.Code == 11264 || err?.Code == 11265) ? FreezeAddTime : freezeTime.AddTime); 90 | } 91 | } 92 | } 93 | freezeTime ??= new FreezeTime() { EndTime = DateTime.Now, AddTime = TimeSpan.FromSeconds(5) }; 94 | action?.Invoke(response, freezeTime); 95 | return null; 96 | } 97 | 98 | /// 99 | /// HTTP异步GET 100 | /// 101 | /// 请求地址 102 | /// 103 | public static async Task GetAsync(string url) 104 | { 105 | HttpRequestMessage request = new() { RequestUri = new Uri(url), Content = null, Method = HttpMethod.Get }; 106 | return await HttpClient.SendAsync(request).ConfigureAwait(false); 107 | } 108 | 109 | /// 110 | /// HTTP异步Post 111 | /// 112 | /// 请求地址 113 | /// 请求内容 114 | /// 115 | public static async Task PostAsync(string url, HttpContent content) 116 | { 117 | HttpRequestMessage request = new() { RequestUri = new Uri(url), Content = content, Method = HttpMethod.Post }; 118 | return await HttpClient.SendAsync(request).ConfigureAwait(false); 119 | } 120 | 121 | /// 122 | /// HTTP异步Put 123 | /// 124 | /// 请求地址 125 | /// 请求内容 126 | /// 127 | public static async Task PutAsync(string url, HttpContent content) 128 | { 129 | HttpRequestMessage request = new() { RequestUri = new Uri(url), Content = content, Method = HttpMethod.Put }; 130 | return await HttpClient.SendAsync(request).ConfigureAwait(false); 131 | } 132 | 133 | /// 134 | /// HTTP异步Delete 135 | /// 136 | /// 请求地址 137 | /// 请求内容 138 | /// 139 | public static async Task DeleteAsync(string url, HttpContent content) 140 | { 141 | HttpRequestMessage request = new() { RequestUri = new Uri(url), Content = content, Method = HttpMethod.Delete }; 142 | return await HttpClient.SendAsync(request).ConfigureAwait(false); 143 | } 144 | } 145 | /// 146 | /// HttpClient请求拦截器 147 | /// 148 | public class HttpLoggingHandler : DelegatingHandler 149 | { 150 | const int printLength = 1024; 151 | /// 152 | /// HttpClient请求拦截器构造函数 153 | /// 154 | /// 155 | public HttpLoggingHandler(HttpMessageHandler innerHandler) : base(innerHandler) { } 156 | /// 157 | /// 发起异步Http请求 158 | /// 159 | /// 160 | /// 161 | /// 162 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 163 | { 164 | string requestString = Regex.Replace(request.ToString(), @"(?<=Bot\s+)[^\n]+", (m) => Regex.Replace(m.Groups[0].Value, @"[^\.]", "*")); // 敏感信息脱敏 165 | string requestContent = request.Content == null ? "" : await request.Content.ReadAsStringAsync(CancellationToken.None); 166 | MediaTypeHeaderValue? requestContentType = request.Content?.Headers.ContentType; 167 | if (requestContent.Length > printLength) requestContent = requestContent[..printLength]; 168 | if ((requestContentType?.CharSet != null) || (requestContentType?.MediaType == "application/json")) { } 169 | else if (string.IsNullOrWhiteSpace(requestContent)) requestContent = "(没有内容)"; 170 | else requestContent = "(内容无法解码)"; 171 | requestContent = $"[HttpHandler][Request]{Environment.NewLine}{requestString}{Environment.NewLine}{requestContent}"; 172 | 173 | HttpResponseMessage response = await base.SendAsync(request, cancellationToken); 174 | if (cancellationToken.IsCancellationRequested) 175 | { 176 | Log.Error(requestContent + "\n请求已取消!"); 177 | return response; // 请求已取消 178 | } 179 | 180 | string responseString = response.ToString(); 181 | string responseContent = response.Content == null ? "" : await response.Content.ReadAsStringAsync(CancellationToken.None); 182 | HttpStatusCode responseStatusCode = response.StatusCode; 183 | MediaTypeHeaderValue? responseContentType = response.Content?.Headers.ContentType; 184 | 185 | if (responseContent.Length > printLength) responseContent = responseContent[..printLength]; 186 | if ((responseContentType?.CharSet != null) || (responseContentType?.MediaType == "application/json")) { } 187 | else if (string.IsNullOrWhiteSpace(responseContent)) responseContent = "(没有内容)"; 188 | else responseContent = "(内容无法解码)"; 189 | responseContent = $"[HttpHandler][Response]{Environment.NewLine}{responseString}{Environment.NewLine}{responseContent}{Environment.NewLine}"; 190 | if (responseStatusCode < HttpStatusCode.BadRequest) Log.Debug(requestContent + '\n' + responseContent); 191 | else Log.Error(requestContent + '\n' + responseContent); 192 | return response; 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/BotWebSocket.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Net.WebSockets; 3 | using System.Text; 4 | using System.Text.Json; 5 | using System.Text.RegularExpressions; 6 | using QQChannelBot.Bot.SocketEvent; 7 | using QQChannelBot.Models; 8 | 9 | namespace QQChannelBot.Bot 10 | { 11 | public partial class BotClient 12 | { 13 | /// 14 | /// 建立到服务器的连接 15 | /// RetryCount 连接失败后允许的重试次数 16 | /// 17 | /// 连接失败后允许的重试次数 18 | /// 19 | public async Task ConnectAsync(int RetryCount) 20 | { 21 | int retryEndTime = 10; 22 | int retryAddTime = 10; 23 | while (RetryCount-- > 0) 24 | { 25 | try 26 | { 27 | GateLimit = await GetWssUrlWithShared(); 28 | if (Uri.TryCreate(GateLimit?.Url, UriKind.Absolute, out Uri? webSocketUri)) 29 | { 30 | if (WebSocketClient.State != WebSocketState.Open && WebSocketClient.State != WebSocketState.Connecting) 31 | { 32 | await WebSocketClient.ConnectAsync(webSocketUri, CancellationToken.None); 33 | OnWebSocketConnected?.Invoke(this); 34 | _ = ReceiveAsync(); 35 | } 36 | break; 37 | } 38 | Log.Error($"[WebSocket][Connect] 使用网关地址<{GateLimit?.Url}> 建立连接失败!"); 39 | } 40 | catch (Exception e) 41 | { 42 | Log.Error($"[WebSocket][Connect] {e.Message} | Status:{WebSocketClient.CloseStatus} | Description:{WebSocketClient.CloseStatusDescription}"); 43 | } 44 | if (RetryCount > 0) 45 | { 46 | for (int i = retryEndTime; 0 < i; --i) 47 | { 48 | Log.Info($"[WebSocket] {i} 秒后再次尝试连接(剩余重试次数:{RetryCount})..."); 49 | await Task.Delay(TimeSpan.FromSeconds(1)); 50 | } 51 | retryEndTime += retryAddTime; 52 | } 53 | else 54 | { 55 | Log.Error($"[WebSocket] 重连次数已耗尽,无法与频道服务器建立连接!"); 56 | } 57 | } 58 | } 59 | /// 60 | /// 鉴权连接 61 | /// 62 | /// 63 | private async Task SendIdentifyAsync() 64 | { 65 | var data = new 66 | { 67 | op = Opcode.Identify, 68 | d = new 69 | { 70 | token = $"Bot {BotAccessInfo.BotAppId}.{BotAccessInfo.BotToken}", 71 | intents = Intents.GetHashCode(), 72 | shared = new[] { ShardId % (GateLimit?.Shards ?? 1), GateLimit?.Shards ?? 1 } 73 | } 74 | }; 75 | string sendMsg = JsonSerializer.Serialize(data); 76 | Log.Debug("[WebSocket][SendIdentify] " + Regex.Replace(sendMsg, @"(?<=Bot\s+)[^""]+", (m) => Regex.Replace(m.Groups[0].Value, @"[^\.]", "*"))); // 敏感信息脱敏处理 77 | await WebSocketSendAsync(sendMsg, WebSocketMessageType.Text, true); 78 | } 79 | /// 80 | /// 发送心跳 81 | /// 82 | /// 83 | private async Task SendHeartBeatAsync() 84 | { 85 | if (WebSocketClient.State == WebSocketState.Open) 86 | { 87 | string sendMsg = "{\"op\": 1, \"d\":" + WebSocketLastSeq + "}"; 88 | Log.Debug($"[WebSocket][SendHeartbeat] {sendMsg}"); 89 | await WebSocketSendAsync(sendMsg, WebSocketMessageType.Text, true); 90 | } 91 | else Log.Error($"[WebSocket][Heartbeat] 未建立连接!"); 92 | } 93 | /// 94 | /// 恢复连接 95 | /// 96 | /// 97 | private async Task SendResumeAsync() 98 | { 99 | try 100 | { 101 | var data = new 102 | { 103 | op = Opcode.Resume, 104 | d = new 105 | { 106 | token = $"Bot {BotAccessInfo.BotAppId}.{BotAccessInfo.BotToken}", 107 | session_id = WebSoketSessionId, 108 | seq = WebSocketLastSeq 109 | } 110 | }; 111 | string sendMsg = JsonSerializer.Serialize(data); 112 | Log.Debug($"[WebSocket][SendResume] {sendMsg}"); 113 | await WebSocketSendAsync(sendMsg, WebSocketMessageType.Text, true); 114 | } 115 | catch (Exception e) 116 | { 117 | Log.Error($"[WebSocket] Resume Error: {e.Message}"); 118 | } 119 | } 120 | /// 121 | /// WebScoket发送数据到服务端 122 | /// 123 | /// 要发送的数据 124 | /// WebSocket消息类型 125 | /// 表示数据已发送结束 126 | /// 用于传播应取消此操作的通知的取消令牌。 127 | /// 128 | private async Task WebSocketSendAsync(string data, WebSocketMessageType msgType = WebSocketMessageType.Text, bool endOfMsg = true, CancellationToken? cancelToken = null) 129 | { 130 | OnWebSoketSending?.Invoke(this, data); 131 | await WebSocketClient.SendAsync(Encoding.UTF8.GetBytes(data), msgType, endOfMsg, cancelToken ?? CancellationToken.None); 132 | } 133 | /// 134 | /// WebSocket接收服务端数据 135 | /// 136 | /// 137 | private async Task ReceiveAsync() 138 | { 139 | while (WebSocketClient.State == WebSocketState.Open) 140 | { 141 | try 142 | { 143 | using IMemoryOwner memory = MemoryPool.Shared.Rent(1024 * 64); 144 | ValueWebSocketReceiveResult result = await WebSocketClient.ReceiveAsync(memory.Memory, CancellationToken.None); 145 | if (result.MessageType == WebSocketMessageType.Text) 146 | { 147 | JsonElement json = JsonDocument.Parse(memory.Memory[..result.Count]).RootElement; 148 | OnWebSocketReceived?.Invoke(this, json.GetRawText()); 149 | await ExcuteCommand(json); 150 | continue; 151 | } 152 | Log.Info($"[WebSocket][Receive] SocketType:{result.MessageType} | Status:{WebSocketClient.CloseStatus}"); 153 | } 154 | catch (Exception e) 155 | { 156 | Log.Error($"[WebSocket][Receive] {e.Message} | Status:{WebSocketClient.CloseStatus}{Environment.NewLine}"); 157 | } 158 | // 关闭代码4009表示频道服务器要求的重连,可以进行Resume 159 | if (WebSocketClient.CloseStatus.GetHashCode() == 4009) IsResume = true; 160 | WebSocketClient.Abort(); 161 | break; 162 | } 163 | if (HeartBeatTimer.Enabled) HeartBeatTimer.Enabled = false; 164 | OnWebSocketClosed?.Invoke(this); 165 | Log.Warn($"[WebSocket] 重新建立到服务器的连接..."); 166 | await Task.Delay(TimeSpan.FromSeconds(1)); 167 | WebSocketClient = new(); 168 | await ConnectAsync(30); 169 | } 170 | /// 171 | /// 根据收到的数据分析用途 172 | /// 173 | /// Wss接收的数据 174 | /// 175 | private async Task ExcuteCommand(JsonElement wssJson) 176 | { 177 | Opcode opcode = (Opcode)wssJson.GetProperty("op").GetInt32(); 178 | switch (opcode) 179 | { 180 | // Receive 服务端进行消息推送 181 | case Opcode.Dispatch: 182 | OnDispatch?.Invoke(this, wssJson); 183 | WebSocketLastSeq = wssJson.GetProperty("s").GetInt32(); 184 | if (!wssJson.TryGetProperty("t", out JsonElement t) || !wssJson.TryGetProperty("d", out JsonElement d)) 185 | { 186 | Log.Warn($"[WebSocket][Op00][Dispatch] {wssJson.GetRawText()}"); 187 | break; 188 | } 189 | string data = d.GetRawText(); 190 | string? type = t.GetString(); 191 | switch (type) 192 | { 193 | case "DIRECT_MESSAGE_CREATE": // 机器人收到私信事件 194 | case "AT_MESSAGE_CREATE": // 收到 @机器人 消息事件 195 | case "MESSAGE_CREATE": // 频道内有人发言(仅私域) 196 | Message? message = d.Deserialize(); 197 | if (message == null) 198 | { 199 | Log.Warn($"[WebSocket][{type}] {data}"); 200 | return; 201 | } 202 | Log.Debug($"[WebSocket][{type}] {data}"); 203 | _ = MessageCenter(message, type); // 处理消息,不需要等待结果 204 | break; 205 | case "GUILD_CREATE": 206 | case "GUILD_UPDATE": 207 | case "GUILD_DELETE": 208 | /*频道事件*/ 209 | Log.Debug($"[WebSocket][{type}] {data}"); 210 | Guild guild = d.Deserialize()!; 211 | switch (type) 212 | { 213 | case "GUILD_CREATE": 214 | case "GUILD_UPDATE": 215 | guild.APIPermissions = await GetGuildPermissionsAsync(guild.Id); 216 | Guilds[guild.Id] = guild; 217 | break; 218 | case "GUILD_DELETE": 219 | Guilds.Remove(guild.Id, out _); 220 | break; 221 | } 222 | OnGuildMsg?.Invoke(this, guild, type); 223 | break; 224 | case "CHANNEL_CREATE": 225 | case "CHANNEL_UPDATE": 226 | case "CHANNEL_DELETE": 227 | /*子频道事件*/ 228 | Log.Debug($"[WebSocket][{type}] {data}"); 229 | Channel channel = d.Deserialize()!; 230 | OnChannelMsg?.Invoke(this, channel, type); 231 | break; 232 | case "GUILD_MEMBER_ADD": 233 | case "GUILD_MEMBER_UPDATE": 234 | case "GUILD_MEMBER_REMOVE": 235 | /*频道成员事件*/ 236 | Log.Debug($"[WebSocket][{type}] {data}"); 237 | MemberWithGuildID memberWithGuild = d.Deserialize()!; 238 | OnGuildMemberMsg?.Invoke(this, memberWithGuild, type); 239 | break; 240 | case "MESSAGE_REACTION_ADD": 241 | case "MESSAGE_REACTION_REMOVE": 242 | /*表情表态事件*/ 243 | Log.Debug($"[WebSocket][{type}] {data}"); 244 | MessageReaction messageReaction = d.Deserialize()!; 245 | OnMessageReaction?.Invoke(this, messageReaction, type); 246 | break; 247 | case "MESSAGE_AUDIT_PASS": 248 | case "MESSAGE_AUDIT_REJECT": 249 | /*消息审核事件*/ 250 | Log.Info($"[WebSocket][{type}] {data}"); 251 | MessageAudited? messageAudited = d.Deserialize()!; 252 | messageAudited.IsPassed = type == "MESSAGE_AUDIT_PASS"; 253 | OnMessageAudit?.Invoke(this, messageAudited); 254 | break; 255 | case "AUDIO_START": 256 | case "AUDIO_FINISH": 257 | case "AUDIO_ON_MIC": 258 | case "AUDIO_OFF_MIC": 259 | /*音频事件*/ 260 | Log.Info($"[WebSocket][{type}] {data}"); 261 | OnAudioMsg?.Invoke(this, wssJson); 262 | break; 263 | case "RESUMED": 264 | Log.Info($"[WebSocket][Op00][RESUMED] 已恢复与服务器的连接"); 265 | await ExcuteCommand(JsonDocument.Parse("{\"op\":" + (int)Opcode.Heartbeat + "}").RootElement); 266 | OnResumed?.Invoke(this, d); 267 | break; 268 | case "READY": 269 | Log.Debug($"[WebSocket][READY] {data}"); 270 | Log.Info($"[WebSocket][Op00] 服务端 鉴权成功"); 271 | await ExcuteCommand(JsonDocument.Parse("{\"op\":" + (int)Opcode.Heartbeat + "}").RootElement); 272 | WebSoketSessionId = d.GetProperty("session_id").GetString(); 273 | 274 | Log.Info($"[WebSocket][GetGuilds] 获取已加入的频道列表,分页大小:100"); 275 | string? guildNext = null; 276 | for (int page = 1; ; ++page) 277 | { 278 | List? guilds = await GetMeGuildsAsync(guildNext); 279 | if (guilds == null) 280 | { 281 | Log.Info($"[WebSocket][GetGuilds] 获取已加入的频道列表,第 {page:00} 页失败"); 282 | break; 283 | } 284 | if (guilds.Count == 0) 285 | { 286 | Log.Info($"[WebSocket][GetGuilds] 获取已加入的频道列表,第 {page:00} 页为空,操作结束"); 287 | break; 288 | } 289 | Log.Info($"[WebSocket][GetGuilds] 获取已加入的频道列表,第 {page:00} 页成功,数量:{guilds.Count}"); 290 | Parallel.ForEach(guilds, (guild, state, i) => 291 | { 292 | guild.APIPermissions = GetGuildPermissionsAsync(guild.Id).Result; 293 | Guilds[guild.Id] = guild; 294 | }); 295 | guildNext = guilds.Last().Id; 296 | } 297 | Log.Info($"[WebSocket][GetGuilds] 机器人已加入 {Guilds.Count} 个频道"); 298 | 299 | Info = d.GetProperty("user").Deserialize()!; 300 | Info.Avatar = (await GetMeAsync())?.Avatar; 301 | OnReady?.Invoke(Info); 302 | break; 303 | default: 304 | Log.Warn($"[WebSocket][{type}] 未知事件"); 305 | break; 306 | } 307 | break; 308 | // Send&Receive 客户端或服务端发送心跳 309 | case Opcode.Heartbeat: 310 | Log.Debug($"[WebSocket][Op01] {(wssJson.Get("d") == null ? "客户端" : "服务器")} 发送心跳包"); 311 | OnHeartbeat?.Invoke(this, wssJson); 312 | await SendHeartBeatAsync(); 313 | HeartBeatTimer.Enabled = true; 314 | break; 315 | // Send 客户端发送鉴权 316 | case Opcode.Identify: 317 | Log.Info($"[WebSocket][Op02] 客户端 发起鉴权"); 318 | OnIdentify?.Invoke(this, wssJson); 319 | await SendIdentifyAsync(); 320 | break; 321 | // Send 客户端恢复连接 322 | case Opcode.Resume: 323 | Log.Info($"[WebSocket][Op06] 客户端 尝试恢复连接.."); 324 | IsResume = false; 325 | OnResume?.Invoke(this, wssJson); 326 | await SendResumeAsync(); 327 | break; 328 | // Receive 服务端通知客户端重新连接 329 | case Opcode.Reconnect: 330 | Log.Info($"[WebSocket][Op07] 服务器 要求客户端重连"); 331 | OnReconnect?.Invoke(this, wssJson); 332 | break; 333 | // Receive 当identify或resume的时候,如果参数有错,服务端会返回该消息 334 | case Opcode.InvalidSession: 335 | Log.Warn($"[WebSocket][Op09] 客户端鉴权信息错误"); 336 | OnInvalidSession?.Invoke(this, wssJson); 337 | break; 338 | // Receive 当客户端与网关建立ws连接之后,网关下发的第一条消息 339 | case Opcode.Hello: 340 | Log.Info($"[WebSocket][Op10][成功与网关建立连接] {wssJson.GetRawText()}"); 341 | OnHello?.Invoke(this, wssJson); 342 | int heartbeat_interval = wssJson.Get("d")?.Get("heartbeat_interval")?.GetInt32() ?? 30000; 343 | HeartBeatTimer.Interval = heartbeat_interval < 30000 ? heartbeat_interval : 30000; // 设置心跳时间为30s 344 | await ExcuteCommand(JsonDocument.Parse("{\"op\":" + (int)(IsResume ? Opcode.Resume : Opcode.Identify) + "}").RootElement); 345 | break; 346 | // Receive 当发送心跳成功之后,就会收到该消息 347 | case Opcode.HeartbeatACK: 348 | Log.Debug($"[WebSocket][Op11] 服务器 收到心跳包"); 349 | OnHeartbeatACK?.Invoke(this, wssJson); 350 | break; 351 | // 未知操作码 352 | default: 353 | Log.Warn($"[WebSocket][OpNC] 未知操作码: {opcode}"); 354 | break; 355 | } 356 | } 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/Command.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace QQChannelBot.Bot 4 | { 5 | /// 6 | /// 指令对象 7 | /// 提前封装的指令类,用于方便的处理指令 8 | /// 9 | public class Command 10 | { 11 | /// 12 | /// 构造指令对象 13 | /// 14 | /// 匹配指令将调用 Rule 属性进行正则匹配
15 | /// 若rule未赋值,默认为 "^name(?=\s|\n|<@!\d+>|$)" 16 | ///
17 | ///
18 | /// 指令名称 19 | /// 回调函数 20 | /// 匹配指令用的正则表达式默认值:Regex("^name(?=\s|\n|<@!\d+>|$)") 21 | /// 需要管理员权限 22 | /// 备注,用户自定义属性功能用途 23 | public Command(string name, Action? callBack = null, Regex? rule = null, bool needAdmin = false, string? note = null) 24 | { 25 | Name = name; 26 | rule ??= new Regex($@"^{Regex.Escape(name)}\s*(?=\s|\d|\n|<@!\d+>|$)"); 27 | CompiledRule = new Regex(rule.ToString(), rule.Options | RegexOptions.Compiled); 28 | CallBack = callBack; 29 | NeedAdmin = needAdmin; 30 | Note = note; 31 | } 32 | /// 33 | /// 指令名称 34 | /// 35 | public string Name { get; set; } 36 | /// 37 | /// 指令命中后的回调函数 38 | /// 例:(sender, args)=>{} 39 | /// 40 | public Action? CallBack { get; set; } 41 | /// 42 | /// 编译为程序集的正则表达式(可加快正则匹配速度) 43 | /// 44 | private Regex CompiledRule { get; set; } 45 | /// 46 | /// 匹配规则 47 | /// 48 | public Regex Rule 49 | { 50 | get => CompiledRule; 51 | set 52 | { 53 | CompiledRule = new(value.ToString(), value.Options | RegexOptions.Compiled); 54 | } 55 | } 56 | /// 57 | /// 管理员权限 58 | /// 若设定为true,将只有管理员才能触发该指令 59 | /// 60 | public bool NeedAdmin { get; set; } = false; 61 | /// 62 | /// 备注 63 | /// 用户自定义属性功能用途 64 | /// 65 | public string? Note { get; set; } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/Identity.cs: -------------------------------------------------------------------------------- 1 | namespace QQChannelBot.Bot 2 | { 3 | /// 4 | /// 鉴权信息 5 | /// 6 | public struct Identity 7 | { 8 | /// 9 | /// 机器人Id 10 | /// 11 | public string BotAppId { get; set; } 12 | /// 13 | /// 机器人Token 14 | /// 15 | public string BotToken { get; set; } 16 | /// 17 | /// 机器人密钥 18 | /// 19 | public string BotSecret { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/InfoSDK.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace QQChannelBot.Bot 4 | { 5 | /// 6 | /// SDK信息 7 | /// 8 | public static class InfoSDK 9 | { 10 | private static AssemblyName sdk = typeof(BotClient).Assembly.GetName(); 11 | /// 12 | /// SDK名称 13 | /// 14 | public static string? Name => sdk.Name; 15 | /// 16 | /// SDK版本 17 | /// 18 | public static Version? Version => sdk.Version; 19 | /// 20 | /// SDK开源地址HTTPS 21 | /// 22 | public static string GitHTTPS => "https://github\uFEFF.com/Antecer/QQChannelBot"; 23 | /// 24 | /// SDK开源地址SSH 25 | /// 26 | public static string GitSSH => @"git@github.com:Antecer/QQChannelBot.git"; 27 | /// 28 | /// 版权信息 29 | /// 30 | public static string Copyright => "Copyright © 2021 Antecer. All rights reserved."; 31 | } 32 | 33 | public partial class BotClient 34 | { 35 | /// 36 | /// 返回SDK相关信息 37 | /// 38 | /// 框架名称_版本号
39 | /// 代码仓库地址
40 | /// 版权信息
41 | /// 作者夹带的一点私货 42 | ///
43 | ///
44 | public static string SDK => $"{InfoSDK.Name}_{InfoSDK.Version}\n{InfoSDK.GitHTTPS}\n{InfoSDK.Copyright}"; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/Log.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using QQChannelBot.Tools; 3 | 4 | namespace QQChannelBot.Bot 5 | { 6 | /// 7 | /// 集中处理日志信息 8 | /// 9 | public static class Log 10 | { 11 | /// 12 | /// 日志数据结构体 13 | /// 14 | /// 15 | /// 16 | /// 17 | public record struct LogEntry(LogLevel Level, string Message, string TimeStamp); 18 | /// 19 | /// 自定义日志输出 20 | /// 21 | public static event Action? LogTo; 22 | /// 23 | /// 日志记录级别 24 | /// 25 | public static LogLevel LogLevel { get; set; } = LogLevel.INFO; 26 | /// 27 | /// 时间格式化器 28 | /// 定义日志输出的时间戳格式 (默认值 HH:mm:ss.fff) 29 | /// 30 | public static string TimeFormatter { get; set; } = "HH:mm:ss.f"; 31 | 32 | /// 33 | /// 获取格式化的日期标签 34 | /// 35 | private static string TimeStamp { get => $"[{DateTime.Now.ToString(TimeFormatter)}]"; } 36 | /// 37 | /// 日志输出队列 38 | /// 39 | private static readonly ConcurrentQueue LogQueue = new(); 40 | 41 | private static bool IsWorking = false; 42 | /// 43 | /// 打印日志 44 | /// 45 | private static void Print(LogEntry logItem) 46 | { 47 | Task.Run(() => 48 | { 49 | LogQueue.Enqueue(logItem); 50 | if (IsWorking) return; 51 | IsWorking = true; 52 | while (LogQueue.TryDequeue(out LogEntry entry)) 53 | { 54 | if (LogLevel <= entry.Level) 55 | { 56 | Console.ForegroundColor = ConsoleColor.White; 57 | Console.Write($"{entry.TimeStamp}[{entry.Level.ToString()[0]}]"); 58 | Console.ForegroundColor = entry.Level switch 59 | { 60 | LogLevel.DEBUG => ConsoleColor.Gray, 61 | LogLevel.INFO => ConsoleColor.DarkGreen, 62 | LogLevel.WARRNING => ConsoleColor.DarkYellow, 63 | LogLevel.ERROR => ConsoleColor.DarkRed, 64 | _ => ConsoleColor.Magenta, 65 | }; 66 | Console.WriteLine(Unicoder.Decode(entry.Message)); 67 | LogTo?.Invoke(entry); 68 | } 69 | } 70 | IsWorking = false; 71 | }); 72 | } 73 | 74 | /// 75 | /// 打印调试 76 | /// 77 | /// 78 | public static void Debug(string message) => Print(new(LogLevel.DEBUG, message, TimeStamp)); 79 | 80 | /// 81 | /// 打印日志 82 | /// 83 | /// 84 | public static void Info(string message) => Print(new(LogLevel.INFO, message, TimeStamp)); 85 | 86 | /// 87 | /// 打印警告 88 | /// 89 | /// 90 | public static void Warn(string message) => Print(new(LogLevel.WARRNING, message, TimeStamp)); 91 | 92 | /// 93 | /// 打印错误 94 | /// 95 | /// 96 | public static void Error(string message) => Print(new(LogLevel.ERROR, message, TimeStamp)); 97 | } 98 | 99 | /// 100 | /// 日志记录等级 101 | /// 102 | public enum LogLevel 103 | { 104 | /// 105 | /// 调试 106 | /// 107 | DEBUG, 108 | /// 109 | /// 消息 110 | /// 111 | INFO, 112 | /// 113 | /// 警告 114 | /// 115 | WARRNING, 116 | /// 117 | /// 错误 118 | /// 119 | ERROR 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/MessageCenter.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Text.RegularExpressions; 3 | using QQChannelBot.Bot.SocketEvent; 4 | using QQChannelBot.Models; 5 | 6 | namespace QQChannelBot.Bot 7 | { 8 | public partial class BotClient 9 | { 10 | /// 11 | /// 集中处理聊天消息 12 | /// 13 | /// 消息对象 14 | /// 消息类型 15 | /// 16 | /// DIRECT_MESSAGE_CREATE - 私信
17 | /// AT_MESSAGE_CREATE - 频道内 @机器人
18 | /// MESSAGE_CREATE - 频道内任意消息(仅私域支持)
19 | ///
20 | /// 21 | private async Task MessageCenter(Message message, string type) 22 | { 23 | // 记录Sender信息 24 | Sender sender = new(message, this); 25 | // 识别消息类型(私聊,AT全员,AT机器人) 26 | if (message.DirectMessage) sender.MessageType = MessageType.Private; 27 | else if (message.MentionEveryone) sender.MessageType = MessageType.AtAll; 28 | else if (message.Mentions?.Any(user => user.Id == Info.Id) == true) sender.MessageType = MessageType.AtMe; 29 | else if (message.Content.Contains($"@{Info.UserName}")) sender.MessageType = MessageType.AtMe; 30 | // 记录机器人在当前频道下的身份组信息 31 | if ((sender.MessageType != MessageType.Private) && !Members.ContainsKey(message.GuildId)) 32 | { 33 | Members[message.GuildId] = await GetMemberAsync(message.GuildId, Info.Id); 34 | } 35 | // 若已经启用全局消息接收,将不单独响应 AT_MESSAGES 事件,否则会造成重复响应。 36 | if (Intents.HasFlag(Intent.MESSAGE_CREATE) && type.StartsWith("A")) return; 37 | // 调用消息拦截器 38 | if (MessageFilter?.Invoke(sender) == true) return; 39 | // 记录收到的消息 40 | LastMessage(message, true); 41 | // 预判收到的消息 42 | string content = message.Content.Trim(); 43 | if (content.StartsWith(Info.Tag)) content = content.TrimStart(Info.Tag).TrimStart(); 44 | else if (content.StartsWith($"@{Info.UserName}")) content = content.TrimStart($"@{Info.UserName}").TrimStart(); 45 | // 识别指令 46 | bool hasCommand = content.StartsWith(CommandPrefix); 47 | content = content.TrimStart(CommandPrefix).TrimStart(); 48 | if ((hasCommand || (sender.MessageType == MessageType.AtMe) || (sender.MessageType == MessageType.Private)) && (content.Length > 0)) 49 | { 50 | // 输出日志信息 (替换 <@!botid> 为 <@botname>) 51 | string msgContent = Regex.Replace(message.Content, @"<@!\d+>", m => message.Mentions?.Find(user => user.Tag == m.Groups[0].Value)?.UserName.Insert(0, "<@") + ">" ?? m.Value); 52 | string senderMaster = (sender.Bot.Guilds.TryGetValue(sender.GuildId, out var guild) ? guild.Name : null) ?? sender.Author.UserName; 53 | Log.Info($"[{senderMaster}][{message.Author.UserName}] {msgContent.Replace("\xA0", " ")}"); // 替换 \xA0 字符为普通空格 54 | // 并行遍历指令列表,提升效率 55 | Command? selectedCMD = Commands.Values.AsParallel().FirstOrDefault(cmd => 56 | { 57 | Match? cmdMatch = cmd?.Rule.Match(content); 58 | if (cmdMatch?.Success != true) return false; 59 | content = content.TrimStart(cmdMatch.Groups[0].Value).TrimStart(); 60 | return true; 61 | }, null); 62 | if (selectedCMD != null) 63 | { 64 | if (selectedCMD.NeedAdmin && !(message.Member.Roles.Any(r => "24".Contains(r)) || message.Author.Id.Equals(GodId))) 65 | { 66 | if (sender.MessageType == MessageType.AtMe) _ = sender.ReplyAsync($"{message.Author.Tag} 你无权使用该命令!"); 67 | else return; 68 | } 69 | else selectedCMD.CallBack?.Invoke(sender, content); 70 | return; 71 | } 72 | } 73 | 74 | // 触发Message到达事件 75 | OnMsgCreate?.Invoke(sender); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/SocketEvent/Intent.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QQChannelBot.Bot.SocketEvent 4 | { 5 | /// 6 | /// 事件订阅权限 7 | /// 8 | /// 基础事件(默认有订阅权限):GUILDS, GUILD_MEMBERS, AT_MESSAGES, GUILD_MESSAGE_REACTIONS
9 | /// 详见:官方文档 10 | ///
11 | ///
12 | [Flags] 13 | public enum Intent 14 | { 15 | /// 16 | /// 17 | /// GUILD_CREATE - 当机器人加入新guild时
18 | /// GUILD_UPDATE - 当guild资料发生变更时
19 | /// GUILD_DELETE - 当机器人退出guild时
20 | /// CHANNEL_CREATE - 当channel被创建时
21 | /// CHANNEL_UPDATE - 当channel被更新时
22 | /// CHANNEL_DELETE - 当channel被删除时 23 | ///
24 | ///
25 | GUILDS = 1 << 0, 26 | 27 | /// 28 | /// 29 | /// GUILD_MEMBER_ADD - 当成员加入时
30 | /// GUILD_MEMBER_UPDATE - 当成员资料变更时
31 | /// GUILD_MEMBER_REMOVE - 当成员被移除时 32 | ///
33 | ///
34 | GUILD_MEMBERS = 1 << 1, 35 | 36 | /// 37 | /// MESSAGE_CREATE - 频道内发送的所有消息的事件(仅私域可用) 38 | /// 39 | MESSAGE_CREATE = 1 << 9, 40 | 41 | /// 42 | /// 43 | /// MESSAGE_REACTION_ADD - 为消息添加表情表态
44 | /// MESSAGE_REACTION_REMOVE - 为消息删除表情表态 45 | ///
46 | ///
47 | GUILD_MESSAGE_REACTIONS = 1 << 10, 48 | 49 | /// 50 | /// DIRECT_MESSAGE_CREATE - 当收到用户发给机器人的私信消息时 51 | /// 52 | DIRECT_MESSAGE_CREATE = 1 << 12, 53 | 54 | /// 55 | /// MESSAGE_AUDIT_PASS - 消息审核通过 56 | /// MESSAGE_AUDIT_REJECT - 消息审核被拒绝 57 | /// 58 | MESSAGE_AUDIT = 1 << 27, 59 | 60 | /// 61 | /// 62 | /// THREAD_CREATE - 当用户创建主题时
63 | /// THREAD_UPDATE - 当用户更新主题时
64 | /// THREAD_DELETE - 当用户删除主题时
65 | /// POST_CREATE - 当用户创建帖子时
66 | /// POST_DELETE - 当用户删除帖子时
67 | /// REPLY_CREATE - 当用户回复评论时
68 | /// REPLY_DELETE - 当用户删除评论时 69 | ///
70 | ///
71 | FORUM_EVENT = 1 << 28, 72 | 73 | /// 74 | /// 75 | /// AUDIO_START - 音频播放开始时
76 | /// AUDIO_FINISH - 音频播放结束时
77 | /// AUDIO_ON_MIC - 上麦时
78 | /// AUDIO_OFF_MIC - 下麦时 79 | ///
80 | ///
81 | AUDIO_ACTION = 1 << 29, 82 | 83 | /// 84 | /// AT_MESSAGE_CREATE - 当收到@机器人的消息时 85 | /// 86 | AT_MESSAGE_CREATE = 1 << 30, 87 | } 88 | 89 | /// 90 | /// 提供事件订阅的默认配置 91 | /// 92 | public static class Intents 93 | { 94 | /// 95 | /// 公域机器人默认开通的事件 96 | /// 97 | public const Intent Public = Intent.GUILDS 98 | | Intent.GUILD_MEMBERS 99 | | Intent.GUILD_MESSAGE_REACTIONS 100 | | Intent.DIRECT_MESSAGE_CREATE 101 | | Intent.MESSAGE_AUDIT 102 | | Intent.AUDIO_ACTION 103 | | Intent.AT_MESSAGE_CREATE; 104 | /// 105 | /// 私域机器人默认开通的事件 106 | /// 107 | public const Intent Private = Public | Intent.MESSAGE_CREATE; 108 | } 109 | 110 | // 服务器推送的通知类型 111 | //public enum ActionType 112 | //{ 113 | // GUILD_CREATE, 114 | // GUILD_UPDATE, 115 | // GUILD_DELETE, 116 | // CHANNEL_CREATE, 117 | // CHANNEL_UPDATE, 118 | // CHANNEL_DELETE, 119 | // GUILD_MEMBER_ADD, 120 | // GUILD_MEMBER_UPDATE, 121 | // GUILD_MEMBER_REMOVE, 122 | // MESSAGE_REACTION_ADD, 123 | // MESSAGE_REACTION_REMOVE, 124 | // DIRECT_MESSAGE_CREATE, 125 | // AUDIO_START, 126 | // AUDIO_FINISH, 127 | // AUDIO_OFF_MIC, 128 | // AT_MESSAGE_CREATE, 129 | //} 130 | } 131 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/SocketEvent/Opcode.cs: -------------------------------------------------------------------------------- 1 | namespace QQChannelBot.Bot.SocketEvent 2 | { 3 | /// 4 | /// 枚举操作码 5 | /// 6 | public enum Opcode 7 | { 8 | /// 9 | /// 服务端进行消息推送 10 | /// 客户端操作:Receive 11 | /// 12 | Dispatch = 0, 13 | /// 14 | /// 客户端或服务端发送心跳 15 | /// 客户端操作:Send/Receive 16 | /// 17 | Heartbeat = 1, 18 | /// 19 | /// 客户端发送鉴权 20 | /// 客户端操作:Send 21 | /// 22 | Identify = 2, 23 | /// 24 | /// 客户端恢复连接 25 | /// 客户端操作:Send 26 | /// 27 | Resume = 6, 28 | /// 29 | /// 服务端通知客户端重新连接 30 | /// 客户端操作:Receive 31 | /// 32 | Reconnect = 7, 33 | /// 34 | /// 当identify或resume的时候,如果参数有错,服务端会返回该消息 35 | /// 客户端操作:Receive 36 | /// 37 | InvalidSession = 9, 38 | /// 39 | /// 当客户端与网关建立ws连接之后,网关下发的第一条消息 40 | /// 客户端操作:Receive 41 | /// 42 | Hello = 10, 43 | /// 44 | /// 当发送心跳成功之后,就会收到该消息 45 | /// 客户端操作:Receive 46 | /// 47 | HeartbeatACK = 11, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /QQChannelBot/Bot/StatusCode/StatusCodes.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QQChannelBot.Bot.StatusCode 4 | { 5 | /// 6 | /// 状态码对照表 7 | /// 8 | public static class StatusCodes 9 | { 10 | /// 11 | /// API请求状态码对照表 12 | /// 13 | public static readonly Dictionary OpenapiCode = new() 14 | { 15 | //{ 100, "Continue 指示客户端可能继续其请求" }, 16 | //{ 101, "SwitchingProtocols 指示正在更改协议版本或协议" }, 17 | { 200, "OK 请求成功,且请求的信息包含在响应中。" }, 18 | { 201, "Created 请求成功并且服务器创建了新的资源。" }, 19 | { 202, "Accepted 服务器已接受请求,但尚未处理。" }, 20 | { 203, "NonAuthoritativeInformation 返回的元信息来自缓存副本而不是原始服务器,因此可能不正确。" }, 21 | { 204, "NoContent 服务器成功处理了请求,但没有返回任何内容。" }, 22 | { 205, "ResetContent 客户端应重置(或重新加载)当前资源。" }, 23 | { 206, "PartialContent 已部分下载了一个文件,可以续传损坏的下载。" }, 24 | { 300, "MultipleChoices 请求的信息有多种表示形式。" }, 25 | { 301, "MovedPermanently 请求的信息已永久移动到 Location 头中指定的 URI。" }, 26 | { 302, "Redirect 服务器目前使用其它位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。" }, 27 | { 303, "SeeOther 将客户端自动重定向到 Location 头中指定的 URI。" }, 28 | { 304, "NotModified 客户端的缓存副本是最新的。 未传输此资源的内容。" }, 29 | { 305, "UseProxy 请求应使用位于 Location 头中指定的 URI 的代理服务器进行访问。" }, 30 | { 307, "TemporaryRedirect 服务器目前使用其它位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。" }, 31 | { 400, "BadRequest 服务器未能识别请求。" }, 32 | { 401, "Unauthorized 请求的资源要求身份验证。" }, 33 | { 403, "Forbidden 服务器拒绝请求。" }, 34 | { 404, "NotFound 服务器找不到请求的资源。" }, 35 | { 405, "MethodNotAllowed 禁止使用当前指定的方法进行请求。" }, 36 | { 406, "NotAcceptable 无法使用请求的内容特性响应请求。" }, 37 | { 407, "ProxyAuthenticationRequired 表示请求的代理要求身份验证。" }, 38 | { 408, "RequestTimeout 服务器等候请求时发生超时。" }, 39 | { 409, "Conflict 服务器在完成请求时发生冲突。 服务器必须在响应中包含有关冲突的信息。" }, 40 | { 410, "Gone 请求的资源已永久删除。" }, 41 | { 411, "LengthRequired 缺少必需的 Content-length 头。" }, 42 | { 429, "频率限制" }, 43 | { 500, "InternalServerError 服务器遇到错误,无法完成请求。" }, 44 | { 501, "NotImplemented 服务器不支持请求的函数方法。" }, 45 | { 502, "BadGateway 服务器作为网关或代理,从上游服务器收到无效响应。" }, 46 | { 503, "ServiceUnavailable 服务器暂时不可用,通常是由于过载或维护。" }, 47 | { 504, "GatewayTimeout 服务器作为网关或代理,但是没有及时从上游服务器收到请求。" }, 48 | { 505, "HttpVersionNotSupported 服务器不支持请求的HTTP 版本。" }, 49 | { 4001, "无效的 opcode" }, 50 | { 4002, "无效的 payload" }, 51 | { 4006, "无效的 session_id,无法继续 resume,请重新发起 identify" }, 52 | { 4007, "seq 错误" }, 53 | { 4008, "发送 payload 过快,请重新连接,并遵守连接后返回的频控信息" }, 54 | { 4009, "连接过期,请重连" }, 55 | { 4010, "无效的 shard" }, 56 | { 4011, "连接需要处理的 guild 过多,请进行合理的分片" }, 57 | { 4012, "无效的 version" }, 58 | { 4013, "无效的 intent" }, 59 | { 4014, "intent 无权限" }, 60 | { 4900, " 4900~4913 内部错误,请重连" }, 61 | { 4914, "机器人已下架,只允许连接沙箱环境,请断开连接,检验当前连接环境" }, 62 | { 4915, "机器人已封禁,不允许连接,请断开连接,申请解封后再连接" }, 63 | { 10001, "UnknownAccount 账号异常" }, 64 | { 10003, "UnknownChannel 子频道异常" }, 65 | { 10004, "UnknownGuild 频道异常" }, 66 | { 11281, "ErrorCheckAdminFailed 检查是否是管理员失败,系统错误,一般重试一次会好,最多只能重试一次" }, 67 | { 11282, "ErrorCheckAdminNotPass 检查是否是管理员未通过,该接口需要管理员权限,但是用户在添加机器人的时候并未授予该权限,属于逻辑错误,可以提示用户进行授权" }, 68 | { 11251, "ErrorWrongAppid 参数中的 appid 错误,开发者填的 token 错误,appid 无法识别" }, 69 | { 11252, "ErrorCheckAppPrivilegeFailed 检查应用权限失败,系统错误,一般重试一次会好,最多只能重试一次" }, 70 | { 11253, "ErrorCheckAppPrivilegeNotPass 检查应用权限不通过,该机器人应用未获得调用该接口的权限,需要向平台申请" }, 71 | { 11254, "ErrorInterfaceForbidden 应用接口被封禁,该机器人虽然获得了该接口权限,但是被封禁了。" }, 72 | { 11261, "ErrorWrongAppid 参数中缺少 appid,同 11251" }, 73 | { 11262, "ErrorCheckRobot 当前接口不支持使用机器人 Bot Token 调用" }, 74 | { 11263, "ErrorCheckGuildAuth 检查频道权限失败,系统错误,一般重试一次会好,最多只能重试一次" }, 75 | { 11264, "ErrorGuildAuthNotPass 检查小站权限未通过,管理员添加机器人的时候未授予该接口权限,属于逻辑错误,可提示用户进行授权" }, 76 | { 11265, "ErrorRobotHasBaned 机器人已经被封禁" }, 77 | { 11241, "ErrorWrongToken 参数中缺少 token" }, 78 | { 11242, "ErrorCheckTokenFailed 校验 token 失败,系统错误,一般重试一次会好,最多只能重试一次" }, 79 | { 11243, "ErrorCheckTokenNotPass 校验 token 未通过,用户填充的 token 错误,需要开发者进行检查" }, 80 | { 11273, "ErrorCheckUserAuth 检查用户权限失败,当前接口不支持使用 Bearer Token 调用" }, 81 | { 11274, "ErrorUserAuthNotPass 检查用户权限未通过,用户 OAuth 授权时未给与该接口权限,可提示用户重新进行授权" }, 82 | { 11275, "ErrorWrongAppid 无 appid ,同 11251" }, 83 | { 12001, "ReplaceIDFailed 替换 id 失败" }, 84 | { 12002, "RequestInvalid 请求体错误" }, 85 | { 12003, "ResponseInvalid 回包错误" }, 86 | { 20028, "ChannelHitWriteRateLimit 子频道消息触发限频" }, 87 | { 50006, "CannotSendEmptyMessage 消息为空" }, 88 | { 50035, "InvalidFormBody form-data 内容异常" }, 89 | { 50037, "只能发送 markdown 消息" }, 90 | { 50038, "非同频道同子频道" }, 91 | { 50039, "获取消息失败" }, 92 | { 50040, "消息模版类型错误" }, 93 | { 50041, "markdown 有空值" }, 94 | { 50042, "markdown 列表长达最大值" }, 95 | { 50043, "guild_id 转换失败" }, 96 | { 50045, "不能回复机器人自己产生的消息" }, 97 | { 50046, "非 at 机器人消息" }, 98 | { 50047, "非机器人产生的消息 或者 at 机器人消息" }, 99 | { 301000, "参数错误" }, 100 | { 301001, "查询频道信息错误" }, 101 | { 301002, "查询子频道权限错误" }, 102 | { 301003, "修改子频道权限错误" }, 103 | { 301004, "私密子频道关联的人数到达上限" }, 104 | { 301005, "调用 Rpc 服务失败" }, 105 | { 301006, "非群成员没有查询权限" }, 106 | { 301007, "参数超过数量限制" }, 107 | { 302000, "参数错误" }, 108 | { 302001, "查询频道信息错误" }, 109 | { 302002, "查询日程列表失败" }, 110 | { 302003, "查询日程失败" }, 111 | { 302004, "修改日程失败" }, 112 | { 302005, "删除日程失败" }, 113 | { 302006, "创建日程失败" }, 114 | { 302007, "获取创建者信息失败" }, 115 | { 302008, "子频道 ID 不能为空" }, 116 | { 302009, "频道系统错误,请联系客服" }, 117 | { 302010, "暂无修改日程权限" }, 118 | { 302011, "日程活动已被删除" }, 119 | { 302012, "每天只能创建 10 个日程,明天再来吧!" }, 120 | { 302013, "创建日程触发安全打击" }, 121 | { 302014, "日程持续时间超过 7 天,请重新选择" }, 122 | { 302015, "开始时间不能早于当前时间" }, 123 | { 302016, "结束时间不能早于开始时间" }, 124 | { 302017, "Schedule 对象为空" }, 125 | { 302018, "参数类型转换失败" }, 126 | { 302019, "调用下游失败,请联系客服" }, 127 | { 302020, "日程内容违规、账号违规" }, 128 | { 302021, "频道内当日新增活动达上限" }, 129 | { 302022, "不能绑定非当前频道的子频道" }, 130 | { 302023, "开始时跳转不可绑定日程子频道" }, 131 | { 302024, "绑定的子频道不存在" }, 132 | { 502000, "禁言相关错误" }, 133 | { 502001, "频道 id 无效" }, 134 | { 502002, "频道 id 为空" }, 135 | { 502003, "用户 id 无效" }, 136 | { 502004, "用户 id 为空" }, 137 | { 502005, "timestamp 不合法" }, 138 | { 502006, "timestamp 无效" }, 139 | { 502007, "参数转换错误" }, 140 | { 502008, "rpc 调用失败" }, 141 | { 502009, "安全打击" }, 142 | { 502010, "请求头错误" }, 143 | { 304003, "URL_NOT_ALLOWED url 未报备" }, 144 | { 304004, "ARK_NOT_ALLOWED 没有发 ark 消息权限" }, 145 | { 304005, "EMBED_LIMIT embed 长度超限" }, 146 | { 304006, "SERVER_CONFIG 后台配置错误" }, 147 | { 304007, "GET_GUILD 查询频道异常" }, 148 | { 304008, "GET_BOT 查询机器人异常" }, 149 | { 304009, "GET_CHENNAL 查询子频道异常" }, 150 | { 304010, "CHANGE_IMAGE_URL 图片转存错误" }, 151 | { 304011, "NO_TEMPLATE 模板不存在" }, 152 | { 304012, "GET_TEMPLATE 取模板错误" }, 153 | { 304014, "TEMPLATE_PRIVILEGE 没有模板权限" }, 154 | { 304016, "SEND_ERROR 发消息错误" }, 155 | { 304017, "UPLOAD_IMAGE 图片上传错误" }, 156 | { 304018, "SESSION_NOT_EXIST 机器人没连上 gateway" }, 157 | { 304019, "AT_EVERYONE_TIMES @全体成员 次数超限" }, 158 | { 304020, "FILE_SIZE 文件大小超限" }, 159 | { 304021, "GET_FILE 下载文件错误" }, 160 | { 304022, "PUSH_TIME 推送消息时间限制" }, 161 | { 304023, "PUSH_MSG_ASYNC_OK 推送消息异步调用成功, 等待人工审核" }, 162 | { 304024, "REPLY_MSG_ASYNC_OK 回复消息异步调用成功, 等待人工审核" }, 163 | { 304025, "BEAT 消息被打击" }, 164 | { 304026, "MSG_ID 回复的消息 id 错误" }, 165 | { 304027, "MSG_EXPIRE 回复的消息过期" }, 166 | { 304028, "MSG_PROTECT 非 At 当前用户的消息不允许回复" }, 167 | { 304029, "CORPUS_ERROR 调语料服务错误" }, 168 | { 304030, "CORPUS_NOT_MATCH 语料不匹配" }, 169 | { 306001, "param invalid 撤回消息参数错误" }, 170 | { 306002, "msgid error 消息 id 错误" }, 171 | { 306003, "fail to get message 获取消息错误(可重试)" }, 172 | { 306004, "no permission to delete message 没有撤回此消息的权限" }, 173 | { 306005, "retract message error 消息撤回失败(可重试)" }, 174 | { 306006, "fail to get channel 获取子频道失败(可重试)" }, 175 | { 500000, "公告错误" }, 176 | { 501001, "参数校验失败" }, 177 | { 501002, "创建子频道公告失败(可重试)" }, 178 | { 501003, "删除子频道公告失败(可重试)" }, 179 | { 501004, "获取频道信息失败(可重试)" }, 180 | { 501005, "MessageID 错误" }, 181 | { 501006, "创建频道全局公告失败(可重试)" }, 182 | { 501007, "删除频道全局公告失败(可重试)" }, 183 | { 501008, "MessageID 不存在" }, 184 | { 610000, "频道权限错误" }, 185 | { 610001, "获取频道 ID 失败" }, 186 | { 610002, "获取 HTTP 头失败" }, 187 | { 610003, "获取机器人号码失败" }, 188 | { 610004, "获取机器人角色失败" }, 189 | { 610005, "获取机器人角色内部错误" }, 190 | { 610006, "拉取机器人权限列表失败" }, 191 | { 610007, "机器人不在频道内" }, 192 | { 610008, "无效参数" }, 193 | { 610009, "获取 API 接口详情失败" }, 194 | { 610010, "API 接口已授权" }, 195 | { 610011, "获取机器人信息失败" }, 196 | { 610012, "限频失败" }, 197 | { 610013, "已限频" }, 198 | { 610014, "api 授权链接发送失败" }, 199 | { 1000000, "发消息错误" }, 200 | { 1100100, "安全打击:消息被限频" }, 201 | { 1100101, "安全打击:内容涉及敏感,请返回修改" }, 202 | { 1100102, "安全打击:抱歉,暂未获得新功能体验资格" }, 203 | { 1100103, "安全打击" }, 204 | { 1100104, "安全打击:该群已失效或当前群已不存在" }, 205 | { 1100300, "系统内部错误" }, 206 | { 1100301, "调用方不是群成员" }, 207 | { 1100302, "获取指定频道名称失败" }, 208 | { 1100303, "主页频道非管理员不允许发消息" }, 209 | { 1100304, "@次数鉴权失败" }, 210 | { 1100305, "TinyId 转换 Uin 失败" }, 211 | { 1100306, "非私有频道成员" }, 212 | { 1100307, "非白名单应用子频道" }, 213 | { 1100308, "触发频道内限频" }, 214 | { 1100499, "其他错误" }, 215 | }; 216 | } 217 | 218 | /// 219 | /// API请求状态 220 | /// 221 | public class ApiStatus 222 | { 223 | /// 224 | /// 代码 225 | /// 226 | [JsonPropertyName("code")] 227 | public int Code { get; set; } 228 | /// 229 | /// 原因 230 | /// 231 | [JsonPropertyName("message")] 232 | public string? Message { get; set; } 233 | } 234 | 235 | /// 236 | /// API请求出错的关键信息 237 | /// 238 | public class ApiErrorInfo 239 | { 240 | /// 241 | /// API请求出错的关键信息 242 | /// 243 | /// 接口地址 244 | /// 请求方式 245 | /// 错误代码 246 | /// 错误详情 247 | /// 接口被暂时停用的时间 248 | public ApiErrorInfo(string path, string method, int code, string detail, FreezeTime freezeTime) 249 | { 250 | Path = path; 251 | Method = method; 252 | Code = code; 253 | Detail = detail; 254 | FreezeTime = freezeTime; 255 | } 256 | /// 257 | /// 接口地址 258 | /// 259 | public string Path { get; init; } 260 | /// 261 | /// 请求方式 262 | /// 263 | public string Method { get; init; } 264 | /// 265 | /// 错误代码 266 | /// 267 | public int Code { get; set; } 268 | /// 269 | /// 错误信息 270 | /// 271 | public string Detail { get; set; } 272 | /// 273 | /// 接口被暂时停用的时间 274 | /// 275 | public FreezeTime FreezeTime { get; init; } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /QQChannelBot/Models/APIPermissions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QQChannelBot.Models 4 | { 5 | /// 6 | /// 接口权限对象 7 | /// 8 | public class APIPermission 9 | { 10 | /// 11 | /// 接口地址 12 | /// 13 | /// 例:/guilds/{guild_id}/members/{user_id} 14 | /// 15 | /// 16 | [JsonPropertyName("path")] 17 | public string Path { get; set; } = string.Empty; 18 | /// 19 | /// 请求方法,例:GET 20 | /// 21 | [JsonPropertyName("method")] 22 | public string Method { get; set; } = "GET"; 23 | /// 24 | /// API 接口名称,例:获取频道信息 25 | /// 26 | [JsonPropertyName("desc")] 27 | public string Desc { get; set; } = string.Empty; 28 | /// 29 | /// 授权状态 30 | /// 31 | /// 0 - 未授权
32 | /// 1 - 已授权 33 | ///
34 | ///
35 | [JsonPropertyName("auth_status")] 36 | public int AuthStatus { get; set; } = 0; 37 | } 38 | /// 39 | /// 接口权限列表对象 40 | /// 41 | public class APIPermissions 42 | { 43 | /// 44 | /// 接口权限列表 45 | /// 46 | [JsonPropertyName("apis")] 47 | public List? List { get; set; } 48 | } 49 | 50 | /// 51 | /// 接口权限需求对象 52 | /// 53 | public class APIPermissionDemand 54 | { 55 | /// 56 | /// 申请接口权限的频道 id 57 | /// 58 | [JsonPropertyName("guild_id")] 59 | public string? GuildId { get; set; } 60 | /// 61 | /// 接口权限需求授权链接发送的子频道 id 62 | /// 63 | [JsonPropertyName("channel_id")] 64 | public string? ChannelId { get; set; } 65 | /// 66 | /// 权限接口唯一标识 67 | /// 68 | [JsonPropertyName("api_identify")] 69 | public APIPermissionDemandIdentify ApiIdentify { get; set; } 70 | /// 71 | /// 接口权限链接中的接口权限描述信息 72 | /// 73 | [JsonPropertyName("title")] 74 | public string Title { get; set; } = string.Empty; 75 | /// 76 | /// 接口权限链接中的机器人可使用功能的描述信息 77 | /// 78 | [JsonPropertyName("desc")] 79 | public string Desc { get; set; } = string.Empty; 80 | } 81 | /// 82 | /// 接口权限需求标识对象 83 | /// 84 | public record struct APIPermissionDemandIdentify 85 | { 86 | /// 87 | /// 接口地址 88 | /// 89 | /// 例:/guilds/{guild_id}/members/{user_id} 90 | /// 91 | /// 92 | [JsonPropertyName("path")] 93 | public string Path { get; init; } 94 | /// 95 | /// 请求方法,例:GET 96 | /// 97 | [JsonPropertyName("method")] 98 | public string Method { get; init; } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /QQChannelBot/Models/Announces.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QQChannelBot.Models 4 | { 5 | /// 6 | /// 公告对象 7 | /// 8 | public class Announces 9 | { 10 | /// 11 | /// 频道 id 12 | /// 13 | [JsonPropertyName("guild_id")] 14 | public string? GuildId { get; set; } 15 | /// 16 | /// 子频道 id 17 | /// 18 | [JsonPropertyName("channel_id")] 19 | public string? ChannelId { get; set; } 20 | /// 21 | /// 消息 id 22 | /// 23 | [JsonPropertyName("message_id")] 24 | public string? MessageId { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /QQChannelBot/Models/AudioControl.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QQChannelBot.Models 4 | { 5 | /// 6 | /// 音频对象 7 | /// 8 | public class AudioControl 9 | { 10 | /// 11 | /// 构建音频消息 12 | /// 13 | /// 播放状态 14 | /// 音频数据URL 15 | /// 状态文本 16 | public AudioControl(STATUS status, string? audioUrl = null, string? text = null) 17 | { 18 | AudioUrl = audioUrl; 19 | Status = status; 20 | Text = text; 21 | } 22 | /// 23 | /// 音频数据的url status为0时传 24 | /// 25 | [JsonPropertyName("audio_url")] 26 | public string? AudioUrl { get; set; } 27 | /// 28 | /// 状态文本(比如:简单爱-周杰伦),可选,status为0时传,其他操作不传 29 | /// 30 | [JsonPropertyName("text")] 31 | public string? Text { get; set; } 32 | /// 33 | /// 播放状态,参考 STATUS 34 | /// 35 | [JsonPropertyName("status")] 36 | public STATUS Status { get; set; } 37 | } 38 | /// 39 | /// 枚举播放状态 40 | /// 41 | public enum STATUS 42 | { 43 | /// 44 | /// 开始播放操作 45 | /// 46 | START, 47 | /// 48 | /// 暂停播放操作 49 | /// 50 | PAUSE, 51 | /// 52 | /// 继续播放操作 53 | /// 54 | RESUME, 55 | /// 56 | /// 停止播放操作 57 | /// 58 | STOP 59 | } 60 | /// 61 | /// 语音Action 62 | /// 63 | public class AudioAction 64 | { 65 | /// 66 | /// 频道id 67 | /// 68 | [JsonPropertyName("guild_id")] 69 | public string? GuildId { get; set; } 70 | /// 71 | /// 子频道id 72 | /// 73 | [JsonPropertyName("channel_id")] 74 | public string? ChannelId { get; set; } 75 | /// 76 | /// 音频数据的url status为0时传 77 | /// 78 | [JsonPropertyName("audio_url")] 79 | public string? AudioUrl { get; set; } 80 | /// 81 | /// 状态文本(比如:简单爱-周杰伦),可选,status为0时传,其他操作不传 82 | /// 83 | [JsonPropertyName("text")] 84 | public string? Text { set; get; } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /QQChannelBot/Models/Channel.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QQChannelBot.Models 4 | { 5 | /// 6 | /// 子频道对象 7 | /// 8 | public class Channel 9 | { 10 | /// 11 | /// 子频道id 12 | /// 13 | [JsonPropertyName("id")] 14 | public string Id { get; set; } = string.Empty; 15 | /// 16 | /// 频道id 17 | /// 18 | [JsonPropertyName("guild_id")] 19 | public string GuildId { get; set; } = string.Empty; 20 | /// 21 | /// 子频道名称 22 | /// 23 | [JsonPropertyName("name")] 24 | public string Name { get; set; } = string.Empty; 25 | /// 26 | /// 子频道类型 27 | /// 28 | [JsonPropertyName("type")] 29 | public ChannelType Type { get; set; } 30 | /// 31 | /// 子频道子类型 32 | /// 33 | [JsonPropertyName("sub_type")] 34 | public ChannelSubType? SubType { get; set; } 35 | /// 36 | /// 频道位置排序,非必填,但不能够和其他子频道的值重复 37 | /// 38 | [JsonPropertyName("position")] 39 | public int Possition { get; set; } 40 | /// 41 | /// 分组Id 42 | /// 43 | [JsonPropertyName("parent_id")] 44 | public string? ParentId { get; set; } 45 | /// 46 | /// 创建人Id 47 | /// 48 | [JsonPropertyName("owner_id")] 49 | public string? OwerId { get; set; } 50 | /// 51 | /// 子频道私密类型 52 | /// 53 | [JsonPropertyName("private_type")] 54 | public ChannelPrivateType PrivateType { get; set; } 55 | /// 56 | /// 子频道私密类型成员Id 57 | /// 58 | [JsonPropertyName("private_user_ids")] 59 | public List? PrivateUserIds { get; set; } 60 | /// 61 | /// 子频道发言权限 62 | /// 63 | [JsonPropertyName("speak_permission")] 64 | public ChannelSpeakPermission SpeakPermission { get; set; } 65 | /// 66 | /// 用于标识应用子频道应用类型,仅应用子频道时会使用该字段 67 | /// 具体定义请参考 应用子频道的应用类型 68 | /// 69 | [JsonPropertyName("application_id")] 70 | public string? ApplicationId { get; set; } 71 | /// 72 | /// 应用子频道类型列表 73 | /// 74 | public static Dictionary AppType => new() 75 | { 76 | { "1000000", "王者开黑大厅" }, 77 | { "1000001", "互动小游戏" }, 78 | { "1000010", "腾讯投票" }, 79 | { "1000051", "飞车开黑大厅" }, 80 | { "1000050", "日程提醒" }, 81 | { "1000070", "CoDM开黑大厅" }, 82 | { "1010000", "和平精英开黑大厅" }, 83 | }; 84 | /// 85 | /// #频道名 标签 86 | /// 数据内容为:<#ChannelId> 87 | /// 88 | [JsonIgnore] 89 | public string Tag => $"<#{Id}>"; 90 | } 91 | /// 92 | /// 子频道类型 93 | /// 94 | public enum ChannelType 95 | { 96 | /// 97 | /// 文字子频道 98 | /// 99 | 文字 = 0, 100 | /// 101 | /// 保留,不可用 102 | /// 103 | Reserve1 = 1, 104 | /// 105 | /// 语音子频道 106 | /// 107 | 语音 = 2, 108 | /// 109 | /// 保留,不可用 110 | /// 111 | Reserve2 = 3, 112 | /// 113 | /// 子频道分组 114 | /// 115 | 分组 = 4, 116 | /// 117 | /// 直播子频道 118 | /// 119 | 直播 = 10005, 120 | /// 121 | /// 应用子频道 122 | /// 123 | 应用 = 10006, 124 | /// 125 | /// 论坛子频道 126 | /// 127 | 论坛 = 10007 128 | } 129 | /// 130 | /// 子频道子类型(目前只有文字子频道有) 131 | /// 132 | public enum ChannelSubType 133 | { 134 | /// 135 | /// 闲聊 136 | /// 137 | 闲聊, 138 | /// 139 | /// 公告 140 | /// 141 | 公告, 142 | /// 143 | /// 攻略 144 | /// 145 | 攻略, 146 | /// 147 | /// 开黑 148 | /// 149 | 开黑 150 | } 151 | /// 152 | /// 子频道私密类型 153 | /// 154 | public enum ChannelPrivateType 155 | { 156 | /// 157 | /// 公开频道 158 | /// 159 | Public, 160 | /// 161 | /// 群主和管理员可见 162 | /// 163 | Admin, 164 | /// 165 | /// 群主和管理员+指定成员可见 166 | /// 167 | Members 168 | } 169 | /// 170 | /// 子频道发言权限 171 | /// 172 | public enum ChannelSpeakPermission 173 | { 174 | /// 175 | /// 无效类型 176 | /// 177 | Null, 178 | /// 179 | /// 所有人 180 | /// 181 | Everyone, 182 | /// 183 | /// 群主和管理员+指定成员 184 | /// 185 | Members 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /QQChannelBot/Models/ChannelPermissions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QQChannelBot.Models 4 | { 5 | /// 6 | /// 子频道权限对象 7 | /// 8 | public class ChannelPermissions 9 | { 10 | /// 11 | /// 子频道Id 12 | /// 13 | [JsonPropertyName("channel_id")] 14 | public string ChaannelId { get; set; } = ""; 15 | /// 16 | /// 用户Id 17 | /// 此属性和RoleId只会同时存在一个 18 | /// 19 | [JsonPropertyName("user_id")] 20 | public string? UserId { get; set; } 21 | /// 22 | /// 身份组Id 23 | /// 此属性和UserId只会同时存在一个 24 | /// 25 | [JsonPropertyName("role_id")] 26 | public string? RoleId { get; set; } 27 | /// 28 | /// 用户拥有的子频道权限 29 | /// 30 | /// "1" - 查看
31 | /// "2" - 管理
32 | /// "4" - 发言 33 | ///
34 | ///
35 | [JsonPropertyName("permissions"), JsonConverter(typeof(PrivacyTypeToStringNumberConverter))] 36 | public PrivacyType Permissions { get; set; } 37 | } 38 | 39 | /// 40 | /// 子频道私密权限 41 | /// 42 | [Flags] 43 | public enum PrivacyType 44 | { 45 | /// 46 | /// 没有任何权限 47 | /// 48 | 隐藏 = 0, 49 | /// 50 | /// 可查看子频道 51 | /// 52 | 查看 = 1 << 0, 53 | /// 54 | /// 可管理子频道 55 | /// 56 | 管理 = 1 << 1, 57 | /// 58 | /// 可发言子频道 59 | /// 60 | 发言 = 1 << 2 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /QQChannelBot/Models/Emoji.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QQChannelBot.Models 4 | { 5 | /// 6 | /// 表情类型 7 | /// 8 | public enum EmojiType 9 | { 10 | /// 11 | /// 系统表情 12 | /// 13 | System = 1, 14 | /// 15 | /// emoji表情 16 | /// 17 | Emoji = 2, 18 | } 19 | 20 | /// 21 | /// 表情对象 22 | /// 23 | public class Emoji 24 | { 25 | /// 26 | /// 表情ID 27 | /// 28 | /// 系统表情使用数字为ID,emoji使用emoji本身为id,参考 EmojiType 列表 29 | /// 30 | /// 31 | [JsonPropertyName("id")] 32 | public string? Id { get; set; } 33 | /// 34 | /// 表情类型 35 | /// 36 | [JsonPropertyName("type")] 37 | public EmojiType Type { get; set; } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /QQChannelBot/Models/Guild.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QQChannelBot.Models 4 | { 5 | /// 6 | /// 频道对象 7 | /// 8 | public class Guild 9 | { 10 | /// 11 | /// 频道ID 12 | /// 13 | [JsonPropertyName("id")] 14 | public string Id { get; set; } = string.Empty; 15 | /// 16 | /// 频道名称 17 | /// 18 | [JsonPropertyName("name")] 19 | public string? Name { get; set; } 20 | /// 21 | /// 频道头像地址 22 | /// 23 | [JsonPropertyName("icon")] 24 | public string? Icon { get; set; } 25 | /// 26 | /// 频道创建人用户ID 27 | /// 28 | [JsonPropertyName("owner_id")] 29 | public string? OwnerId { get; set; } 30 | /// 31 | /// 当前人是否是频道创建人 32 | /// 33 | [JsonPropertyName("owner")] 34 | public bool Owner { get; set; } 35 | /// 36 | /// 成员数 37 | /// 38 | [JsonPropertyName("member_count")] 39 | public int MemberCount { get; set; } 40 | /// 41 | /// 最大成员数 42 | /// 43 | [JsonPropertyName("max_members")] 44 | public int MaxMembers { get; set; } 45 | /// 46 | /// 频道描述 47 | /// 48 | [JsonPropertyName("description")] 49 | public string? Description { get; set; } 50 | /// 51 | /// 频道创建时间 52 | /// 53 | [JsonPropertyName("joined_at"), JsonConverter(typeof(DateTimeToStringTimestamp))] 54 | public DateTime JoinedAt { get; set; } 55 | /// 56 | /// 机器人在本频道内拥有的权限的列表 57 | /// 58 | [JsonIgnore] 59 | public List? APIPermissions { get; set; } 60 | } 61 | 62 | /// 63 | /// 频道信息 64 | /// 65 | public class GuildInfo : Guild 66 | { 67 | /// 68 | /// 子频道列表 69 | /// 70 | public HashSet Channels { get; set; } = new(); 71 | /// 72 | /// 角色列表 73 | /// 74 | public HashSet Roles { get; set; } = new(); 75 | /// 76 | /// 成员列表 77 | /// 78 | public HashSet Members { get; set; } = new(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /QQChannelBot/Models/JsonConverters.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using System.Text.RegularExpressions; 5 | using QQChannelBot.Bot.SocketEvent; 6 | 7 | namespace QQChannelBot.Models 8 | { 9 | /// 10 | /// JSON序列化时将 bool 转换为 int
11 | /// JSON反序列化时将 int 转换为 bool 12 | ///
13 | public class BoolToInt32Converter : JsonConverter 14 | { 15 | /// 16 | /// 序列化JSON时 bool 转 int 17 | /// 18 | /// 19 | /// 20 | /// 21 | public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) 22 | { 23 | writer.WriteNumberValue(value ? 1 : 0); 24 | } 25 | /// 26 | /// 反序列化JSON时 int 转 bool 27 | /// 28 | /// 29 | /// 30 | /// 31 | /// 32 | public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 33 | { 34 | return reader.TokenType switch 35 | { 36 | JsonTokenType.True => true, 37 | JsonTokenType.False => false, 38 | JsonTokenType.Number => reader.TryGetInt32(out int num) && Convert.ToBoolean(num), 39 | _ => false, 40 | }; 41 | } 42 | } 43 | 44 | /// 45 | /// JSON序列化时将 Color 转换为 UInt32
46 | /// JSON反序列化时将 UInt32 转换为 Color 47 | ///
48 | public class ColorToUint32Converter : JsonConverter 49 | { 50 | /// 51 | /// 序列化JSON时 Color 转 int 52 | /// 53 | /// 54 | /// 55 | /// 56 | public override void Write(Utf8JsonWriter writer, Color value, JsonSerializerOptions options) 57 | { 58 | writer.WriteNumberValue((uint)value.ToArgb()); 59 | } 60 | /// 61 | /// 反序列化JSON时 int 转 Color 62 | /// 63 | /// 64 | /// 65 | /// 66 | /// 67 | public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 68 | { 69 | return reader.TokenType switch 70 | { 71 | JsonTokenType.Number => Color.FromArgb((int)reader.GetUInt32()), 72 | _ => Color.Black, 73 | }; 74 | } 75 | } 76 | 77 | /// 78 | /// JSON序列化时将 RemindType 转换为 StringNumber
79 | /// JSON反序列化时将 StringNumber 转换为 RemindType 80 | ///
81 | public class RemindTypeToStringNumberConverter : JsonConverter 82 | { 83 | /// 84 | /// JSON序列化时将 RemindType 转 StringNumber 85 | /// 86 | /// 87 | /// 88 | /// 89 | public override void Write(Utf8JsonWriter writer, RemindType value, JsonSerializerOptions options) 90 | { 91 | writer.WriteStringValue(value.ToString("D")); 92 | } 93 | /// 94 | /// JSON反序列化时将 StringNumber 转 RemindType 95 | /// 96 | /// 97 | /// 98 | /// 99 | /// 100 | public override RemindType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 101 | { 102 | int numCode = reader.TokenType switch 103 | { 104 | JsonTokenType.String => int.TryParse(reader.GetString(), out int value) ? value : 0, 105 | JsonTokenType.Number => reader.GetInt32(), 106 | _ => 0 107 | }; 108 | return Enum.GetNames(typeof(RemindType)).Length > numCode ? (RemindType)numCode : RemindType.Never; 109 | } 110 | } 111 | 112 | /// 113 | /// JSON序列化时将 PrivacyType 转换为 StringNumber
114 | /// JSON反序列化时将 StringNumber 转换为 PrivacyType 115 | ///
116 | public class PrivacyTypeToStringNumberConverter : JsonConverter 117 | { 118 | /// 119 | /// JSON序列化时将 PrivacyType 转 StringNumber 120 | /// 121 | /// 122 | /// 123 | /// 124 | public override void Write(Utf8JsonWriter writer, PrivacyType value, JsonSerializerOptions options) 125 | { 126 | writer.WriteStringValue(value.ToString("D")); 127 | } 128 | /// 129 | /// JSON反序列化时将 StringNumber 转 PrivacyType 130 | /// 131 | /// 132 | /// 133 | /// 134 | /// 135 | public override PrivacyType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 136 | { 137 | int numCode = reader.TokenType switch 138 | { 139 | JsonTokenType.String => int.TryParse(reader.GetString(), out int value) ? value : 0, 140 | JsonTokenType.Number => reader.GetInt32(), 141 | _ => 0 142 | }; 143 | return (PrivacyType)numCode; 144 | } 145 | } 146 | 147 | /// 148 | /// JSON序列化JSON时将 DateTime 转换为 Timestamp
149 | /// JSON反序列化JSON时将 Timestamp 转换为 DateTime 150 | ///
151 | public class DateTimeToStringTimestamp : JsonConverter 152 | { 153 | /// 154 | /// 序列化JSON时 DateTime 转 Timestamp 155 | /// 156 | /// 157 | /// 158 | /// 159 | public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) 160 | { 161 | writer.WriteStringValue($"{value:yyyy-MM-ddTHH:mm:sszzz}"); 162 | } 163 | /// 164 | /// 反序列化JSON时 Timestamp 转 DateTime 165 | /// 166 | /// 167 | /// 168 | /// 169 | /// 170 | public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 171 | { 172 | switch (reader.TokenType) 173 | { 174 | case JsonTokenType.Number: 175 | return DateTime.MinValue.AddMilliseconds(reader.GetDouble()); 176 | case JsonTokenType.String: 177 | string timeStamp = reader.GetString() ?? "0"; 178 | Match timeMatch = Regex.Match(timeStamp, @"^\d+$"); 179 | if (timeMatch.Success) 180 | { 181 | double offsetVal = double.Parse(timeMatch.Groups[0].Value); 182 | return timeStamp.Length <= 10 ? DateTime.MinValue.AddSeconds(offsetVal) : DateTime.MinValue.AddMilliseconds(offsetVal); 183 | } 184 | else 185 | { 186 | return DateTime.TryParse(timeStamp, out DateTime result) ? result : DateTime.MinValue; 187 | } 188 | default: 189 | return DateTime.MinValue; 190 | } 191 | } 192 | } 193 | 194 | /// 195 | /// JSON序列化时将 Intent 转换为 StringArray
196 | /// JSON反序列化时将 StringArray 转换为 Intent 197 | ///
198 | public class IntentToStringArrayConverter : JsonConverter 199 | { 200 | /// 201 | /// JSON序列化时将 RemindType 转 StringNumber 202 | /// 203 | /// 204 | /// 205 | /// 206 | public override void Write(Utf8JsonWriter writer, Intent value, JsonSerializerOptions options) 207 | { 208 | JsonSerializer.Serialize(writer, value.ToString().Split(',').Select(f => f.Trim()), options); 209 | } 210 | /// 211 | /// JSON反序列化时将 StringNumber 转 RemindType 212 | /// 213 | /// 214 | /// 215 | /// 216 | /// 217 | public override Intent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 218 | { 219 | var intents = JsonSerializer.Deserialize>(ref reader, options)?.Select(s => Enum.Parse(s)); 220 | return intents?.Aggregate((a, b) => a | b) ?? Intents.Public; 221 | } 222 | } 223 | } -------------------------------------------------------------------------------- /QQChannelBot/Models/Member.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QQChannelBot.Models 4 | { 5 | /// 6 | /// 成员 7 | /// 8 | public class Member 9 | { 10 | /// 11 | /// 用户基础信息,来自QQ资料,只有成员相关接口中会填充此信息 12 | /// 13 | [JsonPropertyName("user")] 14 | public User? User { get; set; } 15 | /// 16 | /// 用户在频道内的昵称(默认为空) 17 | /// 18 | [JsonPropertyName("nick")] 19 | public string? Nick { get; set; } 20 | /// 21 | /// 用户在频道内的身份组ID, 默认值可参考DefaultRoles 22 | /// 23 | [JsonPropertyName("roles")] 24 | public List Roles { get; set; } = new() { "1" }; 25 | /// 26 | /// 用户加入频道的时间 ISO8601 timestamp 27 | /// 28 | [JsonPropertyName("joined_at"), JsonConverter(typeof(DateTimeToStringTimestamp))] 29 | public DateTime? JoinedAt { get; set; } 30 | /// 31 | /// 该字段作用未知,等待官方文档更新 32 | /// 33 | [JsonPropertyName("deaf")] 34 | public bool? Deaf { get; set; } 35 | /// 36 | /// 该成员是否被禁言 37 | /// 38 | [JsonPropertyName("mute")] 39 | public bool? Mute { get; set; } 40 | /// 41 | /// 该字段作用未知,等待官方文档更新 42 | /// 43 | [JsonPropertyName("pending")] 44 | public bool? Pending { get; set; } 45 | } 46 | 47 | /// 48 | /// 有频道ID的成员 49 | /// 50 | public class MemberWithGuildID : Member 51 | { 52 | /// 53 | /// 频道id 54 | /// 55 | [JsonPropertyName("guild_id")] 56 | public string GuildId { get; set; } = string.Empty; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /QQChannelBot/Models/Message.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QQChannelBot.Models 4 | { 5 | /// 6 | /// 消息类型枚举 7 | /// 8 | public enum MessageType 9 | { 10 | /// 11 | /// 公共 12 | /// 13 | Public, 14 | /// 15 | /// @机器人 16 | /// 17 | AtMe, 18 | /// 19 | /// @全员 20 | /// 21 | AtAll, 22 | /// 23 | /// 私聊 24 | /// 25 | Private 26 | } 27 | 28 | /// 29 | /// 消息对象 30 | /// 31 | public class Message 32 | { 33 | /// 34 | /// 消息id 35 | /// 36 | [JsonPropertyName("id")] 37 | public string Id { get; set; } = ""; 38 | /// 39 | /// 子频道 id 40 | /// 41 | [JsonPropertyName("channel_id")] 42 | public string ChannelId { get; set; } = ""; 43 | /// 44 | /// 频道 id 45 | /// 46 | [JsonPropertyName("guild_id")] 47 | public string GuildId { get; set; } = ""; 48 | /// 49 | /// 消息内容 50 | /// 51 | [JsonPropertyName("content")] 52 | public string Content { get; set; } = ""; 53 | /// 54 | /// 是否 私聊消息 55 | /// 56 | [JsonPropertyName("direct_message")] 57 | public bool DirectMessage { get; set; } 58 | /// 59 | /// 是否 @全员消息 60 | /// 61 | [JsonPropertyName("mention_everyone")] 62 | public bool MentionEveryone { get; set; } 63 | /// 64 | /// 消息创建时间 65 | /// 66 | [JsonPropertyName("timestamp"), JsonConverter(typeof(DateTimeToStringTimestamp))] 67 | public DateTime CreateTime { get; set; } 68 | /// 69 | /// 消息编辑时间 70 | /// 71 | [JsonPropertyName("edited_timestamp"), JsonConverter(typeof(DateTimeToStringTimestamp))] 72 | public DateTime EditedTime { get; set; } 73 | /// 74 | /// 消息创建者 75 | /// 76 | [JsonPropertyName("author")] 77 | public User Author { get; set; } = new(); 78 | /// 79 | /// 附件(可多个) 80 | /// 81 | [JsonPropertyName("attachments")] 82 | public List? Attachments { get; set; } 83 | /// 84 | /// embed 85 | /// 86 | [JsonPropertyName("embeds")] 87 | public List? Embeds { get; set; } 88 | /// 89 | /// 消息中@的人 90 | /// 91 | [JsonPropertyName("mentions")] 92 | public List? Mentions { get; set; } 93 | /// 94 | /// 消息创建者的member信息 95 | /// 96 | [JsonPropertyName("member")] 97 | public Member Member { get; set; } = new(); 98 | /// 99 | /// ark消息 100 | /// 101 | [JsonPropertyName("ark")] 102 | public MessageArk? Ark { get; set; } 103 | } 104 | 105 | /// 106 | /// 消息体结构 107 | /// 108 | public class MessageToCreate 109 | { 110 | /// 111 | /// 消息内容,文本内容,支持内嵌格式 112 | /// 113 | [JsonPropertyName("content")] 114 | public string? Content { get; set; } 115 | /// 116 | /// embed 消息,一种特殊的 ark 117 | /// 118 | [JsonPropertyName("embed")] 119 | public MessageEmbed? Embed { get; set; } 120 | /// 121 | /// ark 消息 122 | /// 123 | [JsonPropertyName("ark")] 124 | public MessageArk? Ark { get; set; } 125 | /// 126 | /// 引用消息(需要传递被引用的消息Id) 127 | /// 128 | [JsonPropertyName("message_reference")] 129 | public MessageReference? Reference { get; set; } 130 | /// 131 | /// 图片 url 地址 132 | /// 133 | [JsonPropertyName("image")] 134 | public string? Image { get; set; } 135 | /// 136 | /// 要回复的目标消息Id 137 | /// 带了 id 视为被动回复消息,否则视为主动推送消息 138 | /// 139 | [JsonPropertyName("msg_id")] 140 | public string? Id { get; set; } 141 | } 142 | 143 | /// 144 | /// embed消息 145 | /// 146 | public class MessageEmbed 147 | { 148 | /// 149 | /// 标题 150 | /// 151 | [JsonPropertyName("title")] 152 | public string? Title { get; set; } 153 | /// 154 | /// 描述 (见NodeSDK文档) 155 | /// 156 | [JsonPropertyName("description")] 157 | public string? Description { get; set; } 158 | /// 159 | /// 消息弹窗内容 160 | /// 161 | [JsonPropertyName("prompt")] 162 | public string? Prompt { get; set; } 163 | /// 164 | /// 缩略图 165 | /// 166 | [JsonPropertyName("thumbnail")] 167 | public MessageEmbedThumbnail? Thumbnail { get; set; } 168 | /// 169 | /// 消息列表 170 | /// 171 | [JsonPropertyName("fields")] 172 | public List? Fields { get; set; } 173 | } 174 | 175 | /// 176 | /// 引用消息 177 | /// 178 | public class MessageReference 179 | { 180 | /// 181 | /// 需要引用回复的消息 id 182 | /// 183 | [JsonPropertyName("message_id")] 184 | public string? MessageId { get; set; } 185 | /// 186 | /// 是否忽略获取引用消息详情错误,默认否 187 | /// 188 | [JsonPropertyName("ignore_get_message_error")] 189 | public bool IgnoreGetMessageError { get; set; } = false; 190 | } 191 | 192 | /// 193 | /// 缩略图对象 194 | /// 195 | public class MessageEmbedThumbnail 196 | { 197 | /// 198 | /// 图片地址 199 | /// 200 | [JsonPropertyName("url")] 201 | public string? Url { get; set; } 202 | } 203 | 204 | /// 205 | /// Embed行内容 206 | /// 207 | public class MessageEmbedField 208 | { 209 | /// 210 | /// 构造函数 211 | /// 212 | /// 213 | public MessageEmbedField(string? name = null) { Name = name; } 214 | /// 215 | /// 字段名 216 | /// 217 | [JsonPropertyName("name")] 218 | public string? Name { get; set; } 219 | } 220 | 221 | /// 222 | /// 附件对象 223 | /// 224 | public class MessageAttachment 225 | { 226 | /// 227 | /// 附件Id 228 | /// 229 | [JsonPropertyName("id")] 230 | 231 | public string? Id { get; set; } 232 | /// 233 | /// 附件类型 234 | /// 235 | [JsonPropertyName("content_type")] 236 | public string? ContentType { get; set; } 237 | /// 238 | /// 下载地址 239 | /// 240 | [JsonPropertyName("url")] 241 | public string Url { set; get; } = string.Empty; 242 | /// 243 | /// 文件名 244 | /// 245 | [JsonPropertyName("filename")] 246 | public string? FileName { get; set; } 247 | /// 248 | /// 附件大小 249 | /// 250 | [JsonPropertyName("size")] 251 | public long? Size { get; set; } 252 | /// 253 | /// 图片宽度 254 | /// 仅附件为图片时才有 255 | /// 256 | [JsonPropertyName("width")] 257 | public int? Width { get; set; } 258 | /// 259 | /// 图片高度 260 | /// 仅附件为图片时才有 261 | /// 262 | [JsonPropertyName("height")] 263 | public int? Height { get; set; } 264 | } 265 | 266 | /// 267 | /// ark消息 268 | /// 269 | public class MessageArk 270 | { 271 | /// 272 | /// ark模板id(需要先申请) 273 | /// 274 | [JsonPropertyName("template_id")] 275 | public int TemplateId { get; set; } 276 | /// 277 | /// kv值列表 278 | /// 279 | [JsonPropertyName("kv")] 280 | public List Kv { get; set; } = new(); 281 | } 282 | 283 | /// 284 | /// ark的键值对 285 | /// 286 | public class MessageArkKv 287 | { 288 | /// 289 | /// 键 290 | /// 291 | [JsonPropertyName("key")] 292 | public string Key { get; set; } = ""; 293 | /// 294 | /// 值 295 | /// 296 | [JsonPropertyName("value")] 297 | public string? Value { get; set; } 298 | /// 299 | /// ark obj类型的列表 300 | /// 301 | [JsonPropertyName("obj")] 302 | public List? Obj { get; set; } 303 | } 304 | 305 | /// 306 | /// ark obj类型 307 | /// 308 | public class MessageArkObj 309 | { 310 | /// 311 | /// ark objkv列表 312 | /// 313 | [JsonPropertyName("obj_kv")] 314 | public List? ObjKv { get; set; } 315 | } 316 | 317 | /// 318 | /// ark obj键值对 319 | /// 320 | public class MessageArkObjKv 321 | { 322 | /// 323 | /// 构造函数 324 | /// 325 | public MessageArkObjKv() { } 326 | /// 327 | /// ark obj键值对构造函数 328 | /// 329 | /// 330 | /// 331 | public MessageArkObjKv(string key, string value) 332 | { 333 | Key = key; 334 | Value = value; 335 | } 336 | /// 337 | /// 键 338 | /// 339 | [JsonPropertyName("key")] 340 | public string Key { get; set; } = ""; 341 | /// 342 | /// 值 343 | /// 344 | [JsonPropertyName("value")] 345 | public string? Value { get; set; } 346 | } 347 | 348 | /// 349 | /// 拉取消息的操作类型 350 | /// 351 | public enum GetMsgTypesEnum 352 | { 353 | /// 354 | /// 获取目标id前后的消息 355 | /// 356 | around, 357 | /// 358 | /// 获取目标id之前的消息 359 | /// 360 | before, 361 | /// 362 | /// 获取目标id之后的消息 363 | /// 364 | after, 365 | /// 366 | /// 最新limit的消息 367 | /// 368 | latest 369 | } 370 | 371 | /// 372 | /// 消息审核对象 373 | /// 374 | public class MessageAudited 375 | { 376 | /// 377 | /// 消息审核Id 378 | /// 379 | [JsonPropertyName("audit_id")] 380 | public string AuditId { get; set; } = string.Empty; 381 | /// 382 | /// 被审核的消息Id 383 | /// 只有审核通过事件才会有值 384 | /// 385 | [JsonPropertyName("message_id")] 386 | public string? MessageId { get; set; } 387 | /// 388 | /// 频道Id 389 | /// 390 | [JsonPropertyName("guild_id")] 391 | public string GuildId { get; set; } = string.Empty; 392 | /// 393 | /// 子频道Id 394 | /// 395 | [JsonPropertyName("channel_id")] 396 | public string ChannelId { get; set; } = string.Empty; 397 | /// 398 | /// 消息审核时间 399 | /// 400 | [JsonPropertyName("audit_time"), JsonConverter(typeof(DateTimeToStringTimestamp))] 401 | public DateTime AuditTime { get; set; } 402 | /// 403 | /// 消息创建时间 404 | /// 405 | [JsonPropertyName("create_time"), JsonConverter(typeof(DateTimeToStringTimestamp))] 406 | public DateTime CreateTime { get; set; } 407 | /// 408 | /// 扩展属性,用于标注审核是否通过 409 | /// 410 | [JsonIgnore] 411 | public bool IsPassed { get; set; } = false; 412 | } 413 | 414 | /// 415 | /// 私信会话对象(DMS) 416 | /// 417 | public record struct DirectMessageSource 418 | { 419 | /// 420 | /// 私信会话关联的频道Id 421 | /// 422 | [JsonPropertyName("guild_id")] 423 | public string GuildId { get; init; } 424 | /// 425 | /// 私信会话关联的子频道Id 426 | /// 427 | [JsonPropertyName("channel_id")] 428 | public string ChannelId { get; init; } 429 | /// 430 | /// 创建私信会话时间戳 431 | /// 432 | [JsonPropertyName("create_time"), JsonConverter(typeof(DateTimeToStringTimestamp))] 433 | public DateTime CreateTime { get; init; } 434 | } 435 | } -------------------------------------------------------------------------------- /QQChannelBot/Models/MessageReaction.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QQChannelBot.Models 4 | { 5 | /// 6 | /// 表情表态 7 | /// 8 | public class MessageReaction 9 | { 10 | /// 11 | /// 用户Id 12 | /// 13 | [JsonPropertyName("user_id")] 14 | public string UserId { get; set; } = string.Empty; 15 | /// 16 | /// 频道Id 17 | /// 18 | [JsonPropertyName("guild_id")] 19 | public string GuildId { get; set; } = string.Empty; 20 | /// 21 | /// 子频道Id 22 | /// 23 | [JsonPropertyName("channel_id")] 24 | public string? ChannelId { get; set; } 25 | /// 26 | /// 表态对象 27 | /// 28 | [JsonPropertyName("target")] 29 | public ReactionTarget? Target { get; set; } 30 | /// 31 | /// 表态所用表情 32 | /// 33 | [JsonPropertyName("emoji")] 34 | public Emoji? Emoji { get; set; } 35 | } 36 | /// 37 | /// 表态对象 38 | /// 39 | public class ReactionTarget 40 | { 41 | /// 42 | /// 表态对象ID 43 | /// 44 | [JsonPropertyName("id")] 45 | public string? Id { get; set; } 46 | /// 47 | /// 表态对象类型 48 | /// 49 | [JsonPropertyName("type")] 50 | public ReactionTargetType Type { get; set; } 51 | } 52 | /// 53 | /// 表态对象类型 54 | /// 55 | public enum ReactionTargetType 56 | { 57 | /// 58 | /// 消息 59 | /// 60 | 消息, 61 | /// 62 | /// 帖子 63 | /// 64 | 帖子, 65 | /// 66 | /// 评论 67 | /// 68 | 评论, 69 | /// 70 | /// 回复 71 | /// 72 | 回复, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /QQChannelBot/Models/Mute.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace QQChannelBot.Models 5 | { 6 | /// 7 | /// 禁言时间 8 | /// 9 | public class MuteTime 10 | { 11 | /// 12 | /// 禁言时间(默认1分钟) 13 | /// 14 | public MuteTime() 15 | { 16 | MuteSeconds = "60"; 17 | } 18 | /// 19 | /// 禁言指定的时长 20 | /// 21 | /// 禁言多少秒 22 | public MuteTime(int seconds) 23 | { 24 | MuteSeconds = seconds.ToString(); 25 | } 26 | /// 27 | /// 禁言到指定时间 28 | /// 29 | /// 解禁时间戳 30 | /// 31 | /// 格式:"yyyy-MM-dd HH:mm:ss"
32 | /// 示例:"2077-01-01 08:00:00" 33 | ///
34 | public MuteTime(string timestamp) 35 | { 36 | MuteEndTimestamp = new DateTimeOffset(Convert.ToDateTime(timestamp)).ToUnixTimeSeconds().ToString(); 37 | } 38 | /// 39 | /// 禁言到期时间戳,绝对时间戳,单位:秒 40 | /// 41 | [JsonPropertyName("mute_end_timestamp")] 42 | public string? MuteEndTimestamp { get; set; } 43 | /// 44 | /// 禁言多少秒 45 | /// 46 | [JsonPropertyName("mute_seconds")] 47 | public string? MuteSeconds { get; set; } 48 | } 49 | 50 | /// 51 | /// 根据时间字符串构建禁言时间 52 | /// 53 | public class MuteMaker : MuteTime 54 | { 55 | readonly Regex TypeTimeStamp = new(@"(\d{4})[-年](\d\d)[-月](\d\d)[\s日]*(\d\d)[:点时](\d\d)[:分](\d\d)秒?"); 56 | readonly Regex TypeTimeDelay = new(@"(\d+)\s*(年|星期|周|日|天|小?时|分钟?|秒钟?)"); 57 | /// 58 | /// 构造禁言时间 59 | /// 60 | /// 支持以下正则匹配的格式 (优先使用时间戳模式):
61 | /// 时间戳模式 - "^(\d{4})[-年](\d\d)[-月](\d\d)[\s日]*(\d\d)[:点时](\d\d)[:分](\d\d)秒?\s*$"
62 | /// 倒计时模式 - "^(\d+)\s*(年|星期|周|日|天|小?时|分钟?|秒钟?)?\s*$" 63 | ///
64 | ///
65 | public MuteMaker(string timeString = "1分钟") 66 | { 67 | if (string.IsNullOrEmpty(timeString)) return; 68 | Match mts = TypeTimeStamp.Match(timeString); 69 | if (mts.Success) 70 | { 71 | string timstamp = $"{mts.Groups[1].Value}-{mts.Groups[2].Value}-{mts.Groups[3].Value} {mts.Groups[4].Value}:{mts.Groups[5].Value}:{mts.Groups[6].Value}"; 72 | MuteEndTimestamp = new DateTimeOffset(Convert.ToDateTime(timstamp)).ToUnixTimeSeconds().ToString(); 73 | } 74 | else 75 | { 76 | Match mtd = TypeTimeDelay.Match(timeString); 77 | if (mtd.Success) 78 | { 79 | int seconds = mtd.Groups[2].Value switch 80 | { 81 | "年" => 60 * 60 * 24 * 365, 82 | "星期" => 60 * 60 * 24 * 7, 83 | "周" => 60 * 60 * 24 * 7, 84 | "日" => 60 * 60 * 24, 85 | "天" => 60 * 60 * 24, 86 | "小时" => 60 * 60, 87 | "时" => 60 * 60, 88 | "分钟" => 60, 89 | "分" => 60, 90 | _ => 1 91 | }; 92 | if (int.TryParse(mtd.Groups[1].Value, out int timeVal)) seconds *= timeVal; 93 | MuteSeconds = seconds.ToString(); 94 | } 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /QQChannelBot/Models/Role.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace QQChannelBot.Models 5 | { 6 | /// 7 | /// 身分组对象 8 | /// 9 | public class Role 10 | { 11 | /// 12 | /// 身份组ID, 默认值可参考 13 | /// 14 | [JsonPropertyName("id")] 15 | public string Id { get; set; } = string.Empty; 16 | /// 17 | /// 身分组名称 18 | /// 19 | [JsonPropertyName("name")] 20 | public string Name { get; set; } = string.Empty; 21 | /// 22 | /// 身份组颜色 23 | /// 24 | [JsonPropertyName("color"), JsonConverter(typeof(ColorToUint32Converter))] 25 | public Color Color { get; set; } 26 | /// 27 | /// ARGB颜色值的HTML表现形式(如:#FFFFFFFF) 28 | /// 29 | [JsonIgnore] 30 | public string? ColorHtml { get => $"#{Color.ToArgb():X8}"; } 31 | /// 32 | /// 该身分组是否在成员列表中单独展示 33 | /// 34 | [JsonPropertyName("hoist"), JsonConverter(typeof(BoolToInt32Converter))] 35 | public bool Hoist { get; set; } 36 | /// 37 | /// 该身分组的人数 38 | /// 39 | [JsonPropertyName("number")] 40 | public uint Number { get; set; } 41 | /// 42 | /// 成员上限 43 | /// 44 | [JsonPropertyName("member_limit")] 45 | public uint MemberLimit { get; set; } 46 | } 47 | /// 48 | /// 频道身份组列表 49 | /// 50 | public class GuildRoles 51 | { 52 | /// 53 | /// 频道Id 54 | /// 55 | [JsonPropertyName("guild_id")] 56 | public string? GuildId { get; set; } 57 | 58 | /// 59 | /// 身份组 60 | /// 61 | [JsonPropertyName("roles")] 62 | public List? Roles { get; set; } 63 | /// 64 | /// 默认分组上限 65 | /// 66 | [JsonPropertyName("role_num_limit")] 67 | public string? RoleNumLimit { get; set; } 68 | } 69 | /// 70 | /// 新建频道身份组的返回值 71 | /// 72 | public class CreateRoleRes 73 | { 74 | /// 75 | /// 身份组ID 76 | /// 77 | [JsonPropertyName("role_id")] 78 | public string? RoleId { get; set; } 79 | /// 80 | /// 新创建的频道身份组对象 81 | /// 82 | [JsonPropertyName("role")] 83 | public Role? Role { get; set; } 84 | } 85 | /// 86 | /// 修改频道身份组的返回值 87 | /// 88 | public class ModifyRolesRes 89 | { 90 | /// 91 | /// 身份组ID 92 | /// 93 | [JsonPropertyName("guild_id")] 94 | public string? GuildId { get; set; } 95 | /// 96 | /// 身份组ID 97 | /// 98 | [JsonPropertyName("role_id")] 99 | public string? RoleId { get; set; } 100 | /// 101 | /// 新创建的频道身份组对象 102 | /// 103 | [JsonPropertyName("role")] 104 | public Role? Role { get; set; } 105 | } 106 | /// 107 | /// 标识需要设置哪些字段 108 | /// 109 | public class Filter 110 | { 111 | /// 112 | /// 配置筛选器 113 | /// 114 | /// 设置名称 115 | /// 设置颜色 116 | /// 设置在成员列表中单独展示 117 | public Filter(bool setName = false, bool setColor = false, bool setHoist = false) 118 | { 119 | Name = setName; 120 | Color = setColor; 121 | Hoist = setHoist; 122 | } 123 | /// 124 | /// 是否设置名称 125 | /// 126 | [JsonPropertyName("name"), JsonConverter(typeof(BoolToInt32Converter))] 127 | public bool Name { get; set; } 128 | /// 129 | /// 是否设置颜色 130 | /// 131 | [JsonPropertyName("color"), JsonConverter(typeof(BoolToInt32Converter))] 132 | public bool Color { get; set; } 133 | /// 134 | /// 是否设置在成员列表中单独展示 135 | /// 136 | [JsonPropertyName("hoist"), JsonConverter(typeof(BoolToInt32Converter))] 137 | public bool Hoist { get; set; } 138 | } 139 | /// 140 | /// 携带需要设置的字段内容 141 | /// 142 | public class Info 143 | { 144 | /// 145 | /// 构造身份组信息 146 | /// 147 | /// 名称 148 | /// 颜色 149 | /// 在成员列表中单独展示 150 | public Info(string? name = null, Color? color = null, bool? hoist = null) 151 | { 152 | Name = name; 153 | Color = color; 154 | Hoist = hoist; 155 | } 156 | /// 157 | /// 构造身份组信息 158 | /// 159 | /// 名称 160 | /// ARGB颜色值的HTML表现形式(如:#FFFFFFFF) 161 | /// 在成员列表中单独展示 162 | public Info(string? name = null, string? colorHtml = null, bool? hoist = null) 163 | { 164 | Name = name; 165 | ColorHtml = colorHtml; 166 | Hoist = hoist; 167 | } 168 | /// 169 | /// 名称 170 | /// 171 | [JsonPropertyName("name")] 172 | public string? Name { get; set; } 173 | /// 174 | /// 颜色 175 | /// 176 | [JsonPropertyName("color"), JsonConverter(typeof(ColorToUint32Converter))] 177 | public Color? Color { get; set; } 178 | /// 179 | /// ARGB的HTML十六进制颜色值 180 | /// 181 | /// 支持这些格式(忽略大小写和前后空白字符):
182 | /// #FFFFFFFF #FFFFFF #FFFF #FFF 183 | ///
184 | /// 注: 因官方API有BUG,框架暂时强制Alpha通道固定为1.0,对功能无影响。 [2021-12-21] 185 | ///
186 | [JsonIgnore] 187 | public string? ColorHtml 188 | { 189 | get 190 | { 191 | return Color == null ? null : ColorTranslator.ToHtml(Color.Value); 192 | } 193 | set 194 | { 195 | value = value?.TrimStart("0x", "#"); 196 | value = value?.Length switch 197 | { 198 | 4 => $"#{value[1]}{value[1]}{value[2]}{value[2]}{value[3]}{value[3]}", 199 | 5 => $"#{value[1]}{value[1]}{value[2]}{value[2]}{value[3]}{value[3]}{value[4]}{value[4]}", 200 | _ => value 201 | }; 202 | Color = string.IsNullOrWhiteSpace(value) ? null : ColorTranslator.FromHtml(value); 203 | } 204 | } 205 | /// 206 | /// 在成员列表中单独展示 207 | /// 208 | [JsonPropertyName("hoist"), JsonConverter(typeof(BoolToInt32Converter))] 209 | public bool? Hoist { get; set; } 210 | } 211 | /// 212 | /// 系统默认身份组 213 | /// 214 | public static class DefaultRoles 215 | { 216 | /// 217 | /// 获取系统默认身份组名称 218 | /// 219 | /// 220 | /// 221 | public static string? Get(string roleId) 222 | { 223 | return roleId switch 224 | { 225 | "1" => "普通成员", 226 | "2" => "管理员", 227 | "4" => "频道主", 228 | "5" => "子频道管理员", 229 | _ => null 230 | }; 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /QQChannelBot/Models/Schedule.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QQChannelBot.Models 4 | { 5 | /// 6 | /// 日程对象 7 | /// 8 | public class Schedule 9 | { 10 | /// 11 | /// 构造日程 12 | /// 13 | public Schedule() 14 | { 15 | StartTime = DateTime.Now.AddMinutes(10); 16 | EndTime = StartTime.AddHours(1); 17 | } 18 | /// 19 | /// 新建日程 20 | /// 21 | /// 注1:开始时间必须大于当前时间
22 | /// 注2:结束时间必须大于开始时间
23 | /// 注3:调用API每日创建日程数量有限 24 | ///
25 | ///
26 | /// 日程名称 27 | /// 日程描述 28 | /// 开始时间(默认五分钟后) 29 | /// 结束时间(默认持续一小时) 30 | /// 日程开始时跳转的频道 31 | /// 日程时间即将到达时的提醒方式 32 | public Schedule( 33 | string name = "新建日程", 34 | string desc = "新的日程", 35 | DateTime? startTime = null, 36 | DateTime? endTime = null, 37 | Channel? jumpChannel = null, 38 | RemindType remindType = RemindType.Never) 39 | { 40 | Name = name; 41 | Description = desc; 42 | StartTime = startTime ?? DateTime.Now.AddMinutes(10); 43 | EndTime = endTime ?? StartTime.AddHours(1); 44 | JumpChannelId = jumpChannel?.Id; 45 | RemindType = remindType; 46 | } 47 | /// 48 | /// 日程 id 49 | /// 50 | [JsonPropertyName("id")] 51 | public string? Id { get; set; } 52 | /// 53 | /// 日程名称 54 | /// 55 | [JsonPropertyName("name")] 56 | public string? Name { get; set; } 57 | /// 58 | /// 日程描述 59 | /// 60 | [JsonPropertyName("description")] 61 | public string? Description { get; set; } 62 | /// 63 | /// 日程开始时间戳(ms) 64 | /// 必须大于当前时间 65 | /// 66 | [JsonPropertyName("start_timestamp")] 67 | public string StartTimestamp 68 | { 69 | get => new DateTimeOffset(StartTime).ToUnixTimeMilliseconds().ToString(); 70 | set => StartTime = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(value)).DateTime; 71 | } 72 | /// 73 | /// 日程开始时间 74 | /// 必须大于当前时间 75 | /// 76 | [JsonIgnore] 77 | public DateTime StartTime { get; set; } 78 | /// 79 | /// 日程结束时间戳(ms) 80 | /// 必须大于开始时间 81 | /// 82 | [JsonPropertyName("end_timestamp")] 83 | public string EndTimestamp 84 | { 85 | get => new DateTimeOffset(EndTime).ToUnixTimeMilliseconds().ToString(); 86 | set => EndTime = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(value)).DateTime; 87 | } 88 | /// 89 | /// 日程结束时间 90 | /// 必须大于开始时间 91 | /// 92 | [JsonIgnore] 93 | public DateTime EndTime { get; set; } 94 | /// 95 | /// 创建者 96 | /// 97 | [JsonPropertyName("creator")] 98 | public Member? Creator { get; set; } 99 | /// 100 | /// 日程开始时跳转到的子频道 id 101 | /// 102 | [JsonPropertyName("jump_channel_id")] 103 | public string? JumpChannelId { get; set; } 104 | /// 105 | /// 日程提醒类型 106 | /// 107 | [JsonPropertyName("remind_type"), JsonConverter(typeof(RemindTypeToStringNumberConverter))] 108 | public RemindType RemindType { get; set; } 109 | } 110 | 111 | /// 112 | /// 日程提醒方式 113 | /// 114 | public enum RemindType 115 | { 116 | /// 117 | /// 不提醒 118 | /// 119 | Never, 120 | /// 121 | /// 开始时提醒 122 | /// 123 | OnStart, 124 | /// 125 | /// 开始前5分钟提醒 126 | /// 127 | Early5Min, 128 | /// 129 | /// 开始前15分钟提醒 130 | /// 131 | Early15Min, 132 | /// 133 | /// 开始前30分钟提醒 134 | /// 135 | Early30Min, 136 | /// 137 | /// 开始前60分钟提醒 138 | /// 139 | Early60Min 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /QQChannelBot/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QQChannelBot.Models 4 | { 5 | /// 6 | /// 用户 7 | /// 8 | public class User 9 | { 10 | /// 11 | /// 用户 id 12 | /// 13 | [JsonPropertyName("id")] 14 | public string Id { get; set; } = ""; 15 | /// 16 | /// 用户名 17 | /// 18 | [JsonPropertyName("username")] 19 | public string UserName { get; set; } = ""; 20 | /// 21 | /// 用户头像地址 22 | /// 23 | [JsonPropertyName("avatar")] 24 | public string? Avatar { get; set; } 25 | /// 26 | /// 是否机器人 27 | /// 28 | [JsonPropertyName("bot")] 29 | public bool Bot { get; set; } = true; 30 | /// 31 | /// 特殊关联应用的 openid,需要特殊申请并配置后才会返回。如需申请,请联系平台运营人员。 32 | /// 33 | [JsonPropertyName("union_openid")] 34 | public string? UnionOpenid { get; set; } 35 | /// 36 | /// 机器人关联的互联应用的用户信息,与 UnionOpenid 关联的应用是同一个。如需申请,请联系平台运营人员。 37 | /// 38 | [JsonPropertyName("union_user_account")] 39 | public string? UnionUserAccount { get; set; } 40 | /// 41 | /// @用户 标签 42 | /// 数据内容为:<@!UserId> 43 | /// 44 | [JsonIgnore] 45 | public string Tag => $"<@!{Id}>"; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /QQChannelBot/Models/WebSocketLimit.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QQChannelBot.Models 4 | { 5 | /// 6 | /// WebSocket连接分片信息 7 | /// 8 | public class WebSocketLimit 9 | { 10 | /// 11 | /// WebSocket 的连接地址 12 | /// 13 | [JsonPropertyName("url")] 14 | public string? Url { get; set; } 15 | /// 16 | /// 建议的 shard 数 17 | /// 18 | [JsonPropertyName("shards")] 19 | public int Shards { get; set; } = 1; 20 | /// 21 | /// 创建Session限制信息 22 | /// 23 | [JsonPropertyName("session_start_limit")] 24 | public SessionStartLimit? SessionLimit { get; set; } 25 | } 26 | 27 | /// 28 | /// Session开始限制 29 | /// 30 | public class SessionStartLimit 31 | { 32 | /// 33 | /// 每 24 小时可创建 Session 数 34 | /// 35 | [JsonPropertyName("total")] 36 | public int Total { get; set; } 37 | /// 38 | /// 目前还可以创建的 Session 数 39 | /// 每个机器人创建的连接数不能超过 remaining 剩余连接数 40 | /// 41 | [JsonPropertyName("remaining")] 42 | public int Remaining { get; set; } 43 | /// 44 | /// 重置计数的剩余时间(ms) 45 | /// 46 | [JsonPropertyName("reset_after")] 47 | public int ResetAfter { get; set; } 48 | /// 49 | /// 每 5s 可以创建的 Session 数 50 | /// 51 | [JsonPropertyName("max_concurrency")] 52 | public int MaxConcurrency { get; set; } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /QQChannelBot/MsgHelper/MsgArk23.cs: -------------------------------------------------------------------------------- 1 | using QQChannelBot.Models; 2 | 3 | namespace QQChannelBot.MsgHelper 4 | { 5 | /// 6 | /// 模板消息 id=23 7 | /// 链接+文本列表模板 8 | /// 9 | public class MsgArk23 : MessageToCreate 10 | { 11 | /// 12 | /// 构造模板消息 13 | /// 14 | /// 描述 15 | /// 提示 16 | /// 多行内容 17 | /// 要回复的消息id 18 | public MsgArk23( 19 | string? desc = null, 20 | string? prompt = null, 21 | List? msgLines = null, 22 | string? replyMsgId = null 23 | ) 24 | { 25 | Id = replyMsgId; 26 | Desc = desc; 27 | Prompt = prompt; 28 | MsgLines = msgLines ?? new(); 29 | Ark = new() 30 | { 31 | TemplateId = 23, 32 | Kv = new() 33 | { 34 | ArkDesc, 35 | ArkPrompt, 36 | new() { Key = "#LIST#", Obj = MsgLines } 37 | } 38 | }; 39 | } 40 | private readonly MessageArkKv ArkDesc = new() { Key = "#DESC#", Value = null }; 41 | private readonly MessageArkKv ArkPrompt = new() { Key = "#PROMPT#", Value = null }; 42 | 43 | /// 44 | /// 设置要回复的目标消息 45 | /// 46 | /// 目标消息的Id 47 | /// 48 | public MsgArk23 SetReplyMsgId(string? msgId) { Id = msgId; return this; } 49 | /// 50 | /// 描述 51 | /// 52 | public string? Desc { get => ArkDesc.Value; set => ArkDesc.Value = value; } 53 | /// 54 | /// 设置描述 55 | /// 56 | /// 描述内容 57 | /// 58 | public MsgArk23 SetDesc(string? desc) { Desc = desc; return this; } 59 | /// 60 | /// 提示消息 61 | /// 62 | public string? Prompt { get => ArkPrompt.Value; set => ArkPrompt.Value = value; } 63 | /// 64 | /// 设置提示 65 | /// 66 | /// 提示内容 67 | /// 68 | public MsgArk23 SetPrompt(string? prompt) { Prompt = prompt; return this; } 69 | /// 70 | /// 内容列表 71 | /// 72 | public List MsgLines { get; set; } 73 | /// 74 | /// 添加一行内容 75 | /// 76 | /// content - 本行要显示的文字
77 | /// link - 本行文字绑定的超链接(URL需要审核通过才能用) 78 | ///
79 | ///
80 | /// 内容描述 81 | /// 内容链接 [可选] 82 | /// 83 | public MsgArk23 AddLine(string? content, string? link = null) 84 | { 85 | List ojbk = new() 86 | { 87 | new() { Key = "desc", Value = content } 88 | }; 89 | if (link != null) ojbk.Add(new() { Key = "link", Value = link }); 90 | 91 | MsgLines.Add(new() { ObjKv = ojbk }); 92 | return this; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /QQChannelBot/MsgHelper/MsgArk24.cs: -------------------------------------------------------------------------------- 1 | using QQChannelBot.Models; 2 | 3 | namespace QQChannelBot.MsgHelper 4 | { 5 | /// 6 | /// 模板消息 id=24 7 | /// 文本、缩略图模板 8 | /// 9 | public class MsgArk24 : MessageToCreate 10 | { 11 | /// 12 | /// 构造模板消息 13 | /// 14 | /// 描述 15 | /// 提示 16 | /// 标题 17 | /// 详情 18 | /// 图片URL 19 | /// 跳转链接 20 | /// 子标题 21 | /// 要回复的消息id 22 | public MsgArk24( 23 | string? desc = null, 24 | string? prompt = null, 25 | string? title = null, 26 | string? metaDesc = null, 27 | string? image = null, 28 | string? link = null, 29 | string? subTitle = null, 30 | string? replyMsgId = null 31 | ) 32 | { 33 | Id = replyMsgId; 34 | Desc = desc; 35 | Prompt = prompt; 36 | Title = title; 37 | MetaDesc = metaDesc; 38 | Img = image; 39 | Link = link; 40 | SubTitle = subTitle; 41 | Ark = new() 42 | { 43 | TemplateId = 24, 44 | Kv = new() 45 | { 46 | ArkDesc, 47 | ArkPrompt, 48 | ArkTitle, 49 | ArkMetaDesc, 50 | ArkImage, 51 | ArkLink, 52 | ArkSubTitle 53 | } 54 | }; 55 | } 56 | private readonly MessageArkKv ArkDesc = new() { Key = "#DESC#", Value = null }; 57 | private readonly MessageArkKv ArkPrompt = new() { Key = "#PROMPT#", Value = null }; 58 | private readonly MessageArkKv ArkTitle = new() { Key = "#TITLE#", Value = null }; 59 | private readonly MessageArkKv ArkMetaDesc = new() { Key = "#METADESC#", Value = null }; 60 | private readonly MessageArkKv ArkImage = new() { Key = "#IMG#", Value = null }; 61 | private readonly MessageArkKv ArkLink = new() { Key = "#LINK#", Value = null }; 62 | private readonly MessageArkKv ArkSubTitle = new() { Key = "#SUBTITLE#", Value = null }; 63 | 64 | /// 65 | /// 设置要回复的目标消息 66 | /// 67 | /// 目标消息的Id 68 | /// 69 | public MsgArk24 SetReplyMsgId(string? msgId) { Id = msgId; return this; } 70 | /// 71 | /// 描述 72 | /// 73 | public string? Desc { get => ArkDesc.Value; set => ArkDesc.Value = value; } 74 | /// 75 | /// 设置描述 76 | /// 77 | /// 描述内容 78 | /// 79 | public MsgArk24 SetDesc(string? desc) { Desc = desc; return this; } 80 | /// 81 | /// 提示 82 | /// 83 | public string? Prompt { get => ArkPrompt.Value; set => ArkPrompt.Value = value; } 84 | /// 85 | /// 设置提示 86 | /// 87 | /// 提示内容 88 | /// 89 | public MsgArk24 SetPrompt(string? prompt) { Prompt = prompt; return this; } 90 | /// 91 | /// 标题 92 | /// 93 | public string? Title { get => ArkTitle.Value; set => ArkTitle.Value = value; } 94 | /// 95 | /// 设置标题 96 | /// 97 | /// 标题内容 98 | /// 99 | public MsgArk24 SetTitle(string? title) { Title = title; return this; } 100 | /// 101 | /// 详情 102 | /// 103 | public string? MetaDesc { get => ArkMetaDesc.Value; set => ArkMetaDesc.Value = value; } 104 | /// 105 | /// 设置详情 106 | /// 107 | /// 详情内容 108 | /// 109 | public MsgArk24 SetMetaDesc(string? metaDesc) { MetaDesc = metaDesc; return this; } 110 | /// 111 | /// 图片URL 112 | /// 113 | public string? Img { get => ArkImage.Value; set => ArkImage.Value = value; } 114 | /// 115 | /// 设置图片 116 | /// 117 | /// 图片URL 118 | /// 119 | public MsgArk24 SetImage(string? imgLink) { Img = imgLink; return this; } 120 | /// 121 | /// 跳转链接 122 | /// 123 | public string? Link { get => ArkLink.Value; set => ArkLink.Value = value; } 124 | /// 125 | /// 设置链接 126 | /// 127 | /// 跳转链接 128 | /// 129 | public MsgArk24 SetLink(string? link) { Link = link; return this; } 130 | /// 131 | /// 子标题 132 | /// 子标题显示在模板消息底部 133 | /// 134 | public string? SubTitle { get => ArkSubTitle.Value; set => ArkSubTitle.Value = value; } 135 | /// 136 | /// 设置子标题 137 | /// 138 | /// 子标题内容 139 | /// 140 | public MsgArk24 SetSubTitle(string? subTitle) { SubTitle = subTitle; return this; } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /QQChannelBot/MsgHelper/MsgArk34.cs: -------------------------------------------------------------------------------- 1 | using QQChannelBot.Models; 2 | 3 | namespace QQChannelBot.MsgHelper 4 | { 5 | /// 6 | /// 模板消息 id=34 7 | /// 大图模板 8 | /// 9 | public class MsgArk34 : MessageToCreate 10 | { 11 | /// 12 | /// 构造模板消息 13 | /// 14 | /// 描述 15 | /// 提示 16 | /// 标题 17 | /// 详情 18 | /// 小图标URL 19 | /// 大图URL 20 | /// 跳转链接 21 | /// 要回复的消息id 22 | public MsgArk34( 23 | string? desc = null, 24 | string? prompt = null, 25 | string? metaTitle = null, 26 | string? metaDesc = null, 27 | string? metaIcon = null, 28 | string? metaPreview = null, 29 | string? metaUrl = null, 30 | string? replyMsgId = null 31 | ) 32 | { 33 | Id = replyMsgId; 34 | Desc = desc; 35 | Prompt = prompt; 36 | MetaTitle = metaTitle; 37 | MetaDesc = metaDesc; 38 | MetaIcon = metaIcon; 39 | MetaPreview = metaPreview; 40 | MetaUrl = metaUrl; 41 | Ark = new() 42 | { 43 | TemplateId = 34, 44 | Kv = new() 45 | { 46 | ArkDesc, 47 | ArkPrompt, 48 | ArkMetaTitle, 49 | ArkMetaDesc, 50 | ArkMetaIcon, 51 | ArkMetaPreview, 52 | ArkMetaUrl 53 | } 54 | }; 55 | } 56 | private readonly MessageArkKv ArkDesc = new() { Key = "#DESC#", Value = null }; 57 | private readonly MessageArkKv ArkPrompt = new() { Key = "#PROMPT#", Value = null }; 58 | private readonly MessageArkKv ArkMetaTitle = new() { Key = "#METATITLE#", Value = null }; 59 | private readonly MessageArkKv ArkMetaDesc = new() { Key = "#METADESC#", Value = null }; 60 | private readonly MessageArkKv ArkMetaIcon = new() { Key = "#METAICON#", Value = null }; 61 | private readonly MessageArkKv ArkMetaPreview = new() { Key = "#METAPREVIEW#", Value = null }; 62 | private readonly MessageArkKv ArkMetaUrl = new() { Key = "#METAURL#", Value = null }; 63 | 64 | /// 65 | /// 设置要回复的目标消息 66 | /// 67 | /// 目标消息的Id 68 | /// 69 | public MsgArk34 SetReplyMsgId(string? msgId) { Id = msgId; return this; } 70 | /// 71 | /// 描述 72 | /// 73 | public string? Desc { get => ArkDesc.Value; set => ArkDesc.Value = value; } 74 | /// 75 | /// 设置描述 76 | /// 77 | /// 描述内容 78 | /// 79 | public MsgArk34 SetDesc(string? desc) { Desc = desc; return this; } 80 | /// 81 | /// 提示 82 | /// 83 | public string? Prompt { get => ArkPrompt.Value; set => ArkPrompt.Value = value; } 84 | /// 85 | /// 设置提示 86 | /// 87 | /// 提示内容 88 | /// 89 | public MsgArk34 SetPrompt(string? prompt) { Prompt = prompt; return this; } 90 | /// 91 | /// 标题 92 | /// 93 | public string? MetaTitle { get => ArkMetaTitle.Value; set => ArkMetaTitle.Value = value; } 94 | /// 95 | /// 设置标题 96 | /// 97 | /// 标题内容 98 | /// 99 | public MsgArk34 SetMetaTitle(string? metaTitle) { MetaTitle = metaTitle; return this; } 100 | /// 101 | /// 详情 102 | /// 103 | public string? MetaDesc { get => ArkMetaDesc.Value; set => ArkMetaDesc.Value = value; } 104 | /// 105 | /// 设置详情 106 | /// 107 | /// 详情内容 108 | /// 109 | public MsgArk34 SetMetaDesc(string? metaDesc) { MetaDesc = metaDesc; return this; } 110 | /// 111 | /// 小图标URL 112 | /// 113 | public string? MetaIcon { get => ArkMetaIcon.Value; set => ArkMetaIcon.Value = value; } 114 | /// 115 | /// 设置小图标 116 | /// 117 | /// 小图标URL 118 | /// 119 | public MsgArk34 SetMetaIcon(string? iconLink) { MetaIcon = iconLink; return this; } 120 | /// 121 | /// 大图URL 122 | /// 123 | public string? MetaPreview { get => ArkMetaPreview.Value; set => ArkMetaPreview.Value = value; } 124 | /// 125 | /// 设置大图 126 | /// 127 | /// 大图URL 128 | /// 129 | public MsgArk34 SetMetaPreview(string? metaPreview) { MetaPreview = metaPreview; return this; } 130 | /// 131 | /// 跳转链接 132 | /// 133 | public string? MetaUrl { get => ArkMetaUrl.Value; set => ArkMetaUrl.Value = value; } 134 | /// 135 | /// 设置跳转链接 136 | /// 137 | /// 跳转链接 138 | /// 139 | public MsgArk34 SetMetaUrl(string? metaUrl) { MetaUrl = metaUrl; return this; } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /QQChannelBot/MsgHelper/MsgArk37.cs: -------------------------------------------------------------------------------- 1 | using QQChannelBot.Models; 2 | 3 | namespace QQChannelBot.MsgHelper 4 | { 5 | /// 6 | /// 模板消息 id=37 7 | /// 8 | /// 大图模板
9 | /// 尺寸为 975*540 10 | ///
11 | ///
12 | public class MsgArk37 : MessageToCreate 13 | { 14 | /// 15 | /// 构造模板消息 16 | /// 17 | /// 描述 18 | /// 标题 19 | /// 子标题 20 | /// 大图URL 21 | /// 跳转链接 22 | /// 要回复的消息id 23 | public MsgArk37( 24 | string? prompt = null, 25 | string? metaTitle = null, 26 | string? metaSubTitle = null, 27 | string? metaCover = null, 28 | string? metaUrl = null, 29 | string? replyMsgId = null 30 | ) 31 | { 32 | Id = replyMsgId; 33 | Prompt = prompt; 34 | MetaTitle = metaTitle; 35 | MetaSubTitle = metaSubTitle; 36 | MetaCover = metaCover; 37 | MetaUrl = metaUrl; 38 | Ark = new() 39 | { 40 | TemplateId = 37, 41 | Kv = new() 42 | { 43 | ArkPrompt, 44 | ArkMetaTitle, 45 | ArkMetaSubTitle, 46 | ArkMetaCover, 47 | ArkMetaUrl 48 | } 49 | }; 50 | } 51 | private readonly MessageArkKv ArkPrompt = new() { Key = "#PROMPT#", Value = null }; 52 | private readonly MessageArkKv ArkMetaTitle = new() { Key = "#METATITLE#", Value = null }; 53 | private readonly MessageArkKv ArkMetaSubTitle = new() { Key = "#METASUBTITLE#", Value = null }; 54 | private readonly MessageArkKv ArkMetaCover = new() { Key = "#METACOVER#", Value = null }; 55 | private readonly MessageArkKv ArkMetaUrl = new() { Key = "#METAURL#", Value = null }; 56 | 57 | /// 58 | /// 设置要回复的目标消息 59 | /// 60 | /// 目标消息的Id 61 | /// 62 | public MsgArk37 SetReplyMsgId(string? msgId) { Id = msgId; return this; } 63 | /// 64 | /// 提示 65 | /// 66 | public string? Prompt { get => ArkPrompt.Value; set => ArkPrompt.Value = value; } 67 | /// 68 | /// 设置提示 69 | /// 70 | /// 提示内容 71 | /// 72 | public MsgArk37 SetPrompt(string? prompt) { Prompt = prompt; return this; } 73 | /// 74 | /// 标题 75 | /// 76 | public string? MetaTitle { get => ArkMetaTitle.Value; set => ArkMetaTitle.Value = value; } 77 | /// 78 | /// 设置标题 79 | /// 80 | /// 标题内容 81 | /// 82 | public MsgArk37 SetMetaTitle(string? metaTitle) { MetaTitle = metaTitle; return this; } 83 | /// 84 | /// 子标题 85 | /// 86 | public string? MetaSubTitle { get => ArkMetaSubTitle.Value; set => ArkMetaSubTitle.Value = value; } 87 | /// 88 | /// 设置子标题 89 | /// 90 | /// 子标题内容 91 | /// 92 | public MsgArk37 SetMetaSubTitle(string? subTitle) { MetaSubTitle = subTitle; return this; } 93 | 94 | /// 95 | /// 大图URL 96 | /// 97 | public string? MetaCover { get => ArkMetaCover.Value; set => ArkMetaCover.Value = value; } 98 | /// 99 | /// 设置大图 100 | /// 101 | /// 大图URL 102 | /// 103 | public MsgArk37 SetMetaCover(string? cover) { MetaCover = cover; return this; } 104 | /// 105 | /// 跳转链接 106 | /// 107 | public string? MetaUrl { get => ArkMetaUrl.Value; set => ArkMetaUrl.Value = value; } 108 | /// 109 | /// 设置跳转链接 110 | /// 111 | /// 跳转链接 112 | /// 113 | public MsgArk37 SetMetaUrl(string? metaUrl) { MetaUrl = metaUrl; return this; } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /QQChannelBot/MsgHelper/MsgDescImg.cs: -------------------------------------------------------------------------------- 1 | using QQChannelBot.Models; 2 | 3 | namespace QQChannelBot.MsgHelper 4 | { 5 | /// 6 | /// 图片消息对象 7 | /// 8 | public class MsgDescImg : MessageToCreate 9 | { 10 | /// 11 | /// 构建文字和图片同时存在的消息 12 | /// 13 | /// 消息内容 14 | /// 图片URL 15 | /// 要回复的消息id 16 | public MsgDescImg( 17 | string? content = null, 18 | string? image = null, 19 | string? replyMsgId = null 20 | ) 21 | { 22 | Id = replyMsgId; 23 | Image = image; 24 | Content = content; 25 | } 26 | /// 27 | /// 设置要回复的目标消息 28 | /// 29 | /// 目标消息的Id 30 | /// 31 | public MsgDescImg SetReplyMsgId(string? msgId) { Id = msgId; return this; } 32 | /// 33 | /// 设置消息内容 34 | /// 35 | /// 文字内容 36 | /// 37 | public MsgDescImg SetContent(string? content) { Content = content; return this; } 38 | /// 39 | /// 设置图片URL 40 | /// 41 | /// 图片URL 42 | /// 43 | public MsgDescImg SetImage(string? image) { Image = image; return this; } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /QQChannelBot/MsgHelper/MsgEmbed.cs: -------------------------------------------------------------------------------- 1 | using QQChannelBot.Models; 2 | 3 | namespace QQChannelBot.MsgHelper 4 | { 5 | /// 6 | /// Embed消息对象 7 | /// 8 | public class MsgEmbed : MessageToCreate 9 | { 10 | /// 11 | /// 构建Embed消息 12 | /// 详情查阅QQ机器人文档 embed消息 13 | /// 14 | /// 标题 15 | /// 提示 16 | /// 缩略图URL 17 | /// 内容列表 18 | /// 要回复的消息id 19 | public MsgEmbed( 20 | string? title = null, 21 | string? prompt = null, 22 | string? thumbnail = null, 23 | List? embedFields = null, 24 | string? replyMsgId = null 25 | ) 26 | { 27 | Id = replyMsgId; 28 | EmbedFields = embedFields ?? new(); 29 | EmbedClass = new MessageEmbed() 30 | { 31 | Thumbnail = EmbedThumbnail, 32 | Fields = EmbedFields 33 | }; 34 | Title = title; 35 | Prompt = prompt; 36 | Thumbnail = thumbnail; 37 | Embed = EmbedClass; 38 | } 39 | private MessageEmbedThumbnail EmbedThumbnail { get; set; } = new(); 40 | private MessageEmbed EmbedClass { get; set; } 41 | 42 | /// 43 | /// 设置要回复的目标消息 44 | /// 45 | /// 目标消息的Id 46 | /// 47 | public MsgEmbed SetReplyMsgId(string? msgId) { Id = msgId; return this; } 48 | /// 49 | /// 标题 50 | /// 51 | public string? Title { get => EmbedClass.Title; set => EmbedClass.Title = value; } 52 | /// 53 | /// 设置标题 54 | /// 55 | /// 标题内容 56 | /// 57 | public MsgEmbed SetTitle(string? title) { Title = title; return this; } 58 | /// 59 | /// 提示 60 | /// 61 | public string? Prompt { get => EmbedClass.Prompt; set => EmbedClass.Prompt = value; } 62 | /// 63 | /// 设置提示 64 | /// 65 | /// 提示内容 66 | /// 67 | public MsgEmbed SetPrompt(string? prompt) { Prompt = prompt; return this; } 68 | /// 69 | /// 缩略图URL 70 | /// 71 | public string? Thumbnail { get => EmbedThumbnail.Url; set => EmbedThumbnail.Url = value; } 72 | /// 73 | /// 设置缩略图 74 | /// 75 | /// 缩略图URL 76 | /// 77 | public MsgEmbed SetThumbnail(string? thumbnail) { Thumbnail = thumbnail; return this; } 78 | /// 79 | /// 消息列表 80 | /// 81 | public List EmbedFields { get; set; } 82 | /// 83 | /// 添加一行内容 84 | /// 85 | /// 行内容 86 | /// 87 | public MsgEmbed AddLine(string? content) { EmbedFields.Add(new(content)); return this; } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /QQChannelBot/MsgHelper/MsgImage.cs: -------------------------------------------------------------------------------- 1 | using QQChannelBot.Models; 2 | 3 | namespace QQChannelBot.MsgHelper 4 | { 5 | /// 6 | /// 图片消息 7 | /// 8 | public class MsgImage : MessageToCreate 9 | { 10 | /// 11 | /// 构建图片消息 12 | /// 13 | /// 图片URL 14 | /// 要回复的消息id 15 | public MsgImage( 16 | string? image = null, 17 | string? replyMsgId = null 18 | ) 19 | { 20 | Id = replyMsgId; 21 | Image = image; 22 | } 23 | /// 24 | /// 设置要回复的目标消息 25 | /// 26 | /// 目标消息的Id 27 | /// 28 | public MsgImage SetReplyMsgId(string? msgId) { Id = msgId; return this; } 29 | /// 30 | /// 设置图片网址 31 | /// 32 | /// 图片URL 33 | /// 34 | public MsgImage SetImage(string? image) { Image = image; return this; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /QQChannelBot/MsgHelper/MsgTag.cs: -------------------------------------------------------------------------------- 1 | using QQChannelBot.Models; 2 | 3 | namespace QQChannelBot.MsgHelper 4 | { 5 | /// 6 | /// 消息内嵌标签 7 | /// 仅作用于content中 8 | /// 9 | public static class MsgTag 10 | { 11 | /// 12 | /// 创建 @用户 标签 13 | /// 14 | /// 用户id 15 | /// 16 | public static string User(string userId) 17 | { 18 | return $"<@!{userId}>"; 19 | } 20 | /// 21 | /// 创建 #子频道 标签 22 | /// 23 | /// 子频道id 24 | /// 25 | public static string Channel(string channelId) 26 | { 27 | return $"<#{channelId}>"; 28 | } 29 | 30 | /// 31 | /// 创建 @用户 标签 32 | /// 33 | /// 用户对象 34 | /// 35 | public static string Tag(this User user) => User(user.Id); 36 | 37 | /// 38 | /// 创建 #子频道 标签 39 | /// 40 | /// 子频道对象 41 | /// 42 | public static string Tag(this Channel channel) => Channel(channel.Id); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /QQChannelBot/MsgHelper/MsgText.cs: -------------------------------------------------------------------------------- 1 | using QQChannelBot.Models; 2 | 3 | namespace QQChannelBot.MsgHelper 4 | { 5 | /// 6 | /// 文字消息 7 | /// 8 | public class MsgText : MessageToCreate 9 | { 10 | /// 11 | /// 构建文字消息 12 | /// 13 | /// 消息内容 14 | /// 要回复的消息Id 15 | public MsgText( 16 | string? content = null, 17 | string? replyMsgId = null 18 | ) 19 | { 20 | Id = replyMsgId; 21 | Content = content; 22 | } 23 | /// 24 | /// 设置要回复的目标消息 25 | /// 26 | /// 目标消息的Id 27 | /// 28 | public MsgText SetReplyMsgId(string? msgId) { Id = msgId; return this; } 29 | /// 30 | /// 设置消息内容 31 | /// 32 | /// 文字内容 33 | /// 34 | public MsgText SetContent(string? content) { Content = content; return this; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /QQChannelBot/QQChannelBot.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 1.6.3 8 | QQ机器人框架 9 | Copyright © 2021 Antecer. All rights reserved. 10 | https://github.com/Antecer/QQChannelBot 11 | https://github.com/Antecer/QQChannelBot.git 12 | QQ;Channel;Bot;频道;机器人 13 | True 14 | MIT 15 | README.md 16 | git 17 | 优化指令查询方法; 18 | True 19 | logo.png 20 | True 21 | 22 | 23 | 24 | 25 | True 26 | \ 27 | 28 | 29 | 30 | 31 | 32 | True 33 | \ 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /QQChannelBot/Tools/JsonHelper.cs: -------------------------------------------------------------------------------- 1 | namespace System.Text.Json 2 | { 3 | /// 4 | /// Json访问帮助类 5 | /// 6 | public static class JsonHelper 7 | { 8 | /// 9 | /// 查找JSON对象 10 | /// 11 | /// 12 | /// json的key 13 | /// 14 | public static JsonElement? Get(this JsonElement element, string name) 15 | { 16 | if (element.ValueKind != JsonValueKind.Object) return null; 17 | return element.TryGetProperty(name, out JsonElement value) ? value : null; 18 | } 19 | /// 20 | /// 索引JSON数组 21 | /// 22 | /// 23 | /// json的index 24 | /// 25 | public static JsonElement? Get(this JsonElement element, int index) 26 | { 27 | if (element.ValueKind != JsonValueKind.Array) 28 | { 29 | if (element.ValueKind == JsonValueKind.Object) return element.Get(index.ToString()); 30 | return null; 31 | } 32 | JsonElement value = element.EnumerateArray().ElementAtOrDefault(index); 33 | return value.ValueKind != JsonValueKind.Undefined ? value : null; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /QQChannelBot/Tools/StringTools.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace System 4 | { 5 | /// 6 | /// 字符串处理工具类 7 | /// 8 | public static class StringTools 9 | { 10 | /// 11 | /// 替换字符串开始位置的字符串 12 | /// 13 | /// 源字符串 14 | /// 查找的串 15 | /// 替换的串 16 | /// 查找字符串时忽略大小写 17 | /// 18 | public static string TrimStart(this string input, string oldValue, string newValue = "", bool ignoreCase = false) 19 | { 20 | if (string.IsNullOrEmpty(input)) return input; 21 | if (!input.StartsWith(oldValue, ignoreCase, null)) return input; 22 | return string.Concat(newValue, input.AsSpan(oldValue.Length)); 23 | } 24 | /// 25 | /// 替换字符串末尾位置的字符串 26 | /// 27 | /// 源字符串 28 | /// 查找的串 29 | /// 替换的串 30 | /// 查找字符串时忽略大小写 31 | /// 32 | public static string TrimEnd(this string input, string oldValue, string newValue = "", bool ignoreCase = false) 33 | { 34 | if (string.IsNullOrEmpty(input)) return input; 35 | if (!input.EndsWith(oldValue, ignoreCase, null)) return input; 36 | return string.Concat(input.Remove(input.Length - oldValue.Length), newValue); 37 | } 38 | /// 39 | /// 替换字符串开始和末尾位置的字符串 40 | /// 41 | /// 源字符串 42 | /// 查找的串 43 | /// 替换的串 44 | /// 查找字符串时忽略大小写 45 | /// 46 | public static string Trim(this string input, string oldValue, string newValue = "", bool ignoreCase = false) 47 | { 48 | return input.TrimStart(oldValue, newValue, ignoreCase).TrimEnd(oldValue, newValue, ignoreCase); 49 | } 50 | /// 51 | /// 判断字符串是否为空白 52 | /// 效果等同于string.IsNullOrWhiteSpace() 53 | /// 54 | /// 55 | /// 56 | public static bool IsBlank(this string? input) 57 | { 58 | return string.IsNullOrWhiteSpace(input); 59 | } 60 | 61 | /// 62 | /// 判断指定的字符串是 null还是空字符串 63 | /// 64 | /// 65 | /// 66 | public static bool IsNullOrEmpty(this string? input) => string.IsNullOrEmpty(input); 67 | /// 68 | /// 判断指定的字符串是 null、空还是仅由空白字符组成 69 | /// 70 | /// 71 | /// 72 | public static bool IsNullOrWhiteSpace(this string? input) => string.IsNullOrWhiteSpace(input); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /QQChannelBot/Tools/Unicoder.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace QQChannelBot.Tools 5 | { 6 | /// 7 | /// Unicode编解码器 8 | /// 9 | public static class Unicoder 10 | { 11 | private static readonly Regex reUnicode = new(@"\\u([0-9a-fA-F]{4})", RegexOptions.Compiled); 12 | /// 13 | /// Unicode编码(\uxxxx)序列转字符串 14 | /// 15 | /// 16 | /// 17 | public static string Decode(string s) 18 | { 19 | return reUnicode.Replace(s, m => 20 | { 21 | if (short.TryParse(m.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out short c)) 22 | { 23 | return "" + (char)c; 24 | } 25 | return m.Value; 26 | }); 27 | } 28 | 29 | private static readonly Regex reUnicodeChar = new(@"[^\u0000-\u00ff]", RegexOptions.Compiled); 30 | /// 31 | /// 字符串转Unicode编码(\uxxxx)序列 32 | /// 33 | /// 34 | /// 35 | public static string Encode(string s) 36 | { 37 | return reUnicodeChar.Replace(s, m => string.Format(@"\u{0:x4}", (short)m.Value[0])); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /QQChannelBot/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antecer/QQChannelBot/18b48c60246e518878dd89e33a72608adabe59c6/QQChannelBot/logo.png --------------------------------------------------------------------------------