feat: Bot 编辑频道支持修改头像

This commit is contained in:
meoacgx committed 2025-12-20 23:04:49 +08:00
1 parent 987be180d0
commit 22e63f065d
3 files changed
+173 -2

No files matched your search

@@ -1,5 +1,8 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
using TelegramPanel.Core.Services;
using TelegramPanel.Data.Entities;
@@ -294,6 +297,70 @@ public class BotTelegramService
return true;
}
public async Task<bool> SetChannelPhotoAsync(
int botId,
long channelTelegramId,
Stream fileStream,
string fileName,
CancellationToken cancellationToken)
{
var bot = await _botManagement.GetBotAsync(botId)
?? throw new InvalidOperationException($"机器人不存在:{botId}");
if (!bot.IsActive)
throw new InvalidOperationException("该机器人已停用");
if (fileStream == null)
throw new ArgumentException("头像文件为空", nameof(fileStream));
fileName = (fileName ?? "photo.jpg").Trim();
if (string.IsNullOrWhiteSpace(fileName))
fileName = "photo.jpg";
// Bot API setChatPhoto 需要 multipart 上传 InputFile。
// 为了提高成功率:做“居中裁剪为正方形 + 缩放到 512x512 + JPEG 压缩”,避免原图过大/比例异常导致失败。
await using var raw = new MemoryStream();
if (fileStream.CanSeek)
fileStream.Position = 0;
await fileStream.CopyToAsync(raw, cancellationToken);
raw.Position = 0;
await using var encoded = new MemoryStream();
try
{
using var image = await Image.LoadAsync(raw, cancellationToken);
image.Mutate(x => x.AutoOrient());
const int targetSize = 512;
image.Mutate(x => x.Resize(new ResizeOptions
{
Mode = ResizeMode.Crop,
Size = new Size(targetSize, targetSize)
}));
await image.SaveAsJpegAsync(encoded, new JpegEncoder { Quality = 85 }, cancellationToken);
encoded.Position = 0;
}
catch (UnknownImageFormatException)
{
throw new InvalidOperationException("不支持的图片格式(建议使用 JPG/PNG)");
}
_ = await _api.CallWithFileAsync(
token: bot.Token,
method: "setChatPhoto",
parameters: new Dictionary<string, string?>
{
["chat_id"] = channelTelegramId.ToString()
},
fileParameterName: "photo",
fileStream: encoded,
fileName: fileName,
cancellationToken: cancellationToken);
return true;
}
public async Task<int> PromoteChatMemberAsync(int botId, IReadOnlyList<long> channelTelegramIds, long userId, BotAdminRights rights, CancellationToken cancellationToken)
{
var bot = await _botManagement.GetBotAsync(botId)
@@ -47,6 +47,77 @@ public class TelegramBotApiClient
return result.Clone();
}
/// <summary>
/// 调用 Bot API 并上传文件(使用 multipart/form-data)
/// </summary>
public async Task<JsonElement> CallWithFileAsync(
string token,
string method,
IReadOnlyDictionary<string, string?> parameters,
string fileParameterName,
Stream fileStream,
string fileName,
CancellationToken cancellationToken)
{
token = (token ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(token))
throw new InvalidOperationException("Bot Token 为空");
method = (method ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(method))
throw new InvalidOperationException("method 为空");
if (string.IsNullOrWhiteSpace(fileParameterName))
throw new InvalidOperationException("fileParameterName 为空");
if (fileStream == null)
throw new InvalidOperationException("fileStream 为空");
fileName = (fileName ?? "file").Trim();
if (string.IsNullOrWhiteSpace(fileName))
fileName = "file";
if (fileStream.CanSeek)
fileStream.Position = 0;
var url = $"https://api.telegram.org/bot{Uri.EscapeDataString(token)}/{Uri.EscapeDataString(method)}";
using var content = new MultipartFormDataContent();
// 添加普通参数
foreach (var (key, value) in parameters)
{
if (!string.IsNullOrWhiteSpace(key) && value != null)
{
content.Add(new StringContent(value), key);
}
}
// 添加文件
var streamContent = new StreamContent(fileStream);
streamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
content.Add(streamContent, fileParameterName, fileName);
using var resp = await _http.PostAsync(url, content, cancellationToken);
var json = await resp.Content.ReadAsStringAsync(cancellationToken);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
var ok = root.TryGetProperty("ok", out var okEl) && okEl.ValueKind == JsonValueKind.True;
if (!ok)
{
var code = root.TryGetProperty("error_code", out var codeEl) ? codeEl.GetInt32() : 0;
var desc = root.TryGetProperty("description", out var descEl) ? descEl.GetString() : "未知错误";
throw new InvalidOperationException($"Bot API 调用失败:{method} ({code}) {desc}");
}
if (!root.TryGetProperty("result", out var result))
throw new InvalidOperationException($"Bot API 返回缺少 result:{method}");
return result.Clone();
}
private static string BuildUrl(string token, string method, IReadOnlyDictionary<string, string?> query)
{
var sb = new StringBuilder();
@@ -74,4 +145,3 @@ public class TelegramBotApiClient
return sb.ToString();
}
}
@@ -3,6 +3,7 @@
@inject TelegramPanel.Core.Services.Telegram.BotTelegramService BotTelegram
@inject ISnackbar Snackbar
@using TelegramPanel.Data.Entities
@using Microsoft.AspNetCore.Components.Forms
<MudDialog>
<DialogContent>
@@ -15,6 +16,23 @@
<MudStack Spacing="2">
<MudTextField @bind-Value="title" Label="频道标题" Variant="Variant.Outlined" Required="true" />
<MudTextField @bind-Value="about" Label="频道简介(可选)" Variant="Variant.Outlined" Lines="4" />
<MudSwitch @bind-Value="editAvatar" Label="修改频道头像" Color="Color.Primary" />
@if (editAvatar)
{
<MudFileUpload T="IReadOnlyList<IBrowserFile>" FilesChanged="OnAvatarChanged" Accept="image/*" MaximumFileCount="1">
<ActivatorContent>
<MudButton Variant="Variant.Outlined" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Image">
选择头像图片
</MudButton>
</ActivatorContent>
</MudFileUpload>
@if (avatarFile != null)
{
<MudText Typo="Typo.body2">已选择:@avatarFile.Name(@avatarFile.Size bytes)</MudText>
}
}
</MudStack>
}
</DialogContent>
@@ -46,6 +64,8 @@
private string title = "";
private string about = "";
private TelegramPanel.Core.Services.Telegram.BotTelegramService.BotChatInfo? chatInfo;
private bool editAvatar;
private IBrowserFile? avatarFile;
protected override async Task OnInitializedAsync()
{
@@ -89,6 +109,16 @@
var normalizedAbout = string.IsNullOrWhiteSpace(about) ? null : about.Trim();
await BotTelegram.UpdateChannelInfoAsync(BotId, ChannelTelegramId, title, normalizedAbout, CancellationToken.None);
if (editAvatar)
{
if (avatarFile == null)
throw new InvalidOperationException("请先选择头像图片");
const long maxBytes = 20 * 1024 * 1024;
await using var stream = avatarFile.OpenReadStream(maxBytes);
await BotTelegram.SetChannelPhotoAsync(BotId, ChannelTelegramId, stream, avatarFile.Name, CancellationToken.None);
}
// 同步回本地 DB(用于列表展示)
var dbChannel = await BotManagement.GetChannelByTelegramIdAsync(BotId, ChannelTelegramId);
var username = chatInfo?.Username ?? dbChannel?.Username;
@@ -124,5 +154,9 @@
{
MudDialog.Cancel();
}
}
private void OnAvatarChanged(IReadOnlyList<IBrowserFile> files)
{
avatarFile = files?.FirstOrDefault();
}
}