Files
2026-07-07 22:09:18 +03:00

147 lines
5.4 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using QMax.Api.Configuration;
using QMax.Api.Contracts;
using QMax.Api.Data;
using QMax.Api.Data.Entities;
using QMax.Api.Infrastructure.Hubs;
using QMax.Api.Infrastructure.Max;
using QMax.Api.Services;
namespace QMax.Api.Controllers;
[ApiController]
[Authorize]
[Route("api/max")]
public sealed class MaxController(
IMaxBridgeClient maxBridge,
MaxBridgeSyncService syncService,
QMaxDbContext db,
IHubContext<QMaxHub> hubContext,
ChatProjectionService projection,
IOptions<QMaxOptions> options) : ControllerBase
{
private readonly QMaxOptions _options = options.Value;
[HttpGet("status")]
public async Task<ActionResult<MaxBridgeStatusDto>> Status(CancellationToken cancellationToken)
{
var status = await maxBridge.GetStatusAsync(cancellationToken);
await SaveStateAsync(status, cancellationToken);
return ToDto(status);
}
[HttpPost("login/start")]
public async Task<ActionResult<MaxBridgeStatusDto>> BeginLogin(BeginMaxLoginRequest request, CancellationToken cancellationToken)
{
var phone = string.IsNullOrWhiteSpace(request.PhoneNumber) ? _options.MaxPhoneNumber : request.PhoneNumber;
if (string.IsNullOrWhiteSpace(phone))
{
return BadRequest("Phone number is required.");
}
var status = await maxBridge.BeginPhoneLoginAsync(phone, cancellationToken);
await SaveStateAsync(status, cancellationToken);
return ToDto(status);
}
[HttpPost("login/code")]
public async Task<ActionResult<MaxBridgeStatusDto>> SubmitCode(SubmitMaxCodeRequest request, CancellationToken cancellationToken)
{
var status = await maxBridge.SubmitLoginCodeAsync(request.Code, cancellationToken);
await SaveStateAsync(status, cancellationToken);
return ToDto(status);
}
[HttpGet("browser/snapshot")]
public async Task<ActionResult<MaxBrowserSnapshotDto>> Snapshot(CancellationToken cancellationToken)
{
var snapshot = await maxBridge.GetSnapshotAsync(cancellationToken);
return new MaxBrowserSnapshotDto(snapshot.Url, snapshot.Title, snapshot.BodyText, snapshot.ScreenshotPngBase64, snapshot.CapturedAt);
}
[HttpPost("sync")]
public async Task<ActionResult<object>> Sync(CancellationToken cancellationToken)
{
var changed = await syncService.SyncOnceAsync(cancellationToken);
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);
if (state is null)
{
state = new MaxAccountState { Id = 1 };
db.MaxAccountStates.Add(state);
}
state.PhoneNumber = _options.MaxPhoneNumber;
state.Status = status.Status;
state.IsAuthorized = status.IsAuthorized;
state.LastUrl = status.Url;
state.LastError = status.LastError;
state.UpdatedAt = DateTimeOffset.UtcNow;
await db.SaveChangesAsync(cancellationToken);
await hubContext.Clients.All.SendAsync("MaxStatusChanged", ToDto(status), cancellationToken);
}
private static MaxBridgeStatusDto ToDto(MaxBridgeStatus status)
{
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);
}
}