Добавить remote-просмотр исходника карточки

RPC TelegramService.ReadSource + ISessionClient.GetMessageAsync, порт ITelegramGateway.ReadSourceAsync и провайдер TelegramSourceContentProvider в ядре; GET /api/cards/{id}/source догружает исходник у сервиса-владельца, в UI — кнопка «Обновить из источника». Медиа-посты пропускаются.
This commit is contained in:
Rustam Khalimov
2026-09-11 17:22:06 +03:00
parent c06ee1cf79
commit db4455a554
30 changed files with 742 additions and 10 deletions
@@ -92,6 +92,11 @@ public static class SessionErrorMessages
/// </summary>
public const string InvalidDialogId = "Некорректный id источника";
/// <summary>
/// Неположительный id сообщения в запросе
/// </summary>
public const string InvalidMessageId = "Некорректный id сообщения";
/// <summary>
/// Телефон не зарегистрирован в Telegram
/// </summary>
@@ -73,6 +73,20 @@ public sealed class SessionFarm
CancellationToken cancellationToken)
=> RequireSession(tenantId).GetMessagesAsync(dialogId, limit, cancellationToken);
/// <summary>
/// Исходное сообщение диалога тенанта по id.
/// </summary>
/// <param name="tenantId">Id тенанта.</param>
/// <param name="dialogId">Подписанный id диалога.</param>
/// <param name="msgId">Id сообщения в Telegram.</param>
/// <param name="cancellationToken">Отмена операции.</param>
public Task<TelegramMessage?> GetMessageAsync(
string tenantId,
string dialogId,
long msgId,
CancellationToken cancellationToken)
=> RequireSession(tenantId).GetMessageAsync(dialogId, msgId, cancellationToken);
/// <summary>
/// Помечает диалог тенанта прочитанным.
/// </summary>
@@ -416,6 +416,30 @@ public sealed class TenantSession : IAsyncDisposable
}
}
/// <summary>
/// Исходное сообщение диалога по id (remote-просмотр).
/// </summary>
/// <param name="dialogId">Подписанный id диалога.</param>
/// <param name="msgId">Id сообщения в Telegram.</param>
/// <param name="cancellationToken">Отмена операции.</param>
/// <returns>Сообщение с текстом либо null (медиа без текста/не найдено).</returns>
public async Task<TelegramMessage?> GetMessageAsync(
string dialogId,
long msgId,
CancellationToken cancellationToken)
{
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
ISessionClient client = await EnsureReadyConnectedAsync(cancellationToken).ConfigureAwait(false);
return await client.GetMessageAsync(dialogId, msgId, cancellationToken).ConfigureAwait(false);
}
finally
{
_gate.Release();
}
}
/// <summary>
/// Помечает диалог прочитанным
/// </summary>
@@ -98,6 +98,18 @@ public interface ISessionClient : IAsyncDisposable
int limit,
CancellationToken cancellationToken);
/// <summary>
/// Возвращает исходное сообщение диалога по id (remote-просмотр карточки).
/// </summary>
/// <param name="dialogId">Подписанный id диалога (каналы "-100…", группы "-…", личные "+…").</param>
/// <param name="msgId">Id сообщения в Telegram.</param>
/// <param name="cancellationToken">Отмена операции.</param>
/// <returns>Сообщение с текстом либо null (медиа без текста/не найдено).</returns>
public Task<TelegramMessage?> GetMessageAsync(
string dialogId,
long msgId,
CancellationToken cancellationToken);
/// <summary>
/// Помечает весь диалог прочитанным.
/// </summary>
@@ -53,6 +53,7 @@ public static class TlMessageMapper
{
if (message is not Message textMessage
|| string.IsNullOrWhiteSpace(textMessage.message)
|| textMessage.media is not null
|| (textMessage.flags & Message.Flags.out_) != 0
|| textMessage.Peer is null)
{
@@ -264,6 +264,36 @@ public sealed class WTelegramSessionClient : ISessionClient
return items;
}
/// <inheritdoc />
public async Task<TelegramMessage?> GetMessageAsync(
string dialogId,
long msgId,
CancellationToken cancellationToken)
{
// Диалог обязан быть доступен аккаунту (иначе INVALID_ARGUMENT, как в истории).
await ResolvePeerAsync(dialogId, cancellationToken).ConfigureAwait(false);
// messages.getMessages по голому id (InputMessageID) — без привязки к peer.
InputMessage[] ids = [new InputMessageID { id = (int)msgId }];
Messages_MessagesBase result = await RunTlCallAsync(
() => _client.Messages_GetMessages(ids),
cancellationToken).ConfigureAwait(false);
(MessageBase[] messages, Dictionary<long, ChatBase> chats, Dictionary<long, User> users) = UnpackMessages(result);
CacheEntities(chats.Values, users.Values);
foreach (MessageBase message in messages)
{
TelegramMessage? mapped = TlMessageMapper.ToMessage(message, chats, users);
if (mapped is not null && mapped.Id == msgId && mapped.DialogId == dialogId)
{
return mapped;
}
}
// Медиа без текста или удалённое сообщение — не найдено (found=false).
return null;
}
/// <inheritdoc />
public async Task MarkReadAsync(string dialogId, CancellationToken cancellationToken)
{
@@ -291,6 +291,44 @@ public sealed class TelegramServiceImpl : TelegramService.TelegramServiceBase
}
/// <summary>
/// ReadSource — исходное сообщение источника по id (remote-просмотр).
/// </summary>
public override async Task<ReadSourceReply> ReadSource(ReadSourceRequest request, ServerCallContext context)
{
string tenantId = RequireTenantId(context);
RequireDialogId(request.DialogId);
if (request.MsgId <= 0)
{
throw new RpcException(new Status(StatusCode.InvalidArgument, SessionErrorMessages.InvalidMessageId));
}
ReadSourceReply reply = await ExecuteAsync(
tenantId,
async ct =>
{
Telegram.TelegramMessage? message = await _sessionFarm
.GetMessageAsync(tenantId, request.DialogId, request.MsgId, ct)
.ConfigureAwait(false);
if (message is null)
{
return new ReadSourceReply { Found = false };
}
return new ReadSourceReply
{
Found = true,
Text = message.Text,
Time = message.DateMs,
};
},
"read_source",
context).ConfigureAwait(false);
return reply;
}
/// <summary>
/// Search — глобальный поиск каналов/групп по ключу.
/// </summary>