Add channel search and subscription
This commit is contained in:
@@ -18,3 +18,13 @@ public sealed record MaxBrowserSnapshotDto(
|
||||
string BodyText,
|
||||
string ScreenshotPngBase64,
|
||||
DateTimeOffset CapturedAt);
|
||||
|
||||
public sealed record MaxChannelSearchResultDto(
|
||||
string ExternalId,
|
||||
string Title,
|
||||
string? AvatarUrl,
|
||||
string? ChatUrl,
|
||||
bool IsSubscribed,
|
||||
string? Description);
|
||||
|
||||
public sealed record SubscribeMaxChannelRequest(string Link);
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class MaxController(
|
||||
MaxBridgeSyncService syncService,
|
||||
QMaxDbContext db,
|
||||
IHubContext<QMaxHub> hubContext,
|
||||
ChatProjectionService projection,
|
||||
IOptions<QMaxOptions> options) : ControllerBase
|
||||
{
|
||||
private readonly QMaxOptions _options = options.Value;
|
||||
@@ -69,6 +70,51 @@ public sealed class MaxController(
|
||||
return new { changed };
|
||||
}
|
||||
|
||||
[HttpGet("channels/search")]
|
||||
public async Task<ActionResult<IReadOnlyList<MaxChannelSearchResultDto>>> SearchChannels(
|
||||
string q,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
return Array.Empty<MaxChannelSearchResultDto>();
|
||||
}
|
||||
|
||||
var results = await maxBridge.SearchChannelsAsync(q.Trim(), cancellationToken);
|
||||
return results.Select(ToDto).ToArray();
|
||||
}
|
||||
|
||||
[HttpPost("channels/subscribe")]
|
||||
public async Task<ActionResult<ChatDto>> SubscribeChannel(
|
||||
SubscribeMaxChannelRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Link))
|
||||
{
|
||||
return BadRequest("Channel link is required.");
|
||||
}
|
||||
|
||||
var update = await maxBridge.JoinChannelAsync(request.Link.Trim(), cancellationToken);
|
||||
if (update is null)
|
||||
{
|
||||
return BadRequest("MAX did not return a channel for this link.");
|
||||
}
|
||||
|
||||
await syncService.ApplyJoinedChannelAsync(update, cancellationToken);
|
||||
var chat = await db.Chats.FirstOrDefaultAsync(
|
||||
x => x.DeletedAt == null &&
|
||||
(x.ExternalId == update.ExternalId ||
|
||||
(!string.IsNullOrWhiteSpace(update.ChatUrl) && x.WebUrl == update.ChatUrl)),
|
||||
cancellationToken);
|
||||
if (chat is null)
|
||||
{
|
||||
return BadRequest("Channel was joined but was not saved in QMAX.");
|
||||
}
|
||||
|
||||
await hubContext.Clients.All.SendAsync("ChatListInvalidated", cancellationToken);
|
||||
return projection.ToDto(chat);
|
||||
}
|
||||
|
||||
private async Task SaveStateAsync(MaxBridgeStatus status, CancellationToken cancellationToken)
|
||||
{
|
||||
var state = await db.MaxAccountStates.FirstOrDefaultAsync(x => x.Id == 1, cancellationToken);
|
||||
@@ -92,4 +138,9 @@ public sealed class MaxController(
|
||||
{
|
||||
return new MaxBridgeStatusDto(status.Mode, status.IsAuthorized, status.LoginStage, status.Status, status.Url, status.Title, status.LastError, status.UpdatedAt);
|
||||
}
|
||||
|
||||
private static MaxChannelSearchResultDto ToDto(MaxChannelSearchResult channel)
|
||||
{
|
||||
return new MaxChannelSearchResultDto(channel.ExternalId, channel.Title, channel.AvatarUrl, channel.ChatUrl, channel.IsSubscribed, channel.Description);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +19,13 @@ public interface IMaxBridgeClient
|
||||
Task<MaxActionResult> SetReactionAsync(string externalChatId, string externalMessageId, string? currentText, string emoji, CancellationToken cancellationToken);
|
||||
Task<MaxActionResult> ClearReactionAsync(string externalChatId, string externalMessageId, string? currentText, string emoji, CancellationToken cancellationToken);
|
||||
Task<MaxMediaDownload?> DownloadMediaAsync(string remoteUrl, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<MaxChannelSearchResult>> SearchChannelsAsync(string query, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<MaxChannelSearchResult>>(Array.Empty<MaxChannelSearchResult>());
|
||||
}
|
||||
|
||||
Task<MaxChatUpdate?> JoinChannelAsync(string link, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult<MaxChatUpdate?>(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,3 +65,11 @@ public sealed record MaxChatPresence(
|
||||
bool IsTyping,
|
||||
string? StatusText,
|
||||
DateTimeOffset UpdatedAt);
|
||||
|
||||
public sealed record MaxChannelSearchResult(
|
||||
string ExternalId,
|
||||
string Title,
|
||||
string? AvatarUrl,
|
||||
string? ChatUrl,
|
||||
bool IsSubscribed,
|
||||
string? Description = null);
|
||||
|
||||
@@ -108,6 +108,30 @@ public sealed class MockMaxBridgeClient : IMaxBridgeClient
|
||||
return Task.FromResult<MaxMediaDownload?>(null);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<MaxChannelSearchResult>> SearchChannelsAsync(string query, CancellationToken cancellationToken)
|
||||
{
|
||||
IReadOnlyList<MaxChannelSearchResult> result = string.IsNullOrWhiteSpace(query)
|
||||
? []
|
||||
: [new MaxChannelSearchResult("mock-channel", "Mock channel", null, "mock-channel", false, "Mock MAX channel")];
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task<MaxChatUpdate?> JoinChannelAsync(string link, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult<MaxChatUpdate?>(new MaxChatUpdate(
|
||||
"mock-channel",
|
||||
"Mock channel",
|
||||
null,
|
||||
DateTimeOffset.UtcNow,
|
||||
[],
|
||||
"Mock channel subscribed",
|
||||
false,
|
||||
DateTimeOffset.UtcNow,
|
||||
null,
|
||||
MockChatUrl("mock-channel"),
|
||||
"Channel"));
|
||||
}
|
||||
|
||||
private static string MockChatUrl(string externalChatId)
|
||||
{
|
||||
return $"pymax://mock/{Uri.EscapeDataString(externalChatId)}";
|
||||
|
||||
@@ -160,6 +160,25 @@ public sealed class WorkerMaxBridgeClient(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<MaxChannelSearchResult>> SearchChannelsAsync(string query, CancellationToken cancellationToken)
|
||||
{
|
||||
return await SendAsync<IReadOnlyList<MaxChannelSearchResult>>(
|
||||
HttpMethod.Post,
|
||||
"/channels/search",
|
||||
new { query },
|
||||
cancellationToken)
|
||||
?? Array.Empty<MaxChannelSearchResult>();
|
||||
}
|
||||
|
||||
public async Task<MaxChatUpdate?> JoinChannelAsync(string link, CancellationToken cancellationToken)
|
||||
{
|
||||
return await SendAsync<MaxChatUpdate>(
|
||||
HttpMethod.Post,
|
||||
"/channels/join",
|
||||
new { link },
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<T?> SendAsync<T>(HttpMethod method, string path, object? body, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -93,6 +93,16 @@ public sealed class MaxBridgeSyncService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> ApplyJoinedChannelAsync(MaxChatUpdate update, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ApplyUpdatesAsync(
|
||||
[update],
|
||||
incrementUnread: false,
|
||||
sendPushNotifications: false,
|
||||
cancellationToken,
|
||||
allowGenericMediaHistoryFetch: false);
|
||||
}
|
||||
|
||||
private async Task<int> ApplyUpdatesAsync(
|
||||
IReadOnlyList<MaxChatUpdate> updates,
|
||||
bool incrementUnread,
|
||||
|
||||
Reference in New Issue
Block a user