147 lines
5.4 KiB
C#
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);
|
|
}
|
|
}
|