diff --git a/TelegramSearchBot.LLM.Test/McpToolHelperTestCollection.cs b/TelegramSearchBot.LLM.Test/McpToolHelperTestCollection.cs
new file mode 100644
index 00000000..df367204
--- /dev/null
+++ b/TelegramSearchBot.LLM.Test/McpToolHelperTestCollection.cs
@@ -0,0 +1,8 @@
+using Xunit;
+
+namespace TelegramSearchBot.Test {
+ [CollectionDefinition(Name, DisableParallelization = true)]
+ public sealed class McpToolHelperTestCollection {
+ public const string Name = "McpToolHelper serial";
+ }
+}
diff --git a/TelegramSearchBot.LLM.Test/Service/AI/LLM/McpToolHelperTests.cs b/TelegramSearchBot.LLM.Test/Service/AI/LLM/McpToolHelperTests.cs
index 300a4ad9..9220c035 100644
--- a/TelegramSearchBot.LLM.Test/Service/AI/LLM/McpToolHelperTests.cs
+++ b/TelegramSearchBot.LLM.Test/Service/AI/LLM/McpToolHelperTests.cs
@@ -182,6 +182,7 @@ public static void ProcessThoughtAsync(
}
// --- Test Class ---
+ [Collection(McpToolHelperTestCollection.Name)]
public class McpToolHelperTests {
#pragma warning disable CS8618 // 单元测试中字段会在初始化方法中赋值
// 移除构造函数中的静态字段和 Mock 成员,改为在测试方法内部声明和设置
@@ -722,21 +723,22 @@ public void GetNativeToolDefinitions_IncludesExternalMcpTools() {
})
};
- McpToolHelper.RegisterExternalTools(
- externalTools,
- async (server, tool, args) => { await Task.CompletedTask; return "mock result"; });
-
- var tools = McpToolHelper.GetNativeToolDefinitions();
-
- var externalTool = tools.FirstOrDefault(t => t.FunctionName == "mcp_test-server_testTool");
- Assert.NotNull(externalTool);
- Assert.Contains("test-server", externalTool.FunctionDescription);
- Assert.Contains("A test external tool", externalTool.FunctionDescription);
-
- // Clean up
- McpToolHelper.RegisterExternalTools(
- new List<(string, McpToolHelper.ExternalToolInfo)>(),
- null);
+ try {
+ McpToolHelper.RegisterExternalTools(
+ externalTools,
+ async (server, tool, args) => { await Task.CompletedTask; return "mock result"; });
+
+ var tools = McpToolHelper.GetNativeToolDefinitions();
+
+ var externalTool = tools.FirstOrDefault(t => t.FunctionName == "mcp_test-server_testTool");
+ Assert.NotNull(externalTool);
+ Assert.Contains("test-server", externalTool.FunctionDescription);
+ Assert.Contains("A test external tool", externalTool.FunctionDescription);
+ } finally {
+ McpToolHelper.RegisterExternalTools(
+ new List<(string, McpToolHelper.ExternalToolInfo)>(),
+ null);
+ }
}
[Fact]
diff --git a/TelegramSearchBot.LLM.Test/Service/Tools/McpInstallerToolServiceTests.cs b/TelegramSearchBot.LLM.Test/Service/Tools/McpInstallerToolServiceTests.cs
index cc860959..d2365da7 100644
--- a/TelegramSearchBot.LLM.Test/Service/Tools/McpInstallerToolServiceTests.cs
+++ b/TelegramSearchBot.LLM.Test/Service/Tools/McpInstallerToolServiceTests.cs
@@ -23,6 +23,7 @@ namespace TelegramSearchBot.Test.Service.Tools {
///
/// Integration tests for McpInstallerToolService verifying the full add/list/remove/restart flow.
///
+ [Collection(McpToolHelperTestCollection.Name)]
public class McpInstallerToolServiceTests : IDisposable {
private readonly ServiceProvider _serviceProvider;
private readonly McpServerManager _mcpServerManager;
@@ -61,6 +62,9 @@ public McpInstallerToolServiceTests() {
}
public void Dispose() {
+ McpToolHelper.RegisterExternalTools(
+ new List<(string, McpToolHelper.ExternalToolInfo)>(),
+ null);
_mcpServerManager.Dispose();
_serviceProvider.Dispose();
}
diff --git a/TelegramSearchBot.Test/Service/Manage/ChatImportServiceTests.cs b/TelegramSearchBot.Test/Service/Manage/ChatImportServiceTests.cs
new file mode 100644
index 00000000..f86fb030
--- /dev/null
+++ b/TelegramSearchBot.Test/Service/Manage/ChatImportServiceTests.cs
@@ -0,0 +1,147 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Telegram.Bot;
+using TelegramSearchBot.Model;
+using TelegramSearchBot.Service.Manage;
+using Xunit;
+
+namespace TelegramSearchBot.Test.Service.Manage {
+ public class ChatImportServiceTests {
+ private readonly DbContextOptions _dbContextOptions;
+
+ public ChatImportServiceTests() {
+ _dbContextOptions = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(Guid.NewGuid().ToString())
+ .Options;
+ }
+
+ [Fact]
+ public async Task ImportFromFileAsync_TextOnlyExport_DoesNotRequireEmbeddedMediaFiles() {
+ var tempDir = Path.Combine(Path.GetTempPath(), $"chat-import-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(tempDir);
+
+ try {
+ var jsonPath = Path.Combine(tempDir, "result.json");
+ await File.WriteAllTextAsync(jsonPath, """
+ {
+ "name": "测试频道",
+ "type": "channel",
+ "id": 777,
+ "messages": [
+ {
+ "id": 1,
+ "type": "service",
+ "date": "2025-08-09T07:33:01",
+ "date_unixtime": "1754695981",
+ "actor": "夸克网盘",
+ "actor_id": "channel2726412745",
+ "action": "create_channel",
+ "title": "夸克网盘",
+ "text": "",
+ "text_entities": []
+ },
+ {
+ "id": 2,
+ "type": "message",
+ "date": "2025-08-09T07:57:05",
+ "date_unixtime": "1754697425",
+ "from": "夸克网盘",
+ "from_id": "channel2726412745",
+ "photo": "(File not included. Change data exporting settings to download.)",
+ "photo_file_size": 257958,
+ "width": 905,
+ "height": 1280,
+ "text": [
+ "链接:",
+ {
+ "type": "link",
+ "text": "https://pan.quark.cn/s/fa9a2f5f8757"
+ }
+ ],
+ "text_entities": [
+ {
+ "type": "plain",
+ "text": "链接:"
+ },
+ {
+ "type": "link",
+ "text": "https://pan.quark.cn/s/fa9a2f5f8757"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "type": "message",
+ "date": "2025-08-09T08:00:00",
+ "date_unixtime": "1754697600",
+ "from": "Alice",
+ "from_id": "user123456",
+ "text": "plain string text",
+ "text_entities": []
+ },
+ {
+ "id": 4,
+ "type": "message",
+ "date": "2025-08-09T08:01:00",
+ "date_unixtime": "1754697660",
+ "from": "Bob",
+ "from_id": "user654321",
+ "photo": "(File not included. Change data exporting settings to download.)",
+ "text": "",
+ "text_entities": [],
+ "caption_entities": [
+ {
+ "type": "bold",
+ "text": "加粗标题"
+ }
+ ]
+ }
+ ]
+ }
+ """);
+
+ await using var context = new DataDbContext(_dbContextOptions);
+ var service = new ChatImportService(
+ new Mock>().Object,
+ new TestSendMessage(),
+ context);
+
+ await service.ImportFromFileAsync(jsonPath);
+
+ var imported = await context.Messages
+ .Where(m => m.GroupId == 777)
+ .OrderBy(m => m.MessageId)
+ .ToListAsync();
+
+ Assert.Equal(4, imported.Count);
+ Assert.Equal(2726412745L, imported[0].FromUserId);
+ Assert.Contains("create_channel", imported[0].Content);
+ Assert.Equal(2726412745L, imported[1].FromUserId);
+ Assert.Contains("https://pan.quark.cn/s/fa9a2f5f8757", imported[1].Content);
+ Assert.Equal("plain string text", imported[2].Content);
+ Assert.Equal(123456L, imported[2].FromUserId);
+ Assert.Contains("**加粗标题**", imported[3].Content);
+ Assert.Equal(654321L, imported[3].FromUserId);
+ } finally {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ private sealed class TestSendMessage : TelegramSearchBot.Manager.SendMessage {
+ public TestSendMessage()
+ : base(
+ new Mock().Object,
+ new Mock>().Object) {
+ }
+
+ public override Task AddTask(Func action, bool isGroup) {
+ return Task.CompletedTask;
+ }
+ }
+ }
+}
diff --git a/TelegramSearchBot/Model/ChatExport/ChatExport.cs b/TelegramSearchBot/Model/ChatExport/ChatExport.cs
index afbd9945..846e476c 100644
--- a/TelegramSearchBot/Model/ChatExport/ChatExport.cs
+++ b/TelegramSearchBot/Model/ChatExport/ChatExport.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
namespace TelegramSearchBot.Model.ChatExport {
public class ChatExport {
@@ -8,6 +9,8 @@ public class ChatExport {
public string Type { get; set; }
public long Id { get; set; }
public List Messages { get; set; }
+ [JsonProperty("messages_count")]
+ public int? MessagesCount { get; set; }
}
public class Message {
@@ -16,7 +19,12 @@ public class Message {
public DateTime Date { get; set; }
public string Date_Unixtime { get; set; }
public string From { get; set; }
+ [JsonProperty("from_id")]
public string From_Id { get; set; }
+ [JsonProperty("actor_id")]
+ public string ActorId { get; set; }
+ public string Title { get; set; }
+ [JsonConverter(typeof(TextItemListConverter))]
public List Text { get; set; }
public List Text_Entities { get; set; }
public string Edited { get; set; }
@@ -40,6 +48,34 @@ public class Message {
public string MimeType { get; set; }
[JsonProperty("duration_seconds")]
public int? DurationSeconds { get; set; }
+ public string Video { get; set; }
+ [JsonProperty("video_file_size")]
+ public int? VideoFileSize { get; set; }
+ public string Voice { get; set; }
+ [JsonProperty("voice_file_size")]
+ public int? VoiceFileSize { get; set; }
+ [JsonProperty("video_note")]
+ public string VideoNote { get; set; }
+ public StickerInfo Sticker { get; set; }
+ public string Location { get; set; }
+ public ContactInfo Contact { get; set; }
+ public PollInfo Poll { get; set; }
+ public string Game { get; set; }
+ public string Dice { get; set; }
+ [JsonProperty("forwarded_from")]
+ public string ForwardedFrom { get; set; }
+ [JsonProperty("saved_messages")]
+ public string SavedMessages { get; set; }
+ public string Actor { get; set; }
+ public string Action { get; set; }
+ [JsonProperty("action_type")]
+ public string ActionType { get; set; }
+ public List Reactions { get; set; }
+ [JsonProperty("via_bot")]
+ public string ViaBot { get; set; }
+ public string Caption { get; set; }
+ [JsonProperty("caption_entities")]
+ public List Caption_Entities { get; set; }
}
public class TextItem {
@@ -57,4 +93,98 @@ public class TextEntity {
[JsonProperty("document_id")]
public string DocumentId { get; set; }
}
+
+ public class StickerInfo {
+ [JsonProperty("emoji")]
+ public string Emoji { get; set; }
+ [JsonProperty("sticker_set_id")]
+ public string StickerSetId { get; set; }
+ }
+
+ public class ContactInfo {
+ [JsonProperty("phone_number")]
+ public string PhoneNumber { get; set; }
+ [JsonProperty("first_name")]
+ public string FirstName { get; set; }
+ [JsonProperty("last_name")]
+ public string LastName { get; set; }
+ }
+
+ public class PollInfo {
+ public string Question { get; set; }
+ public List Options { get; set; }
+ [JsonProperty("total_voters")]
+ public int? TotalVoters { get; set; }
+ }
+
+ public class PollOption {
+ public string Text { get; set; }
+ public int Voters { get; set; }
+ }
+
+ public class ReactionInfo {
+ public string Type { get; set; }
+ public string Emoji { get; set; }
+ [JsonProperty("document_id")]
+ public string DocumentId { get; set; }
+ public int Count { get; set; }
+ }
+
+ public class TextItemListConverter : JsonConverter {
+ public override bool CanWrite => false;
+
+ public override bool CanConvert(Type objectType) {
+ return objectType == typeof(List);
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
+ if (reader.TokenType == JsonToken.Null) {
+ return new List();
+ }
+
+ var token = JToken.Load(reader);
+
+ if (token.Type == JTokenType.String) {
+ return new List {
+ new() {
+ Type = "plain",
+ Text = token.Value() ?? string.Empty
+ }
+ };
+ }
+
+ if (token.Type == JTokenType.Object) {
+ return new List {
+ DeserializeTextItem(token, serializer)
+ };
+ }
+
+ if (token.Type != JTokenType.Array) {
+ return new List();
+ }
+
+ var items = new List();
+ foreach (var itemToken in token.Children()) {
+ items.Add(DeserializeTextItem(itemToken, serializer));
+ }
+
+ return items;
+ }
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { }
+
+ private static TextItem DeserializeTextItem(JToken token, JsonSerializer serializer) {
+ if (token.Type == JTokenType.String) {
+ return new TextItem {
+ Type = "plain",
+ Text = token.Value() ?? string.Empty
+ };
+ }
+
+ return token.ToObject(serializer) ?? new TextItem {
+ Type = "plain",
+ Text = token.ToString(Formatting.None)
+ };
+ }
+ }
}
diff --git a/TelegramSearchBot/Service/Manage/ChatImportService.cs b/TelegramSearchBot/Service/Manage/ChatImportService.cs
index 5e0bb0bb..ddc1604a 100644
--- a/TelegramSearchBot/Service/Manage/ChatImportService.cs
+++ b/TelegramSearchBot/Service/Manage/ChatImportService.cs
@@ -1,6 +1,9 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -40,6 +43,7 @@ public async Task ImportFromFileAsync(string filePath) {
try {
var json = await File.ReadAllTextAsync(filePath);
var chatExport = JsonConvert.DeserializeObject(json);
+ ArgumentNullException.ThrowIfNull(chatExport);
await _send.Log($"开始导入聊天记录: {chatExport.Name}");
@@ -61,46 +65,46 @@ public async Task ImportFromFileAsync(string filePath) {
dbMessage.GroupId = chatExport.Id;
dbMessage.DateTime = message.Date;
dbMessage.Content = GetMessageText(message);
- dbMessage.FromUserId = long.TryParse(message.From_Id, out var fromId) ? fromId : 0;
+ dbMessage.FromUserId = ParseExportUserId(message.From_Id, message.ActorId);
dbMessage.ReplyToMessageId = message.ReplyToMessageId ?? 0;
// 获取json文件所在目录
var jsonDir = Path.GetDirectoryName(filePath);
// 处理照片
- if (!string.IsNullOrEmpty(message.Photo)) {
+ if (HasEmbeddedFile(message.Photo)) {
var photoPath = Path.Combine(Env.WorkDir, "Photos", $"{chatExport.Id}");
Directory.CreateDirectory(photoPath);
var sourcePhotoPath = Path.Combine(jsonDir, message.Photo);
var extension = Path.GetExtension(message.Photo) ?? ".jpg";
var photoFile = Path.Combine(photoPath, $"{message.Id}{extension}");
- if (!File.Exists(photoFile)) {
+ if (!File.Exists(sourcePhotoPath)) {
+ _logger.LogWarning($"图片不存在: {sourcePhotoPath}");
+ } else if (!File.Exists(photoFile)) {
File.Copy(sourcePhotoPath, photoFile, true);
}
}
// 处理文件
- if (!string.IsNullOrEmpty(message.File)) {
- if (!message.File.StartsWith("(File not included.")) {
- var sourceFilePath = Path.Combine(jsonDir, message.File);
- if (!File.Exists(sourceFilePath)) {
- _logger.LogWarning($"文件不存在: {sourceFilePath}");
- } else {
- var extension = Path.GetExtension(message.File) ?? ".dat";
-
- // 根据文件类型保存到不同目录
- var destPath = IProcessVideo.IsVideo(message.File)
- ? Path.Combine(Env.WorkDir, "Videos", $"{chatExport.Id}")
- : IProcessAudio.IsAudio(message.File)
- ? Path.Combine(Env.WorkDir, "Audios", $"{chatExport.Id}")
- : Path.Combine(Env.WorkDir, "Files", $"{chatExport.Id}");
-
- Directory.CreateDirectory(destPath);
- var destFile = Path.Combine(destPath, $"{message.Id}{extension}");
-
- if (!File.Exists(destFile)) {
- File.Copy(sourceFilePath, destFile, true);
- }
+ if (HasEmbeddedFile(message.File)) {
+ var sourceFilePath = Path.Combine(jsonDir, message.File);
+ if (!File.Exists(sourceFilePath)) {
+ _logger.LogWarning($"文件不存在: {sourceFilePath}");
+ } else {
+ var extension = Path.GetExtension(message.File) ?? ".dat";
+
+ // 根据文件类型保存到不同目录
+ var destPath = IProcessVideo.IsVideo(message.File)
+ ? Path.Combine(Env.WorkDir, "Videos", $"{chatExport.Id}")
+ : IProcessAudio.IsAudio(message.File)
+ ? Path.Combine(Env.WorkDir, "Audios", $"{chatExport.Id}")
+ : Path.Combine(Env.WorkDir, "Files", $"{chatExport.Id}");
+
+ Directory.CreateDirectory(destPath);
+ var destFile = Path.Combine(destPath, $"{message.Id}{extension}");
+
+ if (!File.Exists(destFile)) {
+ File.Copy(sourceFilePath, destFile, true);
}
}
}
@@ -117,60 +121,181 @@ public async Task ImportFromFileAsync(string filePath) {
}
private string GetMessageText(ExportMessage message) {
- if (message.Text_Entities != null && message.Text_Entities.Count > 0) {
- var result = new System.Text.StringBuilder();
- foreach (var entity in message.Text_Entities) {
- switch (entity.Type) {
- case "plain":
- result.Append(entity.Text);
- break;
- case "bold":
- result.Append($"**{entity.Text}**");
- break;
- case "italic":
- result.Append($"*{entity.Text}*");
- break;
- case "strikethrough":
- result.Append($"~~{entity.Text}~~");
- break;
- case "spoiler":
- result.Append($"||{entity.Text}||");
- break;
- case "code":
- result.Append($"`{entity.Text}`");
- break;
- case "pre":
- result.Append($"```{entity.Language ?? ""}\n{entity.Text}\n```");
- break;
- case "text_link":
- result.Append($"[{entity.Text}]({entity.Href})");
- break;
- case "mention":
- result.Append($"[{entity.Text}](tg://user?id={entity.Text.TrimStart('@')})");
- break;
- case "bot_command":
- result.Append($"/{entity.Text.TrimStart('/')}");
- break;
- case "email":
- result.Append($"[{entity.Text}](mailto:{entity.Text})");
- break;
- case "hashtag":
- result.Append($"{entity.Text}");
- break;
- case "cashtag":
- result.Append($"{entity.Text}");
- break;
- case "custom_emoji":
- result.Append($"[{entity.Text}](tg://emoji?id={entity.DocumentId})");
- break;
- default:
- result.Append(entity.Text);
- break;
- }
+ var result = new StringBuilder();
+
+ AppendFormattedText(result, message.Text_Entities, message.Text);
+ AppendSection(result, BuildCaptionText(message));
+
+ if (message.Sticker != null) {
+ AppendSection(result, $"[贴纸: {message.Sticker.Emoji ?? "?"}]");
+ }
+
+ if (message.Voice != null) {
+ AppendSection(result, "[语音消息]");
+ }
+
+ if (message.Video != null) {
+ AppendSection(result, "[视频]");
+ }
+
+ if (message.VideoNote != null) {
+ AppendSection(result, "[视频消息]");
+ }
+
+ if (!string.IsNullOrEmpty(message.Poll?.Question)) {
+ AppendSection(result, $"[投票: {message.Poll.Question}]");
+ }
+
+ if (message.Contact != null) {
+ var name = $"{message.Contact.FirstName} {message.Contact.LastName}".Trim();
+ AppendSection(result, $"[联系人: {name}]");
+ }
+
+ if (message.Location != null) {
+ AppendSection(result, "[位置]");
+ }
+
+ if (!string.IsNullOrEmpty(message.Action)) {
+ var serviceParts = new List();
+ if (!string.IsNullOrWhiteSpace(message.Actor)) {
+ serviceParts.Add(message.Actor);
+ }
+
+ serviceParts.Add(message.Action);
+
+ if (!string.IsNullOrWhiteSpace(message.Title) &&
+ !string.Equals(message.Title, message.Actor, StringComparison.Ordinal)) {
+ serviceParts.Add(message.Title);
+ }
+
+ AppendSection(result, $"[服务消息: {string.Join(" ", serviceParts)}]");
+ }
+
+ if (!string.IsNullOrWhiteSpace(message.ForwardedFrom)) {
+ AppendSection(result, $"[转发自: {message.ForwardedFrom}]");
+ }
+
+ if (!string.IsNullOrWhiteSpace(message.ViaBot)) {
+ AppendSection(result, $"[ViaBot: {message.ViaBot}]");
+ }
+
+ if (message.Reactions != null && message.Reactions.Count > 0) {
+ var reactions = string.Join(", ", message.Reactions
+ .Where(r => !string.IsNullOrWhiteSpace(r.Emoji))
+ .Select(r => $"{r.Emoji}x{r.Count}"));
+
+ if (!string.IsNullOrWhiteSpace(reactions)) {
+ AppendSection(result, $"[回应: {reactions}]");
}
- return result.ToString();
}
- return string.Empty;
+
+ return result.ToString();
+ }
+
+ private static void AppendFormattedText(StringBuilder builder, List entities, List items) {
+ if (entities != null && entities.Count > 0) {
+ foreach (var entity in entities) {
+ builder.Append(FormatEntity(entity));
+ }
+
+ return;
+ }
+
+ if (items == null || items.Count == 0) {
+ return;
+ }
+
+ foreach (var item in items) {
+ builder.Append(FormatItem(item));
+ }
+ }
+
+ private static string BuildCaptionText(ExportMessage message) {
+ if (message.Caption_Entities != null && message.Caption_Entities.Count > 0) {
+ var builder = new StringBuilder();
+ foreach (var entity in message.Caption_Entities) {
+ builder.Append(FormatEntity(entity));
+ }
+
+ return builder.ToString();
+ }
+
+ return message.Caption ?? string.Empty;
+ }
+
+ private static string FormatEntity(TextEntity entity) {
+ if (string.IsNullOrEmpty(entity.Text)) {
+ return string.Empty;
+ }
+
+ return entity.Type switch {
+ "plain" => entity.Text,
+ "bold" => $"**{entity.Text}**",
+ "italic" => $"*{entity.Text}*",
+ "strikethrough" => $"~~{entity.Text}~~",
+ "spoiler" => $"||{entity.Text}||",
+ "code" => $"`{entity.Text}`",
+ "pre" => $"```{entity.Language ?? ""}\n{entity.Text}\n```",
+ "text_link" => $"[{entity.Text}]({entity.Href})",
+ "mention" => $"[{entity.Text}](tg://user?id={entity.Text.TrimStart('@')})",
+ "bot_command" => $"/{entity.Text.TrimStart('/')}",
+ "email" => $"[{entity.Text}](mailto:{entity.Text})",
+ "custom_emoji" => $"[{entity.Text}](tg://emoji?id={entity.DocumentId})",
+ _ => entity.Text
+ };
+ }
+
+ private static string FormatItem(TextItem item) {
+ if (string.IsNullOrEmpty(item.Text)) {
+ return string.Empty;
+ }
+
+ return item.Type switch {
+ "bold" => $"**{item.Text}**",
+ "italic" => $"*{item.Text}*",
+ "strikethrough" => $"~~{item.Text}~~",
+ "spoiler" => $"||{item.Text}||",
+ "code" => $"`{item.Text}`",
+ "pre" => $"```{item.Language ?? ""}\n{item.Text}\n```",
+ "link" or "text_link" when !string.IsNullOrWhiteSpace(item.Href) => $"[{item.Text}]({item.Href})",
+ _ => item.Text
+ };
+ }
+
+ private static void AppendSection(StringBuilder builder, string value) {
+ if (string.IsNullOrWhiteSpace(value)) {
+ return;
+ }
+
+ if (builder.Length > 0) {
+ builder.AppendLine();
+ }
+
+ builder.Append(value);
+ }
+
+ private static bool HasEmbeddedFile(string filePath) {
+ return !string.IsNullOrWhiteSpace(filePath) &&
+ !filePath.StartsWith("(File not included.", StringComparison.Ordinal);
+ }
+
+ private static long ParseExportUserId(params string[] rawIds) {
+ foreach (var rawId in rawIds) {
+ if (string.IsNullOrWhiteSpace(rawId)) {
+ continue;
+ }
+
+ if (long.TryParse(rawId, out var numericId)) {
+ return numericId;
+ }
+
+ var match = Regex.Match(rawId, @"-?\d+$");
+ if (match.Success && long.TryParse(match.Value, out numericId)) {
+ return numericId;
+ }
+ }
+
+ return 0;
}
public async Task ExecuteAsync(string command) {