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 hubContext, ChatProjectionService projection, IOptions options) : ControllerBase { private readonly QMaxOptions _options = options.Value; [HttpGet("status")] public async Task> Status(CancellationToken cancellationToken) { var status = await maxBridge.GetStatusAsync(cancellationToken); await SaveStateAsync(status, cancellationToken); return ToDto(status); } [HttpPost("login/start")] public async Task> 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> 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> 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> Sync(CancellationToken cancellationToken) { var changed = await syncService.SyncOnceAsync(cancellationToken); return new { changed }; } [HttpGet("channels/search")] public async Task>> SearchChannels( string q, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(q)) { return Array.Empty(); } var results = await maxBridge.SearchChannelsAsync(q.Trim(), cancellationToken); return results.Select(ToDto).ToArray(); } [HttpPost("channels/subscribe")] public async Task> 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); } }