feat(kick): add module page and queue /api/kick
This commit is contained in:
1 parent
0401312ee3
commit
f551da0a8b
7 files changed
+551
-156
No files matched your search
@@ -824,7 +824,8 @@ public sealed record BotAdminRights(
|
||||
IReadOnlyList<long> channelTelegramIds,
|
||||
long userId,
|
||||
bool permanentBan,
|
||||
CancellationToken cancellationToken)
|
||||
CancellationToken cancellationToken,
|
||||
Func<long, string?, int, int, Task>? perChatCallback = null)
|
||||
{
|
||||
var bot = await _botManagement.GetBotAsync(botId)
|
||||
?? throw new InvalidOperationException($"机器人不存在:{botId}");
|
||||
@@ -936,6 +937,12 @@ public sealed record BotAdminRights(
|
||||
}
|
||||
}
|
||||
|
||||
if (perChatCallback != null)
|
||||
{
|
||||
failures.TryGetValue(chatId, out var err);
|
||||
await perChatCallback(chatId, err, ok, failures.Count);
|
||||
}
|
||||
|
||||
// 降速:避免 Bot API 429
|
||||
if (delayMs > 0 && index < distinctIds.Count - 1)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
@namespace TelegramPanel.Web.Components.ModulePages
|
||||
@inject TelegramPanel.Core.Services.BotManagementService BotManagement
|
||||
@inject TelegramPanel.Core.Services.BatchTaskManagementService TaskManagement
|
||||
@inject ISnackbar Snackbar
|
||||
@inject NavigationManager Navigation
|
||||
@using System.Text.Json
|
||||
@using TelegramPanel.Core.BatchTasks
|
||||
@using TelegramPanel.Data.Entities
|
||||
@using TelegramPanel.Web.ExternalApi
|
||||
@using Microsoft.AspNetCore.Components
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4 tp-page-title">踢人/封禁</MudText>
|
||||
|
||||
<MudPaper Class="pa-4" Elevation="0" Outlined="true">
|
||||
<MudStack Spacing="2">
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
|
||||
提交后会在后台执行,可到【任务中心】查看进度与结果。频道数量较多时可能需要较长时间。
|
||||
</MudAlert>
|
||||
|
||||
<MudSelect T="int" Value="@selectedBotId" ValueChanged="@(async (int id) => await OnSelectedBotChanged(id))"
|
||||
Label="选择机器人" Variant="Variant.Outlined" Dense="true" Disabled="@loading">
|
||||
<MudSelectItem Value="0">全部机器人(谨慎)</MudSelectItem>
|
||||
@foreach (var b in bots)
|
||||
{
|
||||
<MudSelectItem Value="@b.Id">@b.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudNumericField T="long" @bind-Value="userId" Label="用户 ID(user_id)" Variant="Variant.Outlined" Dense="true"
|
||||
Min="1" Disabled="@loading" />
|
||||
|
||||
<MudSwitch @bind-Value="permanentBan" Color="Color.Error" Disabled="@loading"
|
||||
Label="永久封禁(否则仅踢出,可重新加入)" />
|
||||
|
||||
<MudSwitch @bind-Value="useAllChats" Disabled="@(loading || selectedBotId == 0)"
|
||||
Label="使用全部频道/群组(关闭后可手动输入 chat_id 列表)" />
|
||||
|
||||
@if (!useAllChats && selectedBotId != 0)
|
||||
{
|
||||
<MudTextField @bind-Value="chatIdsText" Label="chat_id 列表(可选)" Variant="Variant.Outlined" Lines="3"
|
||||
Placeholder="支持逗号/空格/换行分隔,例如:123,456,789"
|
||||
Disabled="@loading" />
|
||||
}
|
||||
|
||||
<MudStack Row="true" Spacing="2">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.PersonRemove"
|
||||
Disabled="@(!CanSubmit)" OnClick="SubmitAsync">
|
||||
提交任务
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Default" Disabled="@loading"
|
||||
OnClick="@(() => Navigation.NavigateTo("/tasks"))">
|
||||
打开任务中心
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public string ModuleId { get; set; } = "";
|
||||
[Parameter] public string PageKey { get; set; } = "";
|
||||
|
||||
private bool loading = true;
|
||||
private List<Bot> bots = new();
|
||||
private int selectedBotId = 0;
|
||||
private long userId;
|
||||
private bool permanentBan;
|
||||
private bool useAllChats = true;
|
||||
private string chatIdsText = "";
|
||||
|
||||
private bool CanSubmit => !loading && userId > 0 && (selectedBotId == 0 || bots.Any(x => x.Id == selectedBotId));
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
bots = (await BotManagement.GetAllBotsAsync())
|
||||
.Where(b => b.IsActive)
|
||||
.OrderBy(b => b.Id)
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"加载机器人失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSelectedBotChanged(int id)
|
||||
{
|
||||
selectedBotId = id;
|
||||
if (selectedBotId == 0)
|
||||
useAllChats = true;
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
if (!CanSubmit)
|
||||
return;
|
||||
|
||||
var botId = selectedBotId;
|
||||
var runUseAllChats = botId == 0 ? true : useAllChats;
|
||||
var chatIds = runUseAllChats ? new List<long>() : ParseChatIds(chatIdsText);
|
||||
|
||||
loading = true;
|
||||
try
|
||||
{
|
||||
var total = await CountTargetsAsync(botId, runUseAllChats, chatIds);
|
||||
if (total <= 0)
|
||||
{
|
||||
Snackbar.Add("未匹配到任何可操作的频道/群组(请先同步 Bot 频道数据,或检查 chat_id 列表)", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var log = new KickTaskLog
|
||||
{
|
||||
ApiName = "模块页面",
|
||||
BotId = botId,
|
||||
UseAllChats = runUseAllChats,
|
||||
ChatIds = chatIds.Where(x => x != 0).Distinct().OrderBy(x => x).ToList(),
|
||||
UserId = userId,
|
||||
PermanentBan = permanentBan,
|
||||
RequestedAtUtc = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var task = new BatchTask
|
||||
{
|
||||
TaskType = BatchTaskTypes.ExternalApiKick,
|
||||
Total = total,
|
||||
Completed = 0,
|
||||
Failed = 0,
|
||||
Config = JsonSerializer.Serialize(log, new JsonSerializerOptions { WriteIndented = true })
|
||||
};
|
||||
|
||||
var created = await TaskManagement.CreateTaskAsync(task);
|
||||
Snackbar.Add($"已提交后台任务 #{created.Id}。请到【任务中心】查看进度。", Severity.Success);
|
||||
Navigation.NavigateTo("/tasks");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"提交失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int> CountTargetsAsync(int botId, bool runUseAllChats, IReadOnlyCollection<long> chatIds)
|
||||
{
|
||||
if (botId > 0)
|
||||
{
|
||||
var chats = (await BotManagement.GetChatsAsync(botId)).ToList();
|
||||
if (!runUseAllChats && chatIds.Count > 0)
|
||||
chats = chats.Where(c => chatIds.Contains(c.TelegramId)).ToList();
|
||||
return chats.Count;
|
||||
}
|
||||
|
||||
// botId==0: all bots (force useAllChats)
|
||||
var botsList = (await BotManagement.GetAllBotsAsync())
|
||||
.Where(b => b.IsActive)
|
||||
.OrderBy(b => b.Id)
|
||||
.ToList();
|
||||
|
||||
var total = 0;
|
||||
foreach (var b in botsList)
|
||||
{
|
||||
var chats = await BotManagement.GetChatsAsync(b.Id);
|
||||
total += chats.Count();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static List<long> ParseChatIds(string raw)
|
||||
{
|
||||
raw = (raw ?? "").Trim();
|
||||
if (raw.Length == 0)
|
||||
return new List<long>();
|
||||
|
||||
var parts = raw
|
||||
.Replace("\r", "\n", StringComparison.Ordinal)
|
||||
.Split(new[] { ',', ';', ' ', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
var ids = new List<long>();
|
||||
foreach (var p in parts)
|
||||
{
|
||||
if (long.TryParse(p, out var v) && v != 0)
|
||||
ids.Add(v);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TelegramPanel.Core.BatchTasks;
|
||||
using TelegramPanel.Core.Services;
|
||||
using TelegramPanel.Core.Services.Telegram;
|
||||
using TelegramPanel.Data.Entities;
|
||||
using TelegramPanel.Modules;
|
||||
|
||||
namespace TelegramPanel.Web.ExternalApi;
|
||||
|
||||
public sealed class ExternalApiKickTaskHandler : IModuleTaskHandler
|
||||
{
|
||||
public string TaskType => BatchTaskTypes.ExternalApiKick;
|
||||
|
||||
public async Task ExecuteAsync(IModuleTaskExecutionHost host, CancellationToken cancellationToken)
|
||||
{
|
||||
var logger = host.Services.GetRequiredService<ILogger<ExternalApiKickTaskHandler>>();
|
||||
var taskManagement = host.Services.GetRequiredService<BatchTaskManagementService>();
|
||||
var botManagement = host.Services.GetRequiredService<BotManagementService>();
|
||||
var botTelegram = host.Services.GetRequiredService<BotTelegramService>();
|
||||
|
||||
var rawConfig = (host.Config ?? "").Trim();
|
||||
if (rawConfig.Length == 0)
|
||||
throw new InvalidOperationException("任务缺少 Config");
|
||||
|
||||
KickTaskLog? input;
|
||||
try
|
||||
{
|
||||
input = JsonSerializer.Deserialize<KickTaskLog>(rawConfig);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"任务 Config JSON 无效:{ex.Message}");
|
||||
}
|
||||
|
||||
if (input == null)
|
||||
throw new InvalidOperationException("任务 Config JSON 为空");
|
||||
|
||||
if (input.UserId <= 0)
|
||||
throw new InvalidOperationException("任务 user_id 无效");
|
||||
|
||||
input.ChatIds ??= new List<long>();
|
||||
|
||||
var configuredBotId = input.BotId;
|
||||
var useAllChats = configuredBotId == 0 ? true : input.UseAllChats;
|
||||
var chatIdSet = new HashSet<long>(input.ChatIds.Where(x => x != 0));
|
||||
|
||||
var targets = await ResolveTargetsAsync(botManagement, configuredBotId, useAllChats, chatIdSet, cancellationToken);
|
||||
if (targets.TotalChats == 0)
|
||||
throw new InvalidOperationException("未匹配到任何可操作的频道/群组");
|
||||
|
||||
var results = new List<KickResultItem>(targets.TotalChats);
|
||||
var completed = 0;
|
||||
var failed = 0;
|
||||
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
var linkedToken = linkedCts.Token;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var group in targets.Groups)
|
||||
{
|
||||
linkedToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!await host.IsStillRunningAsync(linkedToken))
|
||||
{
|
||||
await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(new KickTaskLog
|
||||
{
|
||||
ApiName = input.ApiName ?? "",
|
||||
BotId = configuredBotId,
|
||||
UseAllChats = useAllChats,
|
||||
ChatIds = chatIdSet.OrderBy(x => x).ToList(),
|
||||
UserId = input.UserId,
|
||||
PermanentBan = input.PermanentBan,
|
||||
RequestedAtUtc = input.RequestedAtUtc,
|
||||
Results = results,
|
||||
Canceled = true
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
var baseCompleted = completed;
|
||||
var baseFailed = failed;
|
||||
var chatById = group.Chats.ToDictionary(c => c.TelegramId);
|
||||
var recorded = new HashSet<long>();
|
||||
|
||||
try
|
||||
{
|
||||
_ = await botTelegram.BanChatMemberAsync(
|
||||
botId: group.Bot.Id,
|
||||
channelTelegramIds: group.Chats.Select(x => x.TelegramId).ToList(),
|
||||
userId: input.UserId,
|
||||
permanentBan: input.PermanentBan,
|
||||
cancellationToken: linkedToken,
|
||||
perChatCallback: async (chatId, error, okCount, failCount) =>
|
||||
{
|
||||
if (!recorded.Add(chatId))
|
||||
return;
|
||||
|
||||
var ok = error == null;
|
||||
results.Add(new KickResultItem(
|
||||
ChatId: chatId.ToString(),
|
||||
Title: chatById.TryGetValue(chatId, out var ch) ? ch.Title : chatId.ToString(),
|
||||
Success: ok,
|
||||
Error: error));
|
||||
|
||||
completed = baseCompleted + okCount;
|
||||
failed = baseFailed + failCount;
|
||||
await host.UpdateProgressAsync(completed, failed, linkedToken);
|
||||
|
||||
if (!await host.IsStillRunningAsync(linkedToken))
|
||||
linkedCts.Cancel();
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException) when (linkedCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// 用户主动取消(把任务状态改为非 running)
|
||||
await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(new KickTaskLog
|
||||
{
|
||||
ApiName = input.ApiName ?? "",
|
||||
BotId = configuredBotId,
|
||||
UseAllChats = useAllChats,
|
||||
ChatIds = chatIdSet.OrderBy(x => x).ToList(),
|
||||
UserId = input.UserId,
|
||||
PermanentBan = input.PermanentBan,
|
||||
RequestedAtUtc = input.RequestedAtUtc,
|
||||
Results = results,
|
||||
Canceled = true
|
||||
}));
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "ExternalApiKick failed on bot {BotId} (chats={Count})", group.Bot.Id, group.Chats.Count);
|
||||
foreach (var chat in group.Chats)
|
||||
{
|
||||
if (!recorded.Add(chat.TelegramId))
|
||||
continue;
|
||||
|
||||
results.Add(new KickResultItem(
|
||||
ChatId: chat.TelegramId.ToString(),
|
||||
Title: chat.Title,
|
||||
Success: false,
|
||||
Error: ex.Message));
|
||||
failed++;
|
||||
}
|
||||
|
||||
await host.UpdateProgressAsync(completed, failed, linkedToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (linkedCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(new KickTaskLog
|
||||
{
|
||||
ApiName = input.ApiName ?? "",
|
||||
BotId = configuredBotId,
|
||||
UseAllChats = useAllChats,
|
||||
ChatIds = chatIdSet.OrderBy(x => x).ToList(),
|
||||
UserId = input.UserId,
|
||||
PermanentBan = input.PermanentBan,
|
||||
RequestedAtUtc = input.RequestedAtUtc,
|
||||
Results = results,
|
||||
Canceled = true
|
||||
}));
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "ExternalApiKick failed (taskId={TaskId})", host.TaskId);
|
||||
await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(new KickTaskLog
|
||||
{
|
||||
ApiName = input.ApiName ?? "",
|
||||
BotId = configuredBotId,
|
||||
UseAllChats = useAllChats,
|
||||
ChatIds = chatIdSet.OrderBy(x => x).ToList(),
|
||||
UserId = input.UserId,
|
||||
PermanentBan = input.PermanentBan,
|
||||
RequestedAtUtc = input.RequestedAtUtc,
|
||||
Results = results,
|
||||
Error = ex.Message
|
||||
}));
|
||||
throw;
|
||||
}
|
||||
|
||||
await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(new KickTaskLog
|
||||
{
|
||||
ApiName = input.ApiName ?? "",
|
||||
BotId = configuredBotId,
|
||||
UseAllChats = useAllChats,
|
||||
ChatIds = chatIdSet.OrderBy(x => x).ToList(),
|
||||
UserId = input.UserId,
|
||||
PermanentBan = input.PermanentBan,
|
||||
RequestedAtUtc = input.RequestedAtUtc,
|
||||
Results = results
|
||||
}));
|
||||
}
|
||||
|
||||
private static async Task<TargetResolution> ResolveTargetsAsync(
|
||||
BotManagementService botManagement,
|
||||
int configuredBotId,
|
||||
bool useAllChats,
|
||||
HashSet<long> configuredChatIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var bots = (await botManagement.GetAllBotsAsync())
|
||||
.Where(b => b.IsActive)
|
||||
.OrderBy(b => b.Id)
|
||||
.ToList();
|
||||
|
||||
if (configuredBotId > 0)
|
||||
bots = bots.Where(b => b.Id == configuredBotId).ToList();
|
||||
|
||||
var groups = new List<TargetGroup>();
|
||||
var totalChats = 0;
|
||||
|
||||
foreach (var bot in bots)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var chats = (await botManagement.GetChatsAsync(bot.Id)).ToList();
|
||||
if (!useAllChats && configuredChatIds.Count > 0)
|
||||
chats = chats.Where(c => configuredChatIds.Contains(c.TelegramId)).ToList();
|
||||
|
||||
if (chats.Count == 0)
|
||||
continue;
|
||||
|
||||
groups.Add(new TargetGroup(bot, chats));
|
||||
totalChats += chats.Count;
|
||||
}
|
||||
|
||||
return new TargetResolution(groups, totalChats);
|
||||
}
|
||||
|
||||
private static string SerializeIndented<T>(T value)
|
||||
{
|
||||
return JsonSerializer.Serialize(value, new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
|
||||
private sealed record TargetGroup(Bot Bot, List<BotChannel> Chats);
|
||||
private sealed record TargetResolution(List<TargetGroup> Groups, int TotalChats);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
@@ -8,7 +7,6 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
using TelegramPanel.Core.Services;
|
||||
using TelegramPanel.Core.Services.Telegram;
|
||||
using TelegramPanel.Core.BatchTasks;
|
||||
using TelegramPanel.Data.Entities;
|
||||
|
||||
@@ -27,7 +25,6 @@ public static class KickApi
|
||||
HttpContext http,
|
||||
IConfiguration configuration,
|
||||
BotManagementService botManagement,
|
||||
BotTelegramService botTelegram,
|
||||
BatchTaskManagementService taskManagement,
|
||||
ILoggerFactory loggerFactory,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -86,10 +83,10 @@ public static class KickApi
|
||||
false,
|
||||
"未配置任何可操作的频道/群组(请先在页面选择机器人与频道/群组)",
|
||||
new KickSummary(0, 0, 0),
|
||||
Array.Empty<KickResultItem>()));
|
||||
Array.Empty<KickResultItem>()));
|
||||
}
|
||||
|
||||
// 记录到任务中心(即使外部调用返回了,也能追溯这次操作)
|
||||
// 提交到任务中心,由后台执行(避免反代/CF 524 超时)
|
||||
var createdTask = await taskManagement.CreateTaskAsync(new BatchTask
|
||||
{
|
||||
TaskType = BatchTaskTypes.ExternalApiKick,
|
||||
@@ -108,120 +105,17 @@ public static class KickApi
|
||||
})
|
||||
});
|
||||
|
||||
await taskManagement.StartTaskAsync(createdTask.Id);
|
||||
logger.LogInformation("Kick task queued: taskId={TaskId} api={ApiName} botId={BotId} total={Total} userId={UserId} permanent={Permanent}",
|
||||
createdTask.Id, matched.Name ?? "", configuredBotId, targets.TotalChats, request.UserId, permanentBan);
|
||||
|
||||
var results = new List<KickResultItem>(targets.TotalChats);
|
||||
var completed = 0;
|
||||
var failedCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var group in targets.Groups)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
var banResult = await botTelegram.BanChatMemberAsync(
|
||||
botId: group.Bot.Id,
|
||||
channelTelegramIds: group.Chats.Select(x => x.TelegramId).ToList(),
|
||||
userId: request.UserId,
|
||||
permanentBan: permanentBan,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
var failures = banResult.Failures;
|
||||
foreach (var chat in group.Chats)
|
||||
{
|
||||
failures.TryGetValue(chat.TelegramId, out var err);
|
||||
var ok = err == null;
|
||||
results.Add(new KickResultItem(
|
||||
ChatId: chat.TelegramId.ToString(),
|
||||
Title: chat.Title,
|
||||
Success: ok,
|
||||
Error: err));
|
||||
|
||||
if (ok) completed++; else failedCount++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Kick API failed on bot {BotId} (chats={Count})", group.Bot.Id, group.Chats.Count);
|
||||
foreach (var chat in group.Chats)
|
||||
{
|
||||
results.Add(new KickResultItem(
|
||||
ChatId: chat.TelegramId.ToString(),
|
||||
Title: chat.Title,
|
||||
Success: false,
|
||||
Error: ex.Message));
|
||||
failedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
await taskManagement.UpdateTaskProgressAsync(createdTask.Id, completed, failedCount);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await taskManagement.UpdateTaskProgressAsync(createdTask.Id, completed, failedCount == 0 ? 1 : failedCount);
|
||||
await taskManagement.UpdateTaskConfigAsync(createdTask.Id, SerializeIndented(new KickTaskLog
|
||||
{
|
||||
ApiName = matched.Name ?? "",
|
||||
BotId = configuredBotId,
|
||||
UseAllChats = useAllChats,
|
||||
ChatIds = configuredChatSet.OrderBy(x => x).ToList(),
|
||||
UserId = request.UserId,
|
||||
PermanentBan = permanentBan,
|
||||
RequestedAtUtc = DateTime.UtcNow,
|
||||
Results = results,
|
||||
Canceled = true
|
||||
}));
|
||||
await taskManagement.CompleteTaskAsync(createdTask.Id, success: false);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Kick API failed (taskId={TaskId})", createdTask.Id);
|
||||
await taskManagement.UpdateTaskProgressAsync(createdTask.Id, completed, failedCount == 0 ? 1 : failedCount);
|
||||
await taskManagement.UpdateTaskConfigAsync(createdTask.Id, SerializeIndented(new KickTaskLog
|
||||
{
|
||||
ApiName = matched.Name ?? "",
|
||||
BotId = configuredBotId,
|
||||
UseAllChats = useAllChats,
|
||||
ChatIds = configuredChatSet.OrderBy(x => x).ToList(),
|
||||
UserId = request.UserId,
|
||||
PermanentBan = permanentBan,
|
||||
RequestedAtUtc = DateTime.UtcNow,
|
||||
Results = results,
|
||||
Error = ex.Message
|
||||
}));
|
||||
await taskManagement.CompleteTaskAsync(createdTask.Id, success: false);
|
||||
throw;
|
||||
}
|
||||
|
||||
var okCount = results.Count(x => x.Success);
|
||||
var total = results.Count;
|
||||
var failedTotal = total - okCount;
|
||||
var actionText = permanentBan ? "Banned" : "Kicked";
|
||||
var message = $"{actionText} user {request.UserId} from {okCount}/{total} chats";
|
||||
|
||||
await taskManagement.UpdateTaskConfigAsync(createdTask.Id, SerializeIndented(new KickTaskLog
|
||||
{
|
||||
ApiName = matched.Name ?? "",
|
||||
BotId = configuredBotId,
|
||||
UseAllChats = useAllChats,
|
||||
ChatIds = configuredChatSet.OrderBy(x => x).ToList(),
|
||||
UserId = request.UserId,
|
||||
PermanentBan = permanentBan,
|
||||
RequestedAtUtc = DateTime.UtcNow,
|
||||
Results = results
|
||||
}));
|
||||
await taskManagement.CompleteTaskAsync(createdTask.Id, success: failedTotal == 0);
|
||||
|
||||
return Results.Ok(new KickResponse(
|
||||
Success: true,
|
||||
Message: message,
|
||||
Summary: new KickSummary(total, okCount, failedTotal),
|
||||
Results: results));
|
||||
return Results.Json(
|
||||
new KickResponse(
|
||||
Success: true,
|
||||
Message: $"已提交后台任务 #{createdTask.Id},请到面板【任务中心】查看进度与结果。",
|
||||
Summary: new KickSummary(targets.TotalChats, 0, 0),
|
||||
Results: Array.Empty<KickResultItem>(),
|
||||
TaskId: createdTask.Id),
|
||||
statusCode: StatusCodes.Status202Accepted);
|
||||
}
|
||||
|
||||
private static async Task<TargetResolution> ResolveTargetsAsync(
|
||||
@@ -280,41 +174,7 @@ public static class KickApi
|
||||
return JsonSerializer.Serialize(value, new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
|
||||
public sealed record KickRequest(
|
||||
[property: JsonPropertyName("user_id")] long UserId,
|
||||
[property: JsonPropertyName("permanent_ban")] bool? PermanentBan = null);
|
||||
|
||||
public sealed record KickResponse(
|
||||
[property: JsonPropertyName("success")] bool Success,
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("summary")] KickSummary Summary,
|
||||
[property: JsonPropertyName("results")] IReadOnlyList<KickResultItem> Results);
|
||||
|
||||
public sealed record KickSummary(
|
||||
[property: JsonPropertyName("total")] int Total,
|
||||
[property: JsonPropertyName("success")] int Success,
|
||||
[property: JsonPropertyName("failed")] int Failed);
|
||||
|
||||
public sealed record KickResultItem(
|
||||
[property: JsonPropertyName("chat_id")] string ChatId,
|
||||
[property: JsonPropertyName("title")] string Title,
|
||||
[property: JsonPropertyName("success")] bool Success,
|
||||
[property: JsonPropertyName("error")] string? Error);
|
||||
|
||||
private sealed record TargetGroup(Bot Bot, List<BotChannel> Chats);
|
||||
private sealed record TargetResolution(List<TargetGroup> Groups, int TotalChats);
|
||||
|
||||
private sealed class KickTaskLog
|
||||
{
|
||||
public string ApiName { get; set; } = "";
|
||||
public int BotId { get; set; }
|
||||
public bool UseAllChats { get; set; }
|
||||
public List<long> ChatIds { get; set; } = new();
|
||||
public long UserId { get; set; }
|
||||
public bool PermanentBan { get; set; }
|
||||
public DateTime RequestedAtUtc { get; set; }
|
||||
public bool Canceled { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public List<KickResultItem>? Results { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace TelegramPanel.Web.ExternalApi;
|
||||
|
||||
public sealed record KickRequest(
|
||||
[property: JsonPropertyName("user_id")] long UserId,
|
||||
[property: JsonPropertyName("permanent_ban")] bool? PermanentBan = null);
|
||||
|
||||
public sealed record KickResponse(
|
||||
[property: JsonPropertyName("success")] bool Success,
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("summary")] KickSummary Summary,
|
||||
[property: JsonPropertyName("results")] IReadOnlyList<KickResultItem> Results,
|
||||
[property: JsonPropertyName("task_id")] int? TaskId = null);
|
||||
|
||||
public sealed record KickSummary(
|
||||
[property: JsonPropertyName("total")] int Total,
|
||||
[property: JsonPropertyName("success")] int Success,
|
||||
[property: JsonPropertyName("failed")] int Failed);
|
||||
|
||||
public sealed record KickResultItem(
|
||||
[property: JsonPropertyName("chat_id")] string ChatId,
|
||||
[property: JsonPropertyName("title")] string Title,
|
||||
[property: JsonPropertyName("success")] bool Success,
|
||||
[property: JsonPropertyName("error")] string? Error);
|
||||
|
||||
public sealed class KickTaskLog
|
||||
{
|
||||
[JsonPropertyName("api_name")]
|
||||
public string ApiName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 0=all bots
|
||||
/// </summary>
|
||||
[JsonPropertyName("bot_id")]
|
||||
public int BotId { get; set; }
|
||||
|
||||
[JsonPropertyName("use_all_chats")]
|
||||
public bool UseAllChats { get; set; }
|
||||
|
||||
[JsonPropertyName("chat_ids")]
|
||||
public List<long> ChatIds { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("user_id")]
|
||||
public long UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("permanent_ban")]
|
||||
public bool PermanentBan { get; set; }
|
||||
|
||||
[JsonPropertyName("requested_at_utc")]
|
||||
public DateTime RequestedAtUtc { get; set; }
|
||||
|
||||
[JsonPropertyName("canceled")]
|
||||
public bool Canceled { get; set; }
|
||||
|
||||
[JsonPropertyName("error")]
|
||||
public string? Error { get; set; }
|
||||
|
||||
[JsonPropertyName("results")]
|
||||
public List<KickResultItem>? Results { get; set; }
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MudBlazor;
|
||||
using TelegramPanel.Modules;
|
||||
using TelegramPanel.Web.Components.ModulePages;
|
||||
using TelegramPanel.Web.ExternalApi;
|
||||
|
||||
namespace TelegramPanel.Web.Modules.BuiltIn;
|
||||
|
||||
public sealed class KickApiModule : ITelegramPanelModule, IModuleApiProvider
|
||||
public sealed class KickApiModule : ITelegramPanelModule, IModuleApiProvider, IModuleUiProvider
|
||||
{
|
||||
public KickApiModule(string version)
|
||||
{
|
||||
@@ -25,7 +27,7 @@ public sealed class KickApiModule : ITelegramPanelModule, IModuleApiProvider
|
||||
|
||||
public void ConfigureServices(IServiceCollection services, ModuleHostContext context)
|
||||
{
|
||||
// built-in:无需额外注入
|
||||
services.AddScoped<IModuleTaskHandler, ExternalApiKickTaskHandler>();
|
||||
}
|
||||
|
||||
public void MapEndpoints(IEndpointRouteBuilder endpoints, ModuleHostContext context)
|
||||
@@ -49,4 +51,22 @@ public sealed class KickApiModule : ITelegramPanelModule, IModuleApiProvider
|
||||
Order = 10
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<ModuleNavItem> GetNavItems(ModuleHostContext context)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
public IEnumerable<ModulePageDefinition> GetPages(ModuleHostContext context)
|
||||
{
|
||||
yield return new ModulePageDefinition
|
||||
{
|
||||
Key = "kick",
|
||||
Title = "踢人/封禁",
|
||||
Icon = Icons.Material.Filled.PersonRemove,
|
||||
Group = "外部 API",
|
||||
Order = 10,
|
||||
ComponentType = typeof(KickApiPage).AssemblyQualifiedName ?? ""
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -262,7 +262,13 @@ if (serilogEnabled)
|
||||
{
|
||||
loggerConfig = loggerConfig
|
||||
.ReadFrom.Configuration(builder.Configuration)
|
||||
.WriteTo.File("logs/telegram-panel-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: retainedFileCountLimit);
|
||||
// buffered=true:降低磁盘抖动/IO 阻塞导致的请求卡顿风险(尤其是低配 VPS/挂载卷/杀软扫描场景)
|
||||
.WriteTo.File(
|
||||
"logs/telegram-panel-.log",
|
||||
rollingInterval: RollingInterval.Day,
|
||||
retainedFileCountLimit: retainedFileCountLimit,
|
||||
buffered: true,
|
||||
flushToDiskInterval: TimeSpan.FromSeconds(1));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user