diff --git a/src/TelegramPanel.Core/Services/Telegram/BotTelegramService.cs b/src/TelegramPanel.Core/Services/Telegram/BotTelegramService.cs index 275fb69..05d7ac1 100644 --- a/src/TelegramPanel.Core/Services/Telegram/BotTelegramService.cs +++ b/src/TelegramPanel.Core/Services/Telegram/BotTelegramService.cs @@ -305,6 +305,8 @@ public class BotTelegramService if (userId <= 0) throw new ArgumentException("userId 无效", nameof(userId)); + var botUserId = await GetBotUserIdAsync(bot.Token, cancellationToken); + var ok = 0; foreach (var chatId in channelTelegramIds.Distinct()) { @@ -312,6 +314,10 @@ public class BotTelegramService try { + var (canPromote, reason) = await CanBotPromoteMembersAsync(bot.Token, chatId, botUserId, cancellationToken); + if (!canPromote) + throw new InvalidOperationException(reason); + var args = new Dictionary { ["chat_id"] = chatId.ToString(), @@ -414,4 +420,108 @@ public class BotTelegramService bool PinMessages, bool PromoteMembers ); + + public async Task PromoteChatMemberWithResultAsync( + int botId, + IReadOnlyList channelTelegramIds, + long userId, + BotAdminRights rights, + CancellationToken cancellationToken) + { + var bot = await _botManagement.GetBotAsync(botId) + ?? throw new InvalidOperationException($"机器人不存在:{botId}"); + + if (!bot.IsActive) + throw new InvalidOperationException("该机器人已停用"); + + if (userId <= 0) + throw new ArgumentException("userId 无效", nameof(userId)); + + var distinctIds = channelTelegramIds.Distinct().ToList(); + var failures = new Dictionary(); + + var botUserId = await GetBotUserIdAsync(bot.Token, cancellationToken); + + var ok = 0; + foreach (var chatId in distinctIds) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var (canPromote, reason) = await CanBotPromoteMembersAsync(bot.Token, chatId, botUserId, cancellationToken); + if (!canPromote) + throw new InvalidOperationException(reason); + + var args = new Dictionary + { + ["chat_id"] = chatId.ToString(), + ["user_id"] = userId.ToString(), + ["can_manage_chat"] = rights.ManageChat ? "true" : "false", + ["can_post_messages"] = rights.PostMessages ? "true" : "false", + ["can_edit_messages"] = rights.EditMessages ? "true" : "false", + ["can_delete_messages"] = rights.DeleteMessages ? "true" : "false", + ["can_invite_users"] = rights.InviteUsers ? "true" : "false", + ["can_restrict_members"] = rights.RestrictMembers ? "true" : "false", + ["can_pin_messages"] = rights.PinMessages ? "true" : "false", + ["can_promote_members"] = rights.PromoteMembers ? "true" : "false" + }; + + await _api.CallAsync(bot.Token, "promoteChatMember", args, cancellationToken); + ok++; + } + catch (Exception ex) + { + var msg = ex.Message; + failures[chatId] = msg; + _logger.LogWarning(ex, "PromoteChatMember failed for bot {BotId} chat {ChatId} user {UserId}", botId, chatId, userId); + } + } + + return new PromoteAdminsResult(ok, distinctIds.Count, failures); + } + + public sealed record PromoteAdminsResult(int SuccessCount, int TotalCount, IReadOnlyDictionary Failures); + + private async Task GetBotUserIdAsync(string token, CancellationToken cancellationToken) + { + var me = await _api.CallAsync(token, "getMe", new Dictionary(), cancellationToken); + if (me.ValueKind != JsonValueKind.Object) + throw new InvalidOperationException("Bot API getMe 返回格式异常"); + if (!me.TryGetProperty("id", out var idEl) || !idEl.TryGetInt64(out var id)) + throw new InvalidOperationException("Bot API getMe 缺少 id"); + return id; + } + + private async Task<(bool CanPromote, string Reason)> CanBotPromoteMembersAsync(string token, long chatId, long botUserId, CancellationToken cancellationToken) + { + var member = await _api.CallAsync(token, "getChatMember", new Dictionary + { + ["chat_id"] = chatId.ToString(), + ["user_id"] = botUserId.ToString() + }, cancellationToken); + + if (member.ValueKind != JsonValueKind.Object) + return (false, "无法检测机器人权限(getChatMember 返回格式异常)"); + + var status = member.TryGetProperty("status", out var statusEl) ? (statusEl.GetString() ?? "") : ""; + if (string.Equals(status, "creator", StringComparison.OrdinalIgnoreCase)) + return (true, ""); + + if (!string.Equals(status, "administrator", StringComparison.OrdinalIgnoreCase)) + return (false, "机器人不是管理员(请先把机器人设为管理员)"); + + var canPromote = ReadBool(member, "can_promote_members"); + if (!canPromote) + return (false, "机器人缺少“添加管理员”权限(请在频道管理员设置里给机器人开启“添加管理员/添加新管理员”)"); + + return (true, ""); + } + + private static bool ReadBool(JsonElement obj, string name) + { + return obj.TryGetProperty(name, out var el) + && (el.ValueKind == JsonValueKind.True || el.ValueKind == JsonValueKind.False) + && el.GetBoolean(); + } } diff --git a/src/TelegramPanel.Web/Components/Dialogs/BotSetAdminsDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/BotSetAdminsDialog.razor index d7e2331..5e2e1a0 100644 --- a/src/TelegramPanel.Web/Components/Dialogs/BotSetAdminsDialog.razor +++ b/src/TelegramPanel.Web/Components/Dialogs/BotSetAdminsDialog.razor @@ -1,12 +1,13 @@ @namespace TelegramPanel.Web.Components.Dialogs @inject TelegramPanel.Core.Services.Telegram.BotTelegramService BotTelegram @inject ISnackbar Snackbar +@using System.Linq - 请输入要设置为管理员的 Telegram 用户 ID(不是手机号/用户名)。机器人需要在目标频道具备“添加管理员”权限。 + 请输入要设置为管理员的 Telegram 用户 ID(不是手机号/用户名)。机器人需要在目标频道具备“添加管理员/添加新管理员”权限,否则会失败。 @@ -40,7 +41,7 @@ - 将对已选频道批量执行 promoteChatMember;失败的频道会被跳过(你可以在日志中查看原因)。 + 将对已选频道批量执行 promoteChatMember;失败会给出原因(常见:机器人不是管理员/缺少“添加管理员”权限/目标用户不在频道内)。 @@ -104,7 +105,7 @@ saving = true; try { - var ok = await BotTelegram.PromoteChatMemberAsync( + var result = await BotTelegram.PromoteChatMemberWithResultAsync( botId: BotId, channelTelegramIds: ChannelTelegramIds, userId: userId.Value, @@ -121,8 +122,24 @@ cancellationToken: CancellationToken.None ); - Snackbar.Add($"已设置管理员:成功 {ok}/{ChannelTelegramIds.Distinct().Count()} 个频道", ok > 0 ? Severity.Success : Severity.Warning); - MudDialog.Close(DialogResult.Ok(ok)); + if (result.SuccessCount == 0) + { + var first = result.Failures.Values.FirstOrDefault() ?? "未知原因"; + Snackbar.Add($"设置失败:{first}", Severity.Error); + return; + } + + if (result.Failures.Count > 0) + { + var first = result.Failures.Values.FirstOrDefault() ?? "未知原因"; + Snackbar.Add($"部分失败:成功 {result.SuccessCount}/{result.TotalCount},示例原因:{first}", Severity.Warning); + } + else + { + Snackbar.Add($"已设置管理员:成功 {result.SuccessCount}/{result.TotalCount} 个频道", Severity.Success); + } + + MudDialog.Close(DialogResult.Ok(result.SuccessCount)); } catch (Exception ex) { @@ -151,4 +168,3 @@ public bool PromoteMembers { get; set; } } } -