Files
Shirobot.Plugin.MyAgent/ShirobotGroupPlatform.cs

143 lines
7.4 KiB
C#

using ShiroBot.SDK.Models;
using ShiroBot.SDK.Plugin;
using ShiroBot.Model.QQ;
using MyAgent;
using MyAgent.Core;
using MyAgent.Tools;
using Channel = ShiroBot.SDK.Models.Channel;
namespace Shirobot.Plugin.MyAgent;
internal sealed class ShirobotGroupPlatform(IBotContext context) : IGroupPlatform
{
private static readonly HttpClient DownloadClient = new() { Timeout = TimeSpan.FromSeconds(20) };
public async Task<MessageSendResult?> SendAsync(
long groupId, string text, IReadOnlyList<ChatAttachment> attachments,
string? replyToMessageId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
using var scope = context.UsePlatform("qq");
var segments = new List<MessageSegment>();
if (!string.IsNullOrWhiteSpace(replyToMessageId)) segments.Add(new QuoteSegment(replyToMessageId));
if (!string.IsNullOrWhiteSpace(text)) segments.Add(new TextSegment(text));
foreach (var attachment in attachments)
{
var uri = !string.IsNullOrWhiteSpace(attachment.LocalPath) && File.Exists(attachment.LocalPath)
? "base64://" + Convert.ToBase64String(await File.ReadAllBytesAsync(attachment.LocalPath, cancellationToken))
: attachment.SourceUrl;
if (!string.IsNullOrWhiteSpace(uri)) segments.Add(new ImageSegment(uri) { Summary = "[图片]" });
}
if (segments.Count == 0) return null;
var sent = await context.Message.SendMessageAsync(Channel.Group(groupId.ToString()), segments);
return new MessageSendResult { Ok = true, MessageId = sent.MessageId, Timestamp = sent.Timestamp };
}
public async Task<IReadOnlyList<GroupChatEvent>> GetHistoryAsync(long groupId, int limit, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
using var scope = context.UsePlatform("qq");
var messages = await context.Message.GetHistoryMessagesAsync(Channel.Group(groupId.ToString()), limit: Math.Clamp(limit, 1, 100));
return messages.Where(message => message.Sender.Id != message.SelfId)
.Select(message => GroupChatEvent.User(groupId, message.Sender.Id, message.Member?.Nick ?? message.Sender.Name ?? message.Sender.Id,
message.GetPlainText(), message.Timestamp, ToAttachments(message.Segments), message.MessageId))
.ToArray();
}
public async Task<GroupProfileResult> GetProfileAsync(long groupId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
using var scope = context.UsePlatform("qq");
var channel = await context.Channel.GetChannelAsync(groupId.ToString());
var members = await context.Channel.GetMembersAsync(groupId.ToString());
return new GroupProfileResult
{
GroupId = groupId,
GroupName = channel?.Name ?? groupId.ToString(),
AvatarUrl = $"https://p.qlogo.cn/gh/{groupId}/{groupId}/100",
MemberCount = members.Count,
Members = members.Select(member => new GroupMemberInfo
{
UserId = long.TryParse(member.User.Id, out var id) ? id : 0,
Name = member.Nick ?? member.User.Name ?? member.User.Id,
Role = member.Role.ToString().ToLowerInvariant(),
AvatarUrl = $"https://q1.qlogo.cn/g?b=qq&nk={member.User.Id}&s=100"
}).ToList()
};
}
internal static ChatAttachment[] ToAttachments(IReadOnlyList<MessageSegment> segments) => segments
.OfType<ResourceSegment>()
.Select(segment => new ChatAttachment
{
Type = segment switch { ImageSegment => "image", AudioSegment => "audio", VideoSegment => "video", FileSegment => "file", _ => "resource" },
Url = segment.Uri,
SourceUrl = segment.Uri,
Title = segment.FileName ?? "",
Size = segment is FileSegment file ? file.FileSize ?? 0 : 0
}).ToArray();
internal async Task<List<ChatAttachment>> ToAttachmentsAsync(
IReadOnlyList<MessageSegment> segments, long groupId, DocumentTextExtractor documents,
CancellationToken cancellationToken)
{
using var scope = context.UsePlatform("qq");
var attachments = new List<ChatAttachment>();
foreach (var segment in segments.OfType<ResourceSegment>())
{
cancellationToken.ThrowIfCancellationRequested();
var url = segment.Uri;
if (string.IsNullOrWhiteSpace(url) && !string.IsNullOrWhiteSpace(segment.ResourceId))
{
if (segment is FileSegment)
url = await (context.GetAdapterExtension<IQFileApi>()?.GetGroupFileDownloadUrlAsync(groupId, segment.ResourceId)
?? Task.FromResult(""));
else
url = await context.Message.GetResourceUrlAsync(segment.ResourceId);
}
var attachment = new ChatAttachment
{
Type = segment switch { ImageSegment => "image", AudioSegment => "audio", VideoSegment => "video", FileSegment => "file", _ => "resource" },
Url = url,
SourceUrl = url,
Title = segment.FileName ?? "",
Size = segment is FileSegment file ? file.FileSize ?? 0 : 0
};
if (segment is FileSegment && Uri.TryCreate(url, UriKind.Absolute, out var uri) && uri.Scheme is "http" or "https")
{
try
{
var cache = Path.Combine(context.PluginDirectory, "data", "attachments");
Directory.CreateDirectory(cache);
var suffix = Path.GetExtension(attachment.Title);
var path = Path.Combine(cache, Guid.NewGuid().ToString("n") + suffix);
using var response = await DownloadClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
if (response.Content.Headers.ContentLength is > 15_000_000) throw new InvalidDataException("Attachment exceeds 15 MB");
await using (var input = await response.Content.ReadAsStreamAsync(cancellationToken))
await using (var output = File.Create(path))
{
var buffer = new byte[81920];
long total = 0;
int read;
while ((read = await input.ReadAsync(buffer, cancellationToken)) > 0)
{
total += read;
if (total > 15_000_000) throw new InvalidDataException("Attachment exceeds 15 MB");
await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
}
}
attachment.LocalPath = path;
attachment.TextContent = await documents.TryExtractAsync(path, attachment.Title,
response.Content.Headers.ContentType?.MediaType ?? "", cancellationToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
ShiroBot.SDK.Abstractions.BotLog.Warning($"Could not read group attachment: {ex.Message}");
}
}
attachments.Add(attachment);
}
return attachments;
}
}