diff --git a/Directory.Build.props b/Directory.Build.props index 7cb3e26..ca0d35d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,10 +5,10 @@ - 1.2.7 - 1.2.7.0 - 1.2.7.0 - 1.2.7 + 1.2.8 + 1.2.8.0 + 1.2.8.0 + 1.2.8 Telegram Panel Contributors diff --git a/README.md b/README.md index a39b582..8587843 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,19 @@ 基于 **WTelegramClient** 的 Telegram 多账户管理面板(.NET 8 / Blazor Server),用于批量管理账号、统计与管理频道/群组、执行批量任务。 -## 社区 +## 目录 + +- [社区与交流](#社区与交流) +- [功能亮点](#功能亮点) +- [Docker 一键部署(推荐)](#docker-一键部署推荐) +- [反向代理一条龙(可选)](#反向代理一条龙可选) +- [本地开发运行(可选)](#本地开发运行可选) +- [模块扩展(可选)](#模块扩展可选) +- [详细文档](#详细文档) +- [防冻结指南(新号必看)](#防冻结指南新号必看) +- [截图](#截图) + +## 社区与交流 - TG 频道:https://t.me/zhanzhangck - 站长交流群:https://t.me/vpsbbq @@ -21,36 +33,9 @@ - 🤖 **Bot 频道管理**:用于管理“频道创建人不在系统中”的频道(把 Bot 设为管理员即可纳入管理),支持批量导出链接、批量邀请成员/设置管理员(可按管理员列表匹配系统账号执行);支持对每个 Bot 一键启用/停用(停用后相关后台不再轮询 getUpdates) - 🏷️ **账号分类(排除操作)**:支持把“养号/不参与运营”的账号分类标记为“排除操作”,不会出现在创建频道/批量任务的执行账号下拉里 +## Docker 一键部署(推荐) -## 🧊 防冻结指南(新号必看) - -Telegram 对新号/风控号的限制比较敏感;本项目提供了很多“批量/高频”能力,请务必谨慎使用。 - -- ⚠️ **新号切记:登录面板后不要进行任何操作!!!至少养 24 小时再创建频道/群、批量邀请/加管理员等敏感操作** -- ✅ **如果是购买的协议号/接码号(需要尽快验号)**:建议导入后先做一次「批量 Telegram 状态检测」,必要时用「深度检测」快速筛掉废号(避免隔天再找商家被拒赔付);验号通过后再开始低频养号与后续操作 -- ⚠️ **重要提醒(买号必看)**:如果你是用商家提供的 Session 导入到系统里,本质上是在使用“同一个会话”。因此“踢设备”只能踢掉**除当前 Session 会话之外**的其他设备;若商家仍保留该 Session,依然有被重复使用的风险。想要更安全:务必用验证码登录一次(以你自己的登录为准),再踢出其他 Session 会话,并绑定 2FA 找回邮箱 -- ✅ 养号完成后:先从少量、低频操作开始,逐步增加频率(宁可慢点,也别一上来就批量) -- ⛔ 如果账号出现限制/冻结迹象:建议先停用该账号,等待恢复后再继续操作 -- 📧 **重要账号务必绑定 2FA 找回邮箱**:尤其是接码手机号的账号,存在“官方客户端也可能突然掉登录”的情况;若未绑定邮箱,掉线后可能无法找回(账号就丢了)。建议准备一个稳定的域名邮箱,把重要账号都绑定到同一域名邮箱体系,便于后续通过邮箱验证码重新登录。非重要账号可酌情忽略。 - -## 截图 - -> 仓库自带后台截图:`screenshot/` - -
-点击展开/收起截图 - -| | | | -|---|---|---| -| | | | -| | | | -| | | | - -
- -## 🐳 Docker 一键部署(推荐) - -面向小白:**`git clone` → `docker compose up` → 浏览器打开 → 登录改密码 → 配置 ApiId/ApiHash**。 +🐳 面向小白:**`git clone` → `docker compose up` → 浏览器打开 → 登录改密码 → 配置 ApiId/ApiHash**。 ### 环境要求 @@ -93,9 +78,9 @@ https://faka.boxmoe.eu.org/ ### Bot Webhook 模式(可选) -> **💡 提示**:如果你不使用BOT相关功能,可以跳过此节。 +> **💡 提示**:如果你不使用 BOT 相关功能,可以跳过此节。 -默认情况下,Bot 使用**长轮询(Long Polling)**模式接收更新。**生产环境建议使用 Webhook 模式**,优势如下: +默认情况下,Bot 使用**长轮询(Long Polling)**模式接收更新。生产环境可选 Webhook: - ✅ 更低的资源消耗(无需持续轮询) - ✅ 更快的响应速度(Telegram 主动推送) @@ -222,7 +207,7 @@ docker compose build --no-cache docker compose up -d ``` -## 🌐 反向代理一条龙(可选) +## 反向代理一条龙(可选) Blazor Server 需要 WebSocket(`/_blazor`),反代必须支持 `Upgrade`。 @@ -249,7 +234,7 @@ location / { dotnet run --project src/TelegramPanel.Web ``` -## 🧩 模块扩展(可选) +## 模块扩展(可选) 项目已支持“可安装/可卸载”的模块扩展(任务 / API / UI),用于后续按需扩展能力、按场景自由定制开发自己的模块,而不是把功能写死在核心里。 @@ -284,3 +269,34 @@ dotnet run --project src/TelegramPanel.Web - `docs/database.md`(数据库/表结构说明) - `docs/advanced.md`(配置项/数据目录/后台任务等) - `docs/modules.md`(模块系统:可安装/可卸载/依赖/回滚) + +## 防冻结指南(新号必看) + +
+点击展开/收起防冻结指南 + +Telegram 对新号/风控号的限制比较敏感;本项目提供了很多“批量/高频”能力,请务必谨慎使用。 + +- ⚠️ **新号切记:登录面板后不要进行任何操作!!!至少养 24 小时再创建频道/群、批量邀请/加管理员等敏感操作** +- ✅ **如果是购买的协议号/接码号(需要尽快验号)**:建议导入后先做一次「批量 Telegram 状态检测」,必要时用「深度检测」快速筛掉废号(避免隔天再找商家被拒赔付);验号通过后再开始低频养号与后续操作 +- ⚠️ **重要提醒(买号必看)**:如果你是用商家提供的 Session 导入到系统里,本质上是在使用“同一个会话”。因此“踢设备”只能踢掉**除当前 Session 会话之外**的其他设备;若商家仍保留该 Session,依然有被重复使用的风险。想要更安全:务必用验证码登录一次(以你自己的登录为准),再踢出其他 Session 会话,并绑定 2FA 找回邮箱 +- ✅ 养号完成后:先从少量、低频操作开始,逐步增加频率(宁可慢点,也别一上来就批量) +- ⛔ 如果账号出现限制/冻结迹象:建议先停用该账号,等待恢复后再继续操作 +- 📧 **重要账号务必绑定 2FA 找回邮箱**:尤其是接码手机号的账号,存在“官方客户端也可能突然掉登录”的情况;若未绑定邮箱,掉线后可能无法找回(账号就丢了)。建议准备一个稳定的域名邮箱,把重要账号都绑定到同一域名邮箱体系,便于后续通过邮箱验证码重新登录。非重要账号可酌情忽略。 + +
+ +## 截图 + +> 仓库自带后台截图:`screenshot/` + +
+点击展开/收起截图 + +| | | | +|---|---|---| +| | | | +| | | | +| | | | + +
diff --git a/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs b/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs index 8ba2f1c..c3b16ad 100644 --- a/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs +++ b/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs @@ -576,25 +576,32 @@ public class ChannelService : IChannelService #region Private Methods - private static long NormalizeChannelId(long channelId) + private static IReadOnlyList GetCandidateChannelIds(long channelId) { - // Bot API 的频道/超级群 chat_id 通常形如:-100xxxxxxxxxx(即 -1000000000000 - channel_id) - // MTProto 的 channel_id 为 int32(可能为负),WTelegram 的 Channel.id 也基于该 int32。 - // 因此这里把 -100... 规范化回 MTProto 的 int32 id,避免出现 “Channel -100... not found”。 + // 兼容 Bot API 的 chat_id 形式:-100xxxxxxxxxx + // 不同库对 channel_id 的表示可能存在 unsigned/signed 差异,这里同时尝试多个候选值匹配 dialogs 里的 Channel.id。 + var list = new List(capacity: 3) { channelId }; + if (channelId <= -1000000000000L) { - var raw = -(channelId + 1000000000000L); - return unchecked((int)raw); + // Bot chat_id = -1000000000000 - channel_id + var raw = (-channelId) - 1000000000000L; + list.Add(raw); + list.Add(unchecked((int)raw)); } - return channelId; + return list.Distinct().ToList(); } private async Task GetChannelByIdAsync(Client client, long channelId) { - channelId = NormalizeChannelId(channelId); + var candidates = GetCandidateChannelIds(channelId); + var candidateSet = candidates.Count == 1 ? null : candidates.ToHashSet(); + var dialogs = await client.Messages_GetAllDialogs(); - return dialogs.chats.Values.OfType().FirstOrDefault(c => c.id == channelId); + return dialogs.chats.Values + .OfType() + .FirstOrDefault(c => candidateSet?.Contains(c.id) ?? c.id == channelId); } private async Task GetOrCreateConnectedClientAsync(int accountId) diff --git a/src/TelegramPanel.Web/Components/Dialogs/BotChannelBatchAdminsDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/BotChannelBatchAdminsDialog.razor index 4ed6de8..7eda21c 100644 --- a/src/TelegramPanel.Web/Components/Dialogs/BotChannelBatchAdminsDialog.razor +++ b/src/TelegramPanel.Web/Components/Dialogs/BotChannelBatchAdminsDialog.razor @@ -1,6 +1,7 @@ @inject AccountManagementService AccountManagement @inject IChannelService ChannelService @inject BotTelegramService BotTelegram +@inject AccountTelegramToolsService AccountTools @inject ISnackbar Snackbar @inject IDialogService DialogService @inject TelegramPanel.Web.Services.ChannelAdminDefaultsService DefaultsService @@ -417,6 +418,8 @@ try { + var joinTried = new HashSet(StringComparer.Ordinal); + foreach (var ch in channels) { var executorId = ResolveExecutorAccountId(ch); @@ -449,6 +452,35 @@ failures.Add($"{ch.Title} => {username}:失败"); } } + catch (Exception ex) when (!joinTried.Contains($"{executorId.Value}:{ch.TelegramId}") && BotChannelJoinRetryHelper.LooksLikeChannelNotFound(ex.Message)) + { + var key = $"{executorId.Value}:{ch.TelegramId}"; + joinTried.Add(key); + + var joined = await BotChannelJoinRetryHelper.TryJoinChannelAsync( + BotTelegram, AccountTools, BotId, executorId.Value, ch, failures, _cts.Token); + if (!joined) + { + failed++; + failures.Add($"{ch.Title} => {username}:{ex.Message}"); + continue; + } + + try + { + var ok = await ChannelService.SetAdminAsync(executorId.Value, ch.TelegramId, username, SelectedRights, title); + if (!ok) + { + failed++; + failures.Add($"{ch.Title} => {username}:失败"); + } + } + catch (Exception ex2) + { + failed++; + failures.Add($"{ch.Title} => {username}:{ex2.Message}"); + } + } catch (Exception ex) { failed++; diff --git a/src/TelegramPanel.Web/Components/Dialogs/BotChannelBatchInviteDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/BotChannelBatchInviteDialog.razor index 94a02a2..e49c2c6 100644 --- a/src/TelegramPanel.Web/Components/Dialogs/BotChannelBatchInviteDialog.razor +++ b/src/TelegramPanel.Web/Components/Dialogs/BotChannelBatchInviteDialog.razor @@ -1,6 +1,7 @@ @inject AccountManagementService AccountManagement @inject IChannelService ChannelService @inject BotTelegramService BotTelegram +@inject AccountTelegramToolsService AccountTools @inject ISnackbar Snackbar @inject IDialogService DialogService @inject TelegramPanel.Web.Services.ChannelInvitePresetsService PresetsService @@ -348,6 +349,8 @@ try { + var joinTried = new HashSet(StringComparer.Ordinal); + foreach (var ch in channels) { var executorId = ResolveExecutorAccountId(ch); @@ -380,6 +383,35 @@ failures.Add($"{ch.Title} => {username}:{result.Error}"); } } + catch (Exception ex) when (!joinTried.Contains($"{executorId.Value}:{ch.TelegramId}") && BotChannelJoinRetryHelper.LooksLikeChannelNotFound(ex.Message)) + { + var key = $"{executorId.Value}:{ch.TelegramId}"; + joinTried.Add(key); + + var joined = await BotChannelJoinRetryHelper.TryJoinChannelAsync( + BotTelegram, AccountTools, BotId, executorId.Value, ch, failures, _cts.Token); + if (!joined) + { + failed++; + failures.Add($"{ch.Title} => {username}:{ex.Message}"); + continue; + } + + try + { + var result = await ChannelService.InviteUserAsync(executorId.Value, ch.TelegramId, username); + if (!result.Success) + { + failed++; + failures.Add($"{ch.Title} => {username}:{result.Error}"); + } + } + catch (Exception ex2) + { + failed++; + failures.Add($"{ch.Title} => {username}:{ex2.Message}"); + } + } catch (Exception ex) { failed++; diff --git a/src/TelegramPanel.Web/Services/BotChannelJoinRetryHelper.cs b/src/TelegramPanel.Web/Services/BotChannelJoinRetryHelper.cs new file mode 100644 index 0000000..141a5c9 --- /dev/null +++ b/src/TelegramPanel.Web/Services/BotChannelJoinRetryHelper.cs @@ -0,0 +1,45 @@ +using TelegramPanel.Core.Services.Telegram; +using TelegramPanel.Data.Entities; + +namespace TelegramPanel.Web.Services; + +public static class BotChannelJoinRetryHelper +{ + public static bool LooksLikeChannelNotFound(string? message) + { + if (string.IsNullOrWhiteSpace(message)) + return false; + + return message.Contains("Channel", StringComparison.OrdinalIgnoreCase) + && message.Contains("not found", StringComparison.OrdinalIgnoreCase); + } + + public static async Task TryJoinChannelAsync( + BotTelegramService botTelegram, + AccountTelegramToolsService accountTools, + int botId, + int accountId, + BotChannel channel, + List failures, + CancellationToken cancellationToken) + { + try + { + var link = await botTelegram.ExportInviteLinkAsync(botId, channel.TelegramId, cancellationToken); + var (success, error, _) = await accountTools.JoinChatOrChannelAsync(accountId, link, cancellationToken); + if (!success) + { + failures.Add($"{channel.Title}:执行账号尝试加入频道失败:{error}"); + return false; + } + + return true; + } + catch (Exception ex) + { + failures.Add($"{channel.Title}:执行账号尝试加入频道失败:{ex.Message}"); + return false; + } + } +} +