├── src
├── Program.cs
├── TelegramStickerPorter.xml
├── Model
│ └── TelegramOptions.cs
├── SingleFilePublish.cs
├── GlobalUsings.cs
├── appsettings.json
├── Properties
│ └── launchSettings.json
├── TelegramStickerPorter.csproj
├── Startup.cs
└── Service
│ ├── AddTelegramServices.cs
│ ├── TelegramJob.cs
│ ├── LoggingSetup.cs
│ ├── MessageService.cs
│ ├── TelegramBotClientManager.cs
│ ├── TelegramBotBackgroundService.cs
│ └── StickerService.cs
├── Dockerfile
├── .gitignore
├── TelegramStickerPorter.sln
├── README.md
├── .github
└── workflows
│ └── build-release-docker.yml
└── LICENSE
/src/Program.cs:
--------------------------------------------------------------------------------
1 | Serve.Run(RunOptions.Default.WithArgs(args));
--------------------------------------------------------------------------------
/src/TelegramStickerPorter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | TelegramStickerPorter
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/Model/TelegramOptions.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramStickerPorter;
2 |
3 | public class TelegramOptions
4 | {
5 | public int ApiId { get; set; }
6 | public string ApiHash { get; set; }
7 | public string BotToken { get; set; }
8 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/runtime-deps:9.0-bookworm-slim
2 | WORKDIR /app
3 | ARG BIN_NAME=TelegramStickerPorter
4 | ARG TARGETARCH
5 | COPY out/linux-${TARGETARCH}/ /app/
6 | RUN chmod +x /app/${BIN_NAME}
7 | EXPOSE 5000
8 | ENTRYPOINT ["/app/TelegramStickerPorter"]
--------------------------------------------------------------------------------
/src/SingleFilePublish.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramStickerPorter;
2 |
3 | public class SingleFilePublish : ISingleFilePublish
4 | {
5 | public Assembly[] IncludeAssemblies()
6 | {
7 | return Array.Empty();
8 | }
9 |
10 | public string[] IncludeAssemblyNames()
11 | {
12 | return new[]
13 | {
14 | "TelegramStickerPorter"
15 | };
16 | }
17 | }
--------------------------------------------------------------------------------
/src/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using Furion;
2 | global using Furion.FriendlyException;
3 | global using Furion.Schedule;
4 | global using Microsoft.CodeAnalysis;
5 | global using Microsoft.Extensions.Logging.Console;
6 | global using System.Reflection;
7 | global using System.Text;
8 | global using Telegram.Bot.Types;
9 | global using Telegram.Bot.Types.Enums;
10 | global using Telegram.Bot.Types.ReplyMarkups;
11 | global using TelegramStickerPorter;
12 | global using WTelegram;
--------------------------------------------------------------------------------
/src/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
3 | "Logging": {
4 | "LogLevel": {
5 | "Default": "Information",
6 | "Microsoft.AspNetCore": "Warning",
7 | "Microsoft.EntityFrameworkCore": "Information"
8 | }
9 | },
10 | "AppSettings": {
11 | "InjectSpecificationDocument": false
12 | },
13 | "Telegram": {
14 | "ApiId": "23319500",
15 | "ApiHash": "814ac0dd67f660119b9b990d514c9a47",
16 | "BotToken": "botToken"
17 | },
18 | "AllowedHosts": "*"
19 | }
--------------------------------------------------------------------------------
/src/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "http": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": false,
8 | "applicationUrl": "http://localhost:5040",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development"
11 | }
12 | },
13 | "https": {
14 | "commandName": "Project",
15 | "dotnetRunMessages": true,
16 | "launchBrowser": false,
17 | "applicationUrl": "https://localhost:7163;http://localhost:5040",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/TelegramStickerPorter.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | disable
6 | enable
7 | 1701;1702;1591
8 | TelegramStickerPorter.xml
9 | en-US
10 | true
11 |
12 |
13 | none
14 | false
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # === .NET Core / ASP.NET Core ===
2 | bin/
3 | obj/
4 | out/
5 | *.user
6 | *.suo
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # VSCode / Rider / Visual Studio settings
11 | .vscode/
12 | .idea/
13 | .vs/
14 |
15 | # ASP.NET Core environment-specific config
16 | appsettings.Development.json
17 | appsettings.*.local.json
18 |
19 | # Logs
20 | *.log
21 | logs/
22 |
23 | # Temporary files
24 | *.tmp
25 | *.temp
26 |
27 | # SQLite / LiteDB / local DBs
28 | *.db
29 | *.sqlite
30 | *.db-shm
31 | *.db-wal
32 |
33 | # Secret user settings (if any)
34 | secrets.json
35 |
36 | # JetBrains Rider settings
37 | .idea/
38 |
39 | # Build results
40 | project.lock.json
41 | project.fragment.lock.json
42 |
43 | # Npm / frontend stuff (if used)
44 | node_modules/
45 |
46 | # Publish output
47 | publish/
48 | dist/
49 | wwwroot/lib/
50 |
51 | # ReSharper
52 | _ReSharper*/
53 | *.[Rr]e[Ss]harper
54 | *.DotSettings.user
55 |
56 | # Mac OS / Linux / Windows system files
57 | .DS_Store
58 | Thumbs.db
59 | desktop.ini
60 |
--------------------------------------------------------------------------------
/src/Startup.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramStickerPorter;
2 |
3 | public class Startup : AppStartup
4 | {
5 | public void ConfigureServices(IServiceCollection services)
6 | {
7 | services.AddLoggingSetup();
8 |
9 | services.AddConsoleFormatter();
10 |
11 | services.AddControllers()
12 | .AddInjectWithUnifyResult();
13 | services.AddSchedule(options =>
14 | {
15 | options.LogEnabled = true;
16 | options.AddJob(App.EffectiveTypes.ScanToBuilders());
17 | });
18 | services.AddTelegram();
19 | }
20 |
21 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
22 | {
23 | if (env.IsDevelopment())
24 | {
25 | app.UseDeveloperExceptionPage();
26 | }
27 |
28 | app.UseRouting();
29 |
30 | app.UseInject(string.Empty);
31 |
32 | app.UseEndpoints(endpoints =>
33 | {
34 | endpoints.MapControllers();
35 | });
36 | }
37 | }
--------------------------------------------------------------------------------
/TelegramStickerPorter.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.3.32519.111
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TelegramStickerPorter", "src\TelegramStickerPorter.csproj", "{F8FC080C-1CA0-4EF3-8833-153190C02AB3}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {F8FC080C-1CA0-4EF3-8833-153190C02AB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {F8FC080C-1CA0-4EF3-8833-153190C02AB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {F8FC080C-1CA0-4EF3-8833-153190C02AB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {F8FC080C-1CA0-4EF3-8833-153190C02AB3}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {B2073C2C-0FD3-452B-8047-8134D68E12CE}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/src/Service/AddTelegramServices.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramStickerPorter;
2 |
3 | public static class AddTelegramServices
4 | {
5 | public static IServiceCollection AddTelegram(this IServiceCollection services)
6 | {
7 | ConfigureWTelegramLogging();
8 |
9 | services.AddSingleton();
10 | services.AddSingleton();
11 | services.AddHostedService(sp => sp.GetRequiredService());
12 | services.AddSingleton();
13 | services.AddSingleton();
14 |
15 | return services;
16 | }
17 |
18 | private static void ConfigureWTelegramLogging()
19 | {
20 | var logDirectory = Path.Combine(AppContext.BaseDirectory, "logs");
21 | Directory.CreateDirectory(logDirectory);
22 |
23 | var logFilePath = Path.Combine(logDirectory, "TelegramBot.log");
24 | var logWriter = new StreamWriter(logFilePath, true, Encoding.UTF8) { AutoFlush = true };
25 |
26 | WTelegram.Helpers.Log = (lvl, str) =>
27 | {
28 | var logLevel = "TDIWE!"[lvl];
29 | var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
30 |
31 | lock (logWriter)
32 | {
33 | logWriter.WriteLine($"{timestamp} [{logLevel}] {str}");
34 | }
35 | };
36 | }
37 | }
--------------------------------------------------------------------------------
/src/Service/TelegramJob.cs:
--------------------------------------------------------------------------------
1 | [JobDetail("job_bot_monitor", Description = "机器人检测", GroupName = "default")]
2 | [PeriodMinutes(5, TriggerId = "trigger_bot_monitor", Description = "每5分钟检测一次", RunOnStart = false)]
3 | public class TelegramJob : IJob
4 | {
5 | private readonly ILogger _logger;
6 | private readonly TelegramBotClientManager _telegramBotClientManager;
7 | private readonly TelegramBotBackgroundService _telegramBotBackgroundService;
8 |
9 | public TelegramJob(
10 | ILogger logger,
11 | TelegramBotClientManager telegramBotClientManager,
12 | TelegramBotBackgroundService telegramBotBackgroundService)
13 | {
14 | _logger = logger;
15 | _telegramBotClientManager = telegramBotClientManager;
16 | _telegramBotBackgroundService = telegramBotBackgroundService;
17 | }
18 |
19 | public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
20 | {
21 | _logger.LogInformation("检测机器人状态");
22 | var isAlive = await _telegramBotClientManager.CanPingTelegramAsync();
23 |
24 | if (!isAlive)
25 | {
26 | _logger.LogInformation("机器人不响应,尝试重新初始化...");
27 | try
28 | {
29 | await _telegramBotBackgroundService.RestartBotAsync(stoppingToken);
30 | }
31 | catch (Exception ex)
32 | {
33 | _logger.LogError(ex, "重新初始化机器人实例失败");
34 | }
35 | }
36 | else
37 | {
38 | _logger.LogInformation("机器人正常运行中");
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/src/Service/LoggingSetup.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramStickerPorter;
2 |
3 | public static class LoggingSetup
4 | {
5 | public static void AddLoggingSetup(this IServiceCollection services)
6 | {
7 | services.AddMonitorLogging(options =>
8 | {
9 | options.IgnorePropertyNames = new[] { "Byte" };
10 | options.IgnorePropertyTypes = new[] { typeof(byte[]) };
11 | });
12 |
13 | services.AddConsoleFormatter(options =>
14 | {
15 | options.DateFormat = "yyyy-MM-dd HH:mm:ss(zzz) dddd";
16 | options.ColorBehavior = LoggerColorBehavior.Enabled;
17 | });
18 |
19 | ConfigureFileLogging(services);
20 | }
21 |
22 | private static void ConfigureFileLogging(IServiceCollection services)
23 | {
24 | LogLevel[] logLevels = { LogLevel.Information, LogLevel.Warning, LogLevel.Error };
25 |
26 | foreach (var logLevel in logLevels)
27 | {
28 | services.AddFileLogging(options =>
29 | {
30 | options.WithTraceId = true;
31 | options.WithStackFrame = true;
32 |
33 | options.FileNameRule = _ =>
34 | {
35 | string logsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs");
36 | Directory.CreateDirectory(logsDir);
37 |
38 | string fileName = $"{DateTime.Now:yyyy-MM-dd}_{logLevel}.log";
39 | return Path.Combine(logsDir, fileName);
40 | };
41 | options.WriteFilter = logMsg => logMsg.LogLevel == logLevel;
42 | options.HandleWriteError = writeError =>
43 | {
44 | writeError.UseRollbackFileName(Path.GetFileNameWithoutExtension(writeError.CurrentFileName) + "-oops" + Path.GetExtension(writeError.CurrentFileName));
45 | };
46 | });
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🚀 TelegramStickerPorter
2 |
3 | > 一个优雅且高效的 Telegram 贴纸 / 表情克隆工具
4 | > 支持 **贴纸包搬运**、**表情包克隆**,一键转移,极简操作!
5 | >
6 | > 演示机器人 [@StickerPorter_Bot](https://t.me/StickerPorter_Bot)
7 |
8 | ------
9 |
10 | ## ✨ 项目亮点
11 |
12 | - 💎 支持 Telegram 贴纸 和 表情 的克隆操作
13 | - 🧠 智能识别用户输入格式
14 | - 🔄 支持命令格式解析自动克隆
15 | - ⚙️ 基于 .NET 9.0 构建,性能可靠
16 | - 🔐 使用 Telegram Bot API,简单配置即可部署
17 | - 🧩 本软件免费无毒,可在虚拟机中运行进行长期挂机。
18 |
19 | ------
20 |
21 | ### AD -- 机场推广
22 |
23 |
24 |
25 | **机场 - 老百姓自己的机场**:[https://老百姓自己的机场.com](https://xn--mes53dm4ex3lhhtdb891k3sd.com/)
26 | 咱老百姓就得用自己的机场 **老百姓自己的机场** 做用的起的机场
27 |
28 |
29 |
30 | ## 🛠 环境要求
31 |
32 | - 最新发布版下载:https://github.com/Riniba/TelegramStickerPorter/releases/latest
33 | - 发布包提供常见的系统版本已经包含运行时。
34 | - 如需其他可自行编译
35 | - 请注意 使用时需具备**全局代理**或能**直连 Telegram**。
36 | - 如果使用的`v2rayN`或者`Clash`等代理软件,**请开启Tun**
37 | - 推荐直接部署到服务器
38 | - 必须拥有一个已申请的 Telegram Bot Token
39 | - 部署教程(适用于小白用户):[点击查看 Wiki](https://github.com/riniba/TelegramStickerPorter/wiki)
40 |
41 |
42 |
43 | ## 📦 功能关键词
44 |
45 | | 功能 | 关键词 |
46 | | :---------------- | :-------------------- |
47 | | Telegram 贴纸克隆 | TelegramStickerPorter |
48 | | Telegram 表情搬运 | 电报贴纸搬运 |
49 | | 电报表情克隆 | telegram表情克隆 |
50 | | 电报贴纸搬运 | telegram贴纸搬运 |
51 |
52 | ------
53 |
54 | ## 📋 使用说明
55 |
56 | 用户与机器人对话时,只需发送以下格式命令:
57 |
58 | ```
59 | 💎 贴纸/表情克隆使用说明 💎
60 |
61 | 请输入您想要的目标贴纸包(或表情包)的名称,以及需要克隆的原始贴纸包(或表情包)链接,格式如下:
62 |
63 | 克隆#您的贴纸包(或表情包)名称#需要克隆的贴纸包(或表情包)链接
64 |
65 | 例如:
66 | 克隆#我的可爱表情包#https://t.me/addstickers/pack_bafb8ef1_by_stickerporter_bot
67 | 克隆#我的酷酷的贴纸包#https://t.me/addemoji/pack_7f810f59_by_stickerporter_bot
68 |
69 | 🔹 克隆:命令前缀,触发克隆操作
70 | 🔹 您的贴纸包(或表情包)名称:您希望克隆后新包的名称
71 | 🔹 原始链接:要复制的 Telegram 表情/贴纸包链接
72 |
73 | 请确保信息填写正确,以便程序顺利克隆哦~ 🚀
74 | ```
75 |
76 | ------
77 |
78 | ## 📃 License
79 |
80 | [MIT License](https://github.com/Riniba/TelegramStickerPorter/blob/main/LICENSE)
81 |
82 | ------
83 |
84 | ## ❤️ 其他
85 |
86 | > 🗨️ 有问题或者建议欢迎提 Issue,也可以直接发 PR!
87 | >
88 | > - Telegram 私聊:https://t.me/Riniba
89 | > - Telegram 频道:https://t.me/RinibaChannel
90 | > - Telegram 群组:https://t.me/RinibaGroup
91 | >
92 | > 本项目目标是打造最简洁的 Telegram 贴纸搬运工具 🛠️
--------------------------------------------------------------------------------
/src/Service/MessageService.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramStickerPorter;
2 |
3 | public class MessageService
4 | {
5 | private readonly ILogger _logger;
6 |
7 | public MessageService(ILogger logger)
8 | {
9 | _logger = logger;
10 | }
11 |
12 | public async Task SendMessageAsync(
13 | Bot bot,
14 | long chatId,
15 | string messageText,
16 | InlineKeyboardMarkup inlineKeyboardMarkup = null,
17 | ParseMode parseMode = ParseMode.Html,
18 | ReplyParameters replyParameters = null)
19 | {
20 | try
21 | {
22 | if (bot == null)
23 | throw new ArgumentNullException(nameof(bot));
24 |
25 | if (string.IsNullOrEmpty(messageText))
26 | return 0;
27 |
28 | var sendMessage = await bot.SendMessage(
29 | chatId,
30 | messageText,
31 | parseMode: parseMode,
32 | replyParameters: replyParameters,
33 | linkPreviewOptions: new LinkPreviewOptions { IsDisabled = true },
34 | replyMarkup: inlineKeyboardMarkup
35 | );
36 |
37 | _logger.LogInformation("消息发送成功。消息Id:{MessageId} 内容:{MessageText}", sendMessage.MessageId, messageText);
38 | return sendMessage.MessageId;
39 | }
40 | catch (Exception ex)
41 | {
42 | _logger.LogError(ex, "发送消息失败。ChatId:{ChatId} 内容:{MessageText}", chatId, messageText);
43 | return 0;
44 | }
45 | }
46 |
47 | public async Task EditMessageAsync(
48 | Bot bot,
49 | long chatId,
50 | int messageId,
51 | string messageText,
52 | InlineKeyboardMarkup inlineKeyboardMarkup = null,
53 | ParseMode parseMode = ParseMode.Html,
54 | ReplyParameters replyParameters = null)
55 | {
56 | try
57 | {
58 | if (bot == null)
59 | throw new ArgumentNullException(nameof(bot));
60 |
61 | if (string.IsNullOrEmpty(messageText))
62 | return;
63 |
64 | await bot.EditMessageText(
65 | chatId,
66 | messageId,
67 | messageText,
68 | parseMode: parseMode,
69 | linkPreviewOptions: new LinkPreviewOptions { IsDisabled = true },
70 | replyMarkup: inlineKeyboardMarkup
71 | );
72 | }
73 | catch (Exception ex)
74 | {
75 | _logger.LogError(ex, "修改消息失败。ChatId:{ChatId} MessageId:{MessageId}", chatId, messageId);
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/src/Service/TelegramBotClientManager.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramStickerPorter;
2 |
3 | public class TelegramBotClientManager
4 | {
5 | private readonly ILogger _logger;
6 | private readonly TelegramOptions _options;
7 | private Bot _bot;
8 | private readonly object _lockObject = new();
9 |
10 | public TelegramBotClientManager(ILogger logger)
11 | {
12 | _logger = logger;
13 | _options = App.GetConfig("Telegram") ?? throw Oops.Oh("未在配置中找到 Telegram 节点");
14 | ValidateOptions(_options);
15 | }
16 |
17 | public bool HasActiveBot
18 | {
19 | get
20 | {
21 | lock (_lockObject)
22 | {
23 | return _bot != null;
24 | }
25 | }
26 | }
27 |
28 | public Bot CreateBot()
29 | {
30 | lock (_lockObject)
31 | {
32 | try
33 | {
34 | StopBotInternal();
35 |
36 | var basePath = AppContext.BaseDirectory;
37 | var dbPath = Path.Combine(basePath, "TelegramBot.sqlite");
38 | var connection = new Microsoft.Data.Sqlite.SqliteConnection($"Data Source={dbPath}");
39 |
40 | _bot = new Bot(
41 | _options.BotToken,
42 | _options.ApiId,
43 | _options.ApiHash,
44 | connection,
45 | SqlCommands.Sqlite);
46 |
47 | _logger.LogInformation("创建新机器人实例成功");
48 | return _bot;
49 | }
50 | catch (Exception ex)
51 | {
52 | _logger.LogError(ex, "创建机器人实例失败");
53 | throw Oops.Oh(ex, "启动机器人时发生错误");
54 | }
55 | }
56 | }
57 |
58 | public Bot GetBot()
59 | {
60 | return _bot ?? throw new InvalidOperationException("机器人实例未初始化");
61 | }
62 |
63 | public void StopBot()
64 | {
65 | lock (_lockObject)
66 | {
67 | StopBotInternal();
68 | }
69 | }
70 |
71 | public async Task CanPingTelegramAsync()
72 | {
73 | Bot bot;
74 | lock (_lockObject)
75 | {
76 | bot = _bot;
77 | }
78 |
79 | if (bot == null)
80 | {
81 | _logger.LogWarning("机器人实例不存在,无法检测连接");
82 | return false;
83 | }
84 |
85 | try
86 | {
87 | var cmds = await bot.GetMyCommands();
88 | return cmds != null;
89 | }
90 | catch (ObjectDisposedException)
91 | {
92 | _logger.LogWarning("机器人实例已被释放,无法检测连接");
93 | return false;
94 | }
95 | catch (Exception ex)
96 | {
97 | _logger.LogError(ex, "检测机器人连接失败");
98 | return false;
99 | }
100 | }
101 |
102 | private void StopBotInternal()
103 | {
104 | if (_bot == null) return;
105 |
106 | try
107 | {
108 | _bot.Dispose();
109 | _logger.LogInformation("机器人实例已释放");
110 | }
111 | catch (Exception ex)
112 | {
113 | _logger.LogError(ex, "释放机器人实例时出错");
114 | }
115 | finally
116 | {
117 | _bot = null;
118 | }
119 | }
120 |
121 | private static void ValidateOptions(TelegramOptions options)
122 | {
123 | if (options.ApiId <= 0)
124 | throw Oops.Oh("Telegram:ApiId 配置必须为正整数");
125 |
126 | if (string.IsNullOrWhiteSpace(options.ApiHash))
127 | throw Oops.Oh("Telegram:ApiHash 配置不能为空");
128 |
129 | if (string.IsNullOrWhiteSpace(options.BotToken))
130 | throw Oops.Oh("Telegram:BotToken 配置不能为空");
131 | }
132 | }
--------------------------------------------------------------------------------
/src/Service/TelegramBotBackgroundService.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramStickerPorter;
2 |
3 | public class TelegramBotBackgroundService : BackgroundService
4 | {
5 | private readonly TelegramBotClientManager _telegramBotClientManager;
6 | private readonly ILogger _logger;
7 | private readonly StickerService _stickerService;
8 | private readonly SemaphoreSlim _restartLock = new(1, 1);
9 |
10 | public TelegramBotBackgroundService(
11 | ILogger logger,
12 | TelegramBotClientManager telegramBotClientManager,
13 | StickerService stickerService)
14 | {
15 | _logger = logger;
16 | _telegramBotClientManager = telegramBotClientManager;
17 | _stickerService = stickerService;
18 | }
19 |
20 | protected override async Task ExecuteAsync(CancellationToken stoppingToken)
21 | {
22 | try
23 | {
24 | await EnsureBotInitializedAsync(stoppingToken);
25 | }
26 | catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
27 | {
28 | _logger.LogInformation("机器人后台服务已取消");
29 | }
30 | catch (Exception ex)
31 | {
32 | _logger.LogError(ex, "机器人启动失败");
33 | }
34 | }
35 |
36 | public Task EnsureBotInitializedAsync(CancellationToken cancellationToken = default)
37 | => InitializeInternalAsync(forceRestart: false, cancellationToken);
38 |
39 | public Task RestartBotAsync(CancellationToken cancellationToken = default)
40 | => InitializeInternalAsync(forceRestart: true, cancellationToken);
41 |
42 | public override async Task StopAsync(CancellationToken cancellationToken)
43 | {
44 | _logger.LogInformation("正在停止机器人后台服务");
45 | _telegramBotClientManager.StopBot();
46 | await base.StopAsync(cancellationToken);
47 | }
48 |
49 | private async Task InitializeInternalAsync(bool forceRestart, CancellationToken cancellationToken)
50 | {
51 | await _restartLock.WaitAsync(cancellationToken);
52 | try
53 | {
54 | if (!forceRestart && _telegramBotClientManager.HasActiveBot)
55 | {
56 | _logger.LogDebug("检测到机器人已在线,跳过初始化");
57 | return;
58 | }
59 |
60 | _logger.LogInformation(forceRestart ? "正在重新初始化机器人..." : "正在初始化机器人...");
61 | var bot = _telegramBotClientManager.CreateBot();
62 | var me = await bot.GetMe();
63 | _logger.LogInformation($"机器人启动: @{me.Username}");
64 |
65 | await ConfigureBotAsync(bot);
66 | }
67 | finally
68 | {
69 | _restartLock.Release();
70 | }
71 | }
72 |
73 | private async Task ConfigureBotAsync(Bot bot)
74 | {
75 | var commands = new[]
76 | {
77 | new Telegram.Bot.Types.BotCommand { Command = "start", Description = "启动机器人" },
78 | new Telegram.Bot.Types.BotCommand { Command = "info", Description = "关于" },
79 | };
80 |
81 | foreach (var cmd in commands)
82 | {
83 | _logger.LogInformation($"命令:{cmd.Command} 描述:{cmd.Description}");
84 | }
85 |
86 | await bot.SetMyCommands(commands, new BotCommandScopeAllPrivateChats());
87 | await bot.DropPendingUpdates();
88 | _logger.LogInformation("机器人丢弃未处理的更新");
89 |
90 | ConfigureErrorHandling(bot);
91 | ConfigureMessageHandling(bot);
92 | }
93 |
94 | private void ConfigureErrorHandling(Bot bot)
95 | {
96 | bot.WantUnknownTLUpdates = true;
97 | bot.OnError += (e, s) =>
98 | {
99 | _logger.LogError($"机器人错误: {e}");
100 | return Task.CompletedTask;
101 | };
102 | }
103 |
104 | private void ConfigureMessageHandling(Bot bot)
105 | {
106 | bot.OnMessage += async (msg, type) => await OnMessageAsync(bot, msg, type);
107 | bot.OnUpdate += update =>
108 | {
109 | _logger.LogInformation("机器人处理更新");
110 | ProcessUpdate(bot, update);
111 | return Task.CompletedTask;
112 | };
113 |
114 | _logger.LogInformation("机器人监听中...");
115 | }
116 |
117 | private async Task OnMessageAsync(Bot bot, WTelegram.Types.Message msg, UpdateType type)
118 | {
119 | if (msg.Chat.Type != ChatType.Group && msg.Chat.Type != ChatType.Supergroup)
120 | {
121 | await HandlePrivateAsync(bot, msg);
122 | }
123 | }
124 |
125 | private void ProcessUpdate(Bot bot, WTelegram.Types.Update update)
126 | {
127 | if (update.Type != UpdateType.Unknown) return;
128 |
129 | if (update.TLUpdate is TL.UpdateDeleteChannelMessages udcm)
130 | _logger.LogInformation($"{udcm.messages.Length} 条消息被删除,来源:{bot.Chat(udcm.channel_id)?.Title}");
131 | else if (update.TLUpdate is TL.UpdateDeleteMessages udm)
132 | _logger.LogInformation($"{udm.messages.Length} 条消息被删除,来源:用户或小型私聊群组");
133 | else if (update.TLUpdate is TL.UpdateReadChannelOutbox urco)
134 | _logger.LogInformation($"某人阅读了 {bot.Chat(urco.channel_id)?.Title} 的消息,直到消息 ID: {urco.max_id}");
135 | }
136 |
137 | private async Task HandlePrivateAsync(Bot bot, WTelegram.Types.Message msg)
138 | {
139 | if (msg.Text == null) return;
140 |
141 | var text = msg.Text.ToLower();
142 |
143 | if (text.StartsWith("/start") || text == "/clonepack" || text == "clonepack" || text == "克隆" || text == "贴纸" || text == "tiezhi" || text == "表情" || text == "biaoqing" || text == "emoji" || text == "stickers")
144 | {
145 | await _stickerService.SendStickerInstructionsAsync(bot, msg);
146 | }
147 | else if (text.StartsWith("克隆#"))
148 | {
149 | await _stickerService.HandleCloneCommandAsync(bot, msg);
150 | }
151 | else if (text.StartsWith("/info"))
152 | {
153 | await _stickerService.SendStickerInfoAsync(bot, msg);
154 | }
155 | }
156 | }
--------------------------------------------------------------------------------
/.github/workflows/build-release-docker.yml:
--------------------------------------------------------------------------------
1 | name: Build & Release + Docker
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths-ignore:
8 | - '**.md'
9 | - '.gitignore'
10 | - '.editorconfig'
11 |
12 | permissions:
13 | contents: write
14 | packages: write
15 |
16 | env:
17 | DOTNET_VERSION: '9.0.x'
18 | CONFIGURATION: Release
19 | PROJECT_PATH: 'src/TelegramStickerPorter.csproj'
20 | IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/telegramstickerporter
21 |
22 | concurrency:
23 | group: auto-release-${{ github.ref }}
24 | cancel-in-progress: true
25 |
26 | jobs:
27 | prepare:
28 | runs-on: ubuntu-latest
29 | outputs:
30 | VERSION: ${{ steps.ver.outputs.VERSION }}
31 | IMAGE_NAME_LC: ${{ steps.ver.outputs.IMAGE_NAME_LC }}
32 | steps:
33 | - id: ver
34 | shell: bash
35 | run: |
36 | VERSION="1.$(TZ='Asia/Shanghai' date +'%Y%m%d.%H%M')"
37 | IMAGE_NAME_LC=$(echo "ghcr.io/${{ github.repository_owner }}/telegramstickerporter" | tr '[:upper:]' '[:lower:]')
38 | echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
39 | echo "IMAGE_NAME_LC=$IMAGE_NAME_LC" >> "$GITHUB_OUTPUT"
40 | echo "$VERSION" > version.txt
41 | - uses: actions/upload-artifact@v4
42 | with:
43 | name: version
44 | path: version.txt
45 | retention-days: 1
46 |
47 | build:
48 | needs: prepare
49 | runs-on: ubuntu-latest
50 | strategy:
51 | matrix:
52 | include:
53 |
54 | - { name: linux-x64, rid: linux-x64, arch: amd64, artifact: TelegramStickerPorter-linux-x64.tar.gz, pack: "tar -czf", docker: true }
55 | - { name: linux-arm64, rid: linux-arm64, arch: arm64, artifact: TelegramStickerPorter-linux-arm64.tar.gz,pack: "tar -czf", docker: true }
56 |
57 | - { name: osx-x64, rid: osx-x64, artifact: TelegramStickerPorter-osx-x64.zip, pack: "zip -r -q", docker: false }
58 | - { name: osx-arm64, rid: osx-arm64, artifact: TelegramStickerPorter-osx-arm64.zip, pack: "zip -r -q", docker: false }
59 |
60 | - { name: windows-x86, rid: win-x86, artifact: TelegramStickerPorter-windows-x86.zip, pack: "zip -r -q", docker: false }
61 | - { name: windows-x64, rid: win-x64, artifact: TelegramStickerPorter-windows-x64.zip, pack: "zip -r -q", docker: false }
62 | - { name: windows-arm64, rid: win-arm64, artifact: TelegramStickerPorter-windows-arm64.zip,pack: "zip -r -q", docker: false }
63 | steps:
64 | - uses: actions/checkout@v4
65 | with:
66 | fetch-depth: 1
67 | - uses: actions/setup-dotnet@v4
68 | with:
69 | dotnet-version: ${{ env.DOTNET_VERSION }}
70 | - name: Publish
71 | run: |
72 | dotnet publish "${{ env.PROJECT_PATH }}" \
73 | --configuration "$CONFIGURATION" \
74 | -r "${{ matrix.rid }}" \
75 | --self-contained true \
76 | -p:PublishSingleFile=true \
77 | -o "out/${{ matrix.rid }}"
78 | find "out/${{ matrix.rid }}" -type f \( -name '*.pdb' -o -name '*.xml' \) -delete
79 | cd "out/${{ matrix.rid }}"
80 | ${{ matrix.pack }} "../${{ matrix.artifact }}" .
81 | cd ../..
82 | - uses: actions/upload-artifact@v4
83 | with:
84 | name: ${{ matrix.name }}
85 | path: out/${{ matrix.artifact }}
86 | retention-days: 3
87 |
88 | docker-build:
89 | needs: [prepare, build]
90 | runs-on: ubuntu-latest
91 | steps:
92 | - uses: actions/checkout@v4
93 | - uses: actions/download-artifact@v4
94 | with:
95 | name: linux-x64
96 | path: out/linux-amd64
97 | - uses: actions/download-artifact@v4
98 | with:
99 | name: linux-arm64
100 | path: out/linux-arm64
101 | - uses: actions/download-artifact@v4
102 | with:
103 | name: version
104 | path: .
105 | - name: Prepare binaries
106 | run: |
107 |
108 | tar -xzf out/linux-amd64/TelegramStickerPorter-linux-x64.tar.gz -C out/linux-amd64 --strip-components=1
109 | chmod +x out/linux-amd64/TelegramStickerPorter
110 |
111 |
112 | tar -xzf out/linux-arm64/TelegramStickerPorter-linux-arm64.tar.gz -C out/linux-arm64 --strip-components=1
113 | chmod +x out/linux-arm64/TelegramStickerPorter
114 | - uses: docker/setup-qemu-action@v3
115 | - uses: docker/setup-buildx-action@v3
116 | - uses: docker/login-action@v3
117 | with:
118 | registry: ghcr.io
119 | username: ${{ github.actor }}
120 | password: ${{ secrets.GITHUB_TOKEN }}
121 | - id: ver
122 | run: echo "VERSION=$(cat version.txt)" >> "$GITHUB_OUTPUT"
123 | - name: Build and push multi-platform image
124 | run: |
125 | IMAGE_NAME_LC=$(echo "$IMAGE_NAME" | tr '[:upper:]' '[:lower:]')
126 |
127 |
128 | docker buildx build \
129 | --platform linux/amd64,linux/arm64 \
130 | --build-arg BIN_NAME=TelegramStickerPorter \
131 | --label "org.opencontainers.image.source=https://github.com/${{ github.repository }}" \
132 | --label "org.opencontainers.image.revision=${{ github.sha }}" \
133 | --label "org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
134 | --provenance=false \
135 | --sbom=false \
136 | --push \
137 | -t $IMAGE_NAME_LC:latest \
138 | -t $IMAGE_NAME_LC:${{ steps.ver.outputs.VERSION }} \
139 | .
140 |
141 |
142 | echo "=== Verifying multi-platform manifest ==="
143 | docker buildx imagetools inspect $IMAGE_NAME_LC:latest
144 | docker buildx imagetools inspect $IMAGE_NAME_LC:${{ steps.ver.outputs.VERSION }}
145 |
146 | release:
147 | if: github.repository == 'Riniba/TelegramStickerPorter'
148 | needs: [prepare, build]
149 | runs-on: ubuntu-latest
150 | steps:
151 | - uses: actions/checkout@v4
152 | - uses: actions/download-artifact@v4
153 | with:
154 | path: artifacts
155 | - id: meta
156 | run: |
157 | echo "DATETIME=$(TZ='Asia/Shanghai' date +'%Y年%m月%d日 %H:%M:%S')" >> "$GITHUB_OUTPUT"
158 | echo "COMMIT_MSG=$(git log -1 --pretty=format:'%s')" >> "$GITHUB_OUTPUT"
159 | - uses: softprops/action-gh-release@v1
160 | env:
161 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
162 | with:
163 | tag_name: ${{ needs.prepare.outputs.VERSION }}
164 | name: "Release ${{ needs.prepare.outputs.VERSION }}"
165 | body: |
166 | **发布时间:** ${{ steps.meta.outputs.DATETIME }}
167 | **发布者:** ${{ github.actor }}
168 |
169 | **更新内容**
170 | ${{ steps.meta.outputs.COMMIT_MSG }}
171 |
172 | Windows (x64 / x86 / ARM64)
173 | Linux (x64 / ARM64)
174 | macOS (Intel / Apple Silicon)
175 |
176 | Windows 用户请下载对应系统架构的 zip 文件
177 | • 64 位系统:`TelegramStickerPorter-windows-x64.zip`
178 | • 32 位系统:`TelegramStickerPorter-windows-x86.zip`
179 | • ARM64 系统:`TelegramStickerPorter-windows-arm64.zip`
180 |
181 | Linux 用户请下载对应架构的 tar.gz 文件
182 | • x64 架构:`TelegramStickerPorter-linux-x64.tar.gz`
183 | • ARM64 架构:`TelegramStickerPorter-linux-arm64.tar.gz`
184 |
185 | macOS 用户请下载对应架构的 zip 文件
186 | • Intel 芯片:`TelegramStickerPorter-osx-x64.zip`
187 | • Apple Silicon:`TelegramStickerPorter-osx-arm64.zip`
188 | files: |
189 | artifacts/**/*.zip
190 | artifacts/**/*.tar.gz
--------------------------------------------------------------------------------
/src/Service/StickerService.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramStickerPorter;
2 |
3 | public class StickerService
4 | {
5 | private readonly ILogger _logger;
6 | private readonly MessageService _messageService;
7 |
8 | public StickerService(ILogger logger, MessageService messageService)
9 | {
10 | _logger = logger;
11 | _messageService = messageService;
12 | }
13 |
14 | public async Task SendStickerInstructionsAsync(Bot bot, Telegram.Bot.Types.Message msg)
15 | {
16 | var messageText = new StringBuilder()
17 | .AppendLine("💎 贴纸/表情克隆使用说明 💎")
18 | .AppendLine()
19 | .AppendLine("请输入您想要的目标贴纸包(或表情包)的名称,以及需要克隆的原始贴纸包(或表情包)链接,格式如下:")
20 | .AppendLine()
21 | .AppendLine("克隆#您的贴纸包(或表情包)名称#需要克隆的贴纸包(或表情包)链接")
22 | .AppendLine()
23 | .AppendLine("例如:")
24 | .AppendLine("克隆#我的可爱表情包#https://t.me/addstickers/pack_bafb8ef1_by_stickerporter_bot")
25 | .AppendLine()
26 | .AppendLine("克隆#我的酷酷的贴纸包#https://t.me/addemoji/pack_7f810f59_by_stickerporter_bot")
27 | .AppendLine()
28 | .AppendLine("🔹 克隆:命令前缀,触发克隆操作。")
29 | .AppendLine("🔹 您的贴纸包(或表情包)名称:您希望克隆后新贴纸包(或表情包)的名称。")
30 | .AppendLine("🔹 需要克隆的贴纸包(或表情包)链接:原始贴纸(或表情包)的链接。")
31 | .AppendLine()
32 | .AppendLine("请确保信息填写正确,以便程序顺利克隆哦~ 🚀")
33 | .ToString();
34 |
35 | await _messageService.SendMessageAsync(bot, msg.Chat.Id, messageText, replyParameters: msg);
36 | }
37 |
38 | public async Task SendStickerInfoAsync(Bot bot, Telegram.Bot.Types.Message msg)
39 | {
40 | var messageText = new StringBuilder()
41 | .AppendLine("💎 开源地址: 💎")
42 | .AppendLine("https://github.com/Riniba/TelegramStickerPorter")
43 | .AppendLine()
44 | .ToString();
45 |
46 | await _messageService.SendMessageAsync(bot, msg.Chat.Id, messageText, replyParameters: msg);
47 | }
48 |
49 | public async Task HandleCloneCommandAsync(Bot bot, Telegram.Bot.Types.Message msg)
50 | {
51 | try
52 | {
53 | string[] parts = msg.Text.Split('#');
54 |
55 | if (parts.Length != 3)
56 | {
57 | var errorMsg = new StringBuilder()
58 | .AppendLine("格式错误!请使用正确的格式:")
59 | .Append("克隆#您的贴纸包(或表情包)名称#需要克隆的贴纸包(或表情包)链接")
60 | .ToString();
61 |
62 | await _messageService.SendMessageAsync(bot, msg.Chat.Id, errorMsg, replyParameters: msg);
63 | return;
64 | }
65 |
66 | string newStickerSetTitle = parts[1];
67 | string stickerUrl = parts[2];
68 |
69 | if (!stickerUrl.StartsWith("https://t.me/add"))
70 | {
71 | await _messageService.SendMessageAsync(bot, msg.Chat.Id,
72 | "贴纸链接格式错误!链接应该以 https://t.me/add 开头",
73 | replyParameters: msg);
74 | return;
75 | }
76 |
77 | string sourceStickerSetName = stickerUrl
78 | .Replace("https://t.me/addstickers/", "")
79 | .Replace("https://t.me/addemoji/", "");
80 |
81 | var statusMessage = $"✨ 正在开始克隆贴纸包,请稍候...\n此过程可能需要几分钟。";
82 | var statusMessageId = await _messageService.SendMessageAsync(bot, msg.Chat.Id, statusMessage);
83 |
84 | _ = Task.Run(async () => await ProcessCloneStickerTaskAsync(
85 | bot, msg, statusMessageId, sourceStickerSetName, newStickerSetTitle));
86 | }
87 | catch (Exception ex)
88 | {
89 | var errorBuilder = $"[错误] 发生异常: {ex.Message}";
90 | _logger.LogError(ex, "处理克隆命令时发生异常");
91 | await _messageService.SendMessageAsync(bot, msg.Chat.Id, errorBuilder);
92 | }
93 | }
94 |
95 | private async Task ProcessCloneStickerTaskAsync(
96 | Bot bot,
97 | Telegram.Bot.Types.Message msg,
98 | int statusMessageId,
99 | string sourceStickerSetName,
100 | string newStickerSetTitle)
101 | {
102 | List stickerErrors = new List();
103 |
104 | try
105 | {
106 | var me = await bot.GetMe();
107 | string botUsername = me.Username?.ToLower();
108 | string newPackName = GeneratePackName(botUsername);
109 |
110 | var sourceSet = await bot.GetStickerSet(sourceStickerSetName);
111 |
112 | var statusBuilder = new StringBuilder()
113 | .AppendLine("📦 源贴纸包信息:")
114 | .AppendLine($"标题: {sourceSet.Title}")
115 | .AppendLine($"贴纸数量: {sourceSet.Stickers.Length}")
116 | .AppendLine($"类型: {sourceSet.StickerType}")
117 | .AppendLine()
118 | .Append("🔄 正在准备克隆...");
119 |
120 | await _messageService.EditMessageAsync(bot, msg.Chat.Id, statusMessageId, statusBuilder.ToString());
121 |
122 | var itemsForNewSet = sourceSet.Stickers
123 | .Select(item => new InputSticker(
124 | sticker: item.FileId,
125 | format: DetermineStickerFormat(item),
126 | emojiList: item.Emoji?.Split() ?? new[] { "😊" }
127 | ))
128 | .ToList();
129 |
130 | if (!itemsForNewSet.Any())
131 | throw Oops.Oh("源包中未找到贴纸");
132 |
133 | await bot.CreateNewStickerSet(
134 | userId: msg.From.Id,
135 | name: newPackName,
136 | title: newStickerSetTitle,
137 | stickers: new[] { itemsForNewSet[0] },
138 | stickerType: sourceSet.StickerType
139 | );
140 |
141 | await _messageService.EditMessageAsync(bot, msg.Chat.Id, statusMessageId,
142 | $"📦 新包创建完成: {newStickerSetTitle}");
143 |
144 | if (itemsForNewSet.Count > 1)
145 | {
146 | await _messageService.EditMessageAsync(bot, msg.Chat.Id, statusMessageId,
147 | $"📦 正在添加资源...");
148 |
149 | for (int i = 1; i < itemsForNewSet.Count; i++)
150 | {
151 | try
152 | {
153 | await _messageService.EditMessageAsync(bot, msg.Chat.Id, statusMessageId,
154 | $"[进度] 正在添加第 {i}/{itemsForNewSet.Count - 1} 个贴纸");
155 |
156 | await bot.AddStickerToSet(
157 | userId: msg.From.Id,
158 | name: newPackName,
159 | sticker: itemsForNewSet[i]
160 | );
161 | }
162 | catch (Exception stickerEx)
163 | {
164 | string errorMsg = $"贴纸 {i} 添加失败: {stickerEx.Message}";
165 | stickerErrors.Add(errorMsg);
166 | _logger.LogError(stickerEx, $"贴纸 {i} 添加失败 - 用户ID: {msg.From.Id}, 包名: {newPackName}");
167 | }
168 |
169 | await Task.Delay(100);
170 | }
171 | }
172 |
173 | var finalMessageBuilder = new StringBuilder()
174 | .AppendLine("✅ 贴纸包克隆完成!")
175 | .AppendLine()
176 | .AppendLine($"📝 标题: {newStickerSetTitle}")
177 | .AppendLine($"🔢 总计: {itemsForNewSet.Count} 个贴纸")
178 | .Append($"🔗 链接: https://t.me/add{(sourceSet.StickerType == StickerType.Regular ? "stickers" : "emoji")}/{newPackName}");
179 |
180 | if (stickerErrors.Any())
181 | {
182 | finalMessageBuilder
183 | .AppendLine()
184 | .AppendLine()
185 | .AppendLine("⚠️ 部分贴纸上传失败:")
186 | .Append(string.Join(Environment.NewLine, stickerErrors));
187 | }
188 |
189 | await _messageService.EditMessageAsync(bot, msg.Chat.Id, statusMessageId, finalMessageBuilder.ToString());
190 | }
191 | catch (Exception ex)
192 | {
193 | var errorBuilder = new StringBuilder()
194 | .AppendLine("❌ 克隆过程中出现错误:")
195 | .AppendLine(ex.Message)
196 | .AppendLine()
197 | .Append("请稍后重试或联系管理员。");
198 |
199 | await _messageService.EditMessageAsync(bot, msg.Chat.Id, statusMessageId, errorBuilder.ToString());
200 | _logger.LogError(ex, $"克隆贴纸包时发生错误 - 用户ID: {msg.From.Id}, 源包: {sourceStickerSetName}");
201 | }
202 | }
203 |
204 | private StickerFormat DetermineStickerFormat(Sticker sticker)
205 | {
206 | if (sticker == null)
207 | throw Oops.Oh("贴纸对象不能为空");
208 |
209 | return sticker.IsVideo ? StickerFormat.Video :
210 | sticker.IsAnimated ? StickerFormat.Animated :
211 | StickerFormat.Static;
212 | }
213 |
214 | private void ValidatePackName(string packName)
215 | {
216 | if (string.IsNullOrEmpty(packName))
217 | throw Oops.Oh("包名称不能为空");
218 |
219 | if (!packName.All(c => char.IsLetterOrDigit(c) || c == '_'))
220 | throw Oops.Oh("包名称只能包含字母、数字和下划线");
221 | }
222 |
223 | private string GeneratePackName(string botUsername)
224 | {
225 | if (string.IsNullOrEmpty(botUsername))
226 | throw Oops.Oh("Bot用户名不能为空");
227 |
228 | string randomId = Guid.NewGuid().ToString("N")[..8];
229 | string packName = $"pack_{randomId}_by_{botUsername}";
230 |
231 | ValidatePackName(packName);
232 | return packName;
233 | }
234 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------