feat: Bot 编辑频道支持修改头像
This commit is contained in:
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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user