first commit
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
namespace Telegram2VkBot;
|
||||
|
||||
public sealed class TelegramOptions
|
||||
{
|
||||
public required string BotToken { get; init; }
|
||||
public required long ChannelId { get; init; }
|
||||
}
|
||||
|
||||
public sealed class VkOptions
|
||||
{
|
||||
public required string AccessToken { get; init; }
|
||||
public required long GroupId { get; init; }
|
||||
public string ApiVersion { get; init; } = "5.199";
|
||||
}
|
||||
|
||||
public sealed class DbOptions
|
||||
{
|
||||
public required string Path { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Telegram.Bot.Polling;
|
||||
|
||||
namespace Telegram2VkBot;
|
||||
|
||||
public sealed class ForwardWorker : BackgroundService
|
||||
{
|
||||
private readonly TelegramOptions _telegram;
|
||||
private readonly VkOptions _vk;
|
||||
private readonly VkApiClient _vkApi;
|
||||
private readonly MappingRepository _repo;
|
||||
private readonly ILogger<ForwardWorker> _logger;
|
||||
|
||||
public ForwardWorker(
|
||||
IOptions<TelegramOptions> telegram,
|
||||
IOptions<VkOptions> vk,
|
||||
IOptions<DbOptions> db,
|
||||
VkApiClient vkApi,
|
||||
MappingRepository repo,
|
||||
ILogger<ForwardWorker> logger)
|
||||
{
|
||||
_telegram = telegram.Value;
|
||||
_vk = vk.Value;
|
||||
_vkApi = vkApi;
|
||||
_repo = repo;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_telegram.BotToken))
|
||||
throw new ArgumentException("TELEGRAM__BOT_TOKEN is empty");
|
||||
if (_telegram.ChannelId == 0)
|
||||
throw new ArgumentException("TELEGRAM__CHANNEL_ID must be non-zero (example: -1001234567890)");
|
||||
if (string.IsNullOrWhiteSpace(_vk.AccessToken))
|
||||
throw new ArgumentException("VK__ACCESS_TOKEN is empty");
|
||||
if (_vk.GroupId == 0)
|
||||
throw new ArgumentException("VK__GROUP_ID must be non-zero (community id without minus)");
|
||||
|
||||
_logger.LogInformation("Init database");
|
||||
await _repo.InitializeAsync(stoppingToken);
|
||||
|
||||
var bot = new TelegramBotClient(_telegram.BotToken);
|
||||
var receiverOptions = new ReceiverOptions
|
||||
{
|
||||
AllowedUpdates = new[]
|
||||
{
|
||||
UpdateType.ChannelPost,
|
||||
UpdateType.EditedChannelPost
|
||||
}
|
||||
};
|
||||
|
||||
_logger.LogInformation("Start Telegram polling for channel {ChannelId}", _telegram.ChannelId);
|
||||
|
||||
using var semaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
bot.StartReceiving(
|
||||
async (client, update, ct) =>
|
||||
{
|
||||
await semaphore.WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
await HandleUpdateAsync(update, bot, ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
},
|
||||
HandleErrorAsync,
|
||||
receiverOptions,
|
||||
stoppingToken);
|
||||
|
||||
// StartReceiving не блокирует, поэтому держим сервис живым.
|
||||
await Task.Delay(Timeout.InfiniteTimeSpan, stoppingToken);
|
||||
}
|
||||
|
||||
private async Task HandleUpdateAsync(Update update, TelegramBotClient bot, CancellationToken ct)
|
||||
{
|
||||
if (update.EditedChannelPost is { } edited && edited.Chat.Id == _telegram.ChannelId)
|
||||
{
|
||||
await HandleChannelPostEditAsync(bot, edited, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (update.ChannelPost is { } msg && msg.Chat.Id == _telegram.ChannelId)
|
||||
{
|
||||
await HandleChannelPostAsync(bot, msg, ct);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleChannelPostAsync(TelegramBotClient bot, Message message, CancellationToken ct)
|
||||
{
|
||||
var ownerId = -Math.Abs(_vk.GroupId);
|
||||
var telegramMessageId = message.MessageId;
|
||||
|
||||
var text = ExtractTelegramText(message);
|
||||
var attachments = await ExtractPhotoAttachmentIfAnyAsync(bot, message, ct);
|
||||
|
||||
// Если это не текст и не фото, просто пропускаем (сложно маппить видео/документы в VK без доп. логики).
|
||||
if (string.IsNullOrWhiteSpace(text) && string.IsNullOrWhiteSpace(attachments))
|
||||
{
|
||||
_logger.LogInformation("Skip message {MessageId}: no text/photo", telegramMessageId);
|
||||
return;
|
||||
}
|
||||
|
||||
var postId = await _vkApi.WallPostAsync(ownerId, text ?? string.Empty, attachments, ct);
|
||||
await _repo.UpsertAsync(
|
||||
telegramChatId: _telegram.ChannelId,
|
||||
telegramMessageId: telegramMessageId,
|
||||
vkOwnerId: ownerId,
|
||||
vkPostId: postId,
|
||||
vkMessage: text,
|
||||
vkAttachments: attachments,
|
||||
ct);
|
||||
|
||||
_logger.LogInformation("Posted new VK post {PostId} for Telegram {MessageId}", postId, telegramMessageId);
|
||||
}
|
||||
|
||||
private async Task HandleChannelPostEditAsync(TelegramBotClient bot, Message editedMessage, CancellationToken ct)
|
||||
{
|
||||
var telegramMessageId = editedMessage.MessageId;
|
||||
var ownerId = -Math.Abs(_vk.GroupId);
|
||||
|
||||
var existing = await _repo.GetAsync(_telegram.ChannelId, telegramMessageId, ct);
|
||||
|
||||
var extractedText = ExtractTelegramText(editedMessage);
|
||||
|
||||
// Если фото в апдейте отсутствует, то оставляем старые вложения, чтобы не "стереть" медиа на стороне VK.
|
||||
string? extractedAttachments = await ExtractPhotoAttachmentIfAnyAsync(bot, editedMessage, ct);
|
||||
var attachmentsToSend = !string.IsNullOrWhiteSpace(extractedAttachments)
|
||||
? extractedAttachments
|
||||
: existing?.VkAttachments;
|
||||
|
||||
var messageToSend = !string.IsNullOrWhiteSpace(extractedText)
|
||||
? extractedText
|
||||
: existing?.VkMessage;
|
||||
|
||||
if (existing == null)
|
||||
{
|
||||
_logger.LogWarning("Mapping not found for edited Telegram {MessageId}; posting new VK post", telegramMessageId);
|
||||
if (string.IsNullOrWhiteSpace(messageToSend) && string.IsNullOrWhiteSpace(attachmentsToSend))
|
||||
return;
|
||||
|
||||
var newPostId = await _vkApi.WallPostAsync(ownerId, messageToSend ?? string.Empty, attachmentsToSend, ct);
|
||||
await _repo.UpsertAsync(_telegram.ChannelId, telegramMessageId, ownerId, newPostId, messageToSend, attachmentsToSend, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageToSend == null) messageToSend = existing.Value.VkMessage;
|
||||
if (attachmentsToSend == null) attachmentsToSend = existing.Value.VkAttachments;
|
||||
|
||||
var messageToSendFinal = messageToSend ?? string.Empty;
|
||||
|
||||
await _vkApi.WallEditAsync(existing.Value.VkOwnerId, existing.Value.VkPostId, messageToSendFinal, attachmentsToSend, ct);
|
||||
await _repo.UpsertAsync(_telegram.ChannelId, telegramMessageId, existing.Value.VkOwnerId, existing.Value.VkPostId, messageToSendFinal, attachmentsToSend, ct);
|
||||
|
||||
_logger.LogInformation("Edited VK post {PostId} for Telegram {MessageId}", existing.Value.VkPostId, telegramMessageId);
|
||||
}
|
||||
|
||||
private static string? ExtractTelegramText(Message message)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(message.Text))
|
||||
return message.Text;
|
||||
if (!string.IsNullOrWhiteSpace(message.Caption))
|
||||
return message.Caption;
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<string?> ExtractPhotoAttachmentIfAnyAsync(TelegramBotClient bot, Message message, CancellationToken ct)
|
||||
{
|
||||
if (message.Photo == null || message.Photo.Length == 0)
|
||||
return null;
|
||||
|
||||
// Telegram предоставляет несколько размеров одного фото. Берем самый большой.
|
||||
var best = message.Photo[^1];
|
||||
|
||||
await using var ms = new MemoryStream();
|
||||
// В Telegram.Bot v22 методы называются без Async-суффикса.
|
||||
await bot.GetInfoAndDownloadFile(best.FileId, ms, ct);
|
||||
|
||||
return await _vkApi.UploadPhotoAndGetAttachmentAsync(ms.ToArray(), ct);
|
||||
}
|
||||
|
||||
private static Task HandleErrorAsync(
|
||||
ITelegramBotClient client,
|
||||
Exception exception,
|
||||
HandleErrorSource errorSource,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Логирование делаем в Worker через исключения, но чтобы не терять ошибки polling'а — просто возвращаем CompletedTask.
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace Telegram2VkBot;
|
||||
|
||||
public sealed class MappingRepository
|
||||
{
|
||||
private readonly DbOptions _dbOptions;
|
||||
private readonly ILogger<MappingRepository> _logger;
|
||||
|
||||
public MappingRepository(DbOptions dbOptions, ILogger<MappingRepository> logger)
|
||||
{
|
||||
_dbOptions = dbOptions;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private string ConnectionString => $"Data Source={_dbOptions.Path};Cache=Shared";
|
||||
|
||||
public async Task InitializeAsync(CancellationToken ct)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(_dbOptions.Path);
|
||||
if (!string.IsNullOrWhiteSpace(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
|
||||
await using var con = new SqliteConnection(ConnectionString);
|
||||
await con.OpenAsync(ct);
|
||||
|
||||
var sql = @"
|
||||
CREATE TABLE IF NOT EXISTS vk_mappings (
|
||||
telegram_chat_id INTEGER NOT NULL,
|
||||
telegram_message_id INTEGER NOT NULL,
|
||||
vk_owner_id INTEGER NOT NULL,
|
||||
vk_post_id INTEGER NOT NULL,
|
||||
vk_message TEXT NULL,
|
||||
vk_attachments TEXT NULL,
|
||||
PRIMARY KEY (telegram_chat_id, telegram_message_id)
|
||||
);";
|
||||
|
||||
await using var cmd = con.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
await cmd.ExecuteNonQueryAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<(long VkOwnerId, int VkPostId, string? VkMessage, string? VkAttachments)?> GetAsync(
|
||||
long telegramChatId,
|
||||
int telegramMessageId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await using var con = new SqliteConnection(ConnectionString);
|
||||
await con.OpenAsync(ct);
|
||||
|
||||
var sql = @"
|
||||
SELECT vk_owner_id, vk_post_id, vk_message, vk_attachments
|
||||
FROM vk_mappings
|
||||
WHERE telegram_chat_id = $telegram_chat_id AND telegram_message_id = $telegram_message_id
|
||||
LIMIT 1;";
|
||||
|
||||
await using var cmd = con.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
cmd.Parameters.AddWithValue("$telegram_chat_id", telegramChatId);
|
||||
cmd.Parameters.AddWithValue("$telegram_message_id", telegramMessageId);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
if (await reader.ReadAsync(ct))
|
||||
{
|
||||
var vkOwnerId = reader.GetInt64(0);
|
||||
var vkPostId = reader.GetInt32(1);
|
||||
var vkMessage = reader.IsDBNull(2) ? null : reader.GetString(2);
|
||||
var vkAttachments = reader.IsDBNull(3) ? null : reader.GetString(3);
|
||||
return (vkOwnerId, vkPostId, vkMessage, vkAttachments);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task UpsertAsync(
|
||||
long telegramChatId,
|
||||
int telegramMessageId,
|
||||
long vkOwnerId,
|
||||
int vkPostId,
|
||||
string? vkMessage,
|
||||
string? vkAttachments,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await using var con = new SqliteConnection(ConnectionString);
|
||||
await con.OpenAsync(ct);
|
||||
|
||||
var sql = @"
|
||||
INSERT INTO vk_mappings (
|
||||
telegram_chat_id,
|
||||
telegram_message_id,
|
||||
vk_owner_id,
|
||||
vk_post_id,
|
||||
vk_message,
|
||||
vk_attachments
|
||||
)
|
||||
VALUES (
|
||||
$telegram_chat_id,
|
||||
$telegram_message_id,
|
||||
$vk_owner_id,
|
||||
$vk_post_id,
|
||||
$vk_message,
|
||||
$vk_attachments
|
||||
)
|
||||
ON CONFLICT (telegram_chat_id, telegram_message_id) DO UPDATE SET
|
||||
vk_owner_id = excluded.vk_owner_id,
|
||||
vk_post_id = excluded.vk_post_id,
|
||||
vk_message = excluded.vk_message,
|
||||
vk_attachments = excluded.vk_attachments;";
|
||||
|
||||
await using var cmd = con.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
cmd.Parameters.AddWithValue("$telegram_chat_id", telegramChatId);
|
||||
cmd.Parameters.AddWithValue("$telegram_message_id", telegramMessageId);
|
||||
cmd.Parameters.AddWithValue("$vk_owner_id", vkOwnerId);
|
||||
cmd.Parameters.AddWithValue("$vk_post_id", vkPostId);
|
||||
cmd.Parameters.AddWithValue("$vk_message", (object?)vkMessage ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("$vk_attachments", (object?)vkAttachments ?? DBNull.Value);
|
||||
|
||||
await cmd.ExecuteNonQueryAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Telegram2VkBot;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
builder.Services.Configure<TelegramOptions>(builder.Configuration.GetSection("TELEGRAM"));
|
||||
builder.Services.Configure<VkOptions>(builder.Configuration.GetSection("VK"));
|
||||
builder.Services.Configure<DbOptions>(builder.Configuration.GetSection("DB"));
|
||||
|
||||
builder.Services.AddHttpClient<VkApiClient>(client =>
|
||||
{
|
||||
client.Timeout = TimeSpan.FromSeconds(60);
|
||||
});
|
||||
builder.Services.AddSingleton<MappingRepository>();
|
||||
|
||||
builder.Services.AddHostedService<ForwardWorker>();
|
||||
|
||||
builder.Logging.AddSimpleConsole(options =>
|
||||
{
|
||||
options.SingleLine = true;
|
||||
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
|
||||
});
|
||||
|
||||
var host = builder.Build();
|
||||
await host.RunAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Telegram.Bot" Version="22.9.5.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Telegram2VkBot;
|
||||
|
||||
public sealed class VkApiClient
|
||||
{
|
||||
private const string VkApiBaseUrl = "https://api.vk.com/method/";
|
||||
|
||||
private readonly VkOptions _options;
|
||||
private readonly HttpClient _http;
|
||||
private readonly JsonSerializerOptions _json;
|
||||
private readonly ILogger<VkApiClient> _logger;
|
||||
|
||||
public VkApiClient(IOptions<VkOptions> options, HttpClient http, ILogger<VkApiClient> logger)
|
||||
{
|
||||
_options = options.Value;
|
||||
_http = http;
|
||||
_logger = logger;
|
||||
_json = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||
}
|
||||
|
||||
public async Task<int> WallPostAsync(
|
||||
long ownerId,
|
||||
string message,
|
||||
string? attachments,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
["owner_id"] = ownerId.ToString(),
|
||||
["message"] = message ?? string.Empty,
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(attachments))
|
||||
{
|
||||
parameters["attachments"] = attachments;
|
||||
}
|
||||
|
||||
var response = await CallMethodAsync("wall.post", parameters, ct);
|
||||
// response: { "post_id": 123 }
|
||||
return response.GetProperty("post_id").GetInt32();
|
||||
}
|
||||
|
||||
public async Task WallEditAsync(
|
||||
long ownerId,
|
||||
int postId,
|
||||
string message,
|
||||
string? attachments,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
["owner_id"] = ownerId.ToString(),
|
||||
["post_id"] = postId.ToString(),
|
||||
["message"] = message ?? string.Empty,
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(attachments))
|
||||
{
|
||||
parameters["attachments"] = attachments;
|
||||
}
|
||||
|
||||
// response: 1
|
||||
await CallMethodAsync("wall.edit", parameters, ct);
|
||||
}
|
||||
|
||||
public async Task<string?> UploadPhotoAndGetAttachmentAsync(
|
||||
byte[] photoBytes,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// 1) photos.getUploadServer
|
||||
var uploadServerResponse = await CallMethodAsync(
|
||||
"photos.getUploadServer",
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["group_id"] = _options.GroupId.ToString(),
|
||||
},
|
||||
ct);
|
||||
|
||||
var uploadUrl = uploadServerResponse.GetProperty("upload_url").GetString();
|
||||
if (string.IsNullOrWhiteSpace(uploadUrl))
|
||||
{
|
||||
_logger.LogWarning("VK upload_url is empty");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2) Upload bytes to upload_url (VK expects multipart form with field name 'photo')
|
||||
using var form = new MultipartFormDataContent();
|
||||
var byteContent = new ByteArrayContent(photoBytes);
|
||||
byteContent.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");
|
||||
form.Add(byteContent, "photo", "photo.jpg");
|
||||
|
||||
using var uploadResp = await _http.PostAsync(uploadUrl, form, ct);
|
||||
var uploadRespText = await uploadResp.Content.ReadAsStringAsync(ct);
|
||||
if (!uploadResp.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("VK upload failed: {StatusCode} {Body}", uploadResp.StatusCode, uploadRespText);
|
||||
return null;
|
||||
}
|
||||
|
||||
using var uploadDoc = JsonDocument.Parse(uploadRespText);
|
||||
// upload response: { server, photo, hash, ... }
|
||||
var server = uploadDoc.RootElement.GetProperty("server").GetString() ?? "";
|
||||
var photo = uploadDoc.RootElement.GetProperty("photo").GetString() ?? "";
|
||||
var hash = uploadDoc.RootElement.GetProperty("hash").GetString() ?? "";
|
||||
|
||||
// 3) photos.saveWallPhoto
|
||||
var saved = await CallMethodAsync(
|
||||
"photos.saveWallPhoto",
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["group_id"] = _options.GroupId.ToString(),
|
||||
["server"] = server,
|
||||
["photo"] = photo,
|
||||
["hash"] = hash,
|
||||
},
|
||||
ct);
|
||||
|
||||
// saved response usually: { "photos": [ { "id": ..., "owner_id": ... } ] }
|
||||
if (saved.TryGetProperty("photos", out var photos) && photos.GetArrayLength() > 0)
|
||||
{
|
||||
var first = photos[0];
|
||||
var ownerId = first.GetProperty("owner_id").GetInt32();
|
||||
var id = first.GetProperty("id").GetInt32();
|
||||
return $"photo{ownerId}_{id}";
|
||||
}
|
||||
|
||||
_logger.LogWarning("VK photos.saveWallPhoto response has no photos array");
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<JsonElement> CallMethodAsync(
|
||||
string method,
|
||||
IReadOnlyDictionary<string, string> parameters,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var query = new Dictionary<string, string>(parameters)
|
||||
{
|
||||
["access_token"] = _options.AccessToken,
|
||||
["v"] = _options.ApiVersion,
|
||||
};
|
||||
|
||||
using var content = new FormUrlEncodedContent(query);
|
||||
using var resp = await _http.PostAsync(VkApiBaseUrl + method, content, ct);
|
||||
var body = await resp.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException($"VK API HTTP error: {(int)resp.StatusCode} {body}");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.TryGetProperty("error", out var error))
|
||||
{
|
||||
var message = error.TryGetProperty("error_msg", out var em) ? em.GetString() : null;
|
||||
var code = error.TryGetProperty("error_code", out var ec) ? ec.ToString() : null;
|
||||
throw new InvalidOperationException($"VK API error: code={code} msg={message}");
|
||||
}
|
||||
|
||||
if (!root.TryGetProperty("response", out var response))
|
||||
{
|
||||
throw new InvalidOperationException($"VK API unexpected response: {body}");
|
||||
}
|
||||
|
||||
// Return detached element by cloning to avoid disposing doc
|
||||
return JsonDocument.Parse(response.GetRawText()).RootElement.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user