fix: align backend APIs and upload flow
This commit is contained in:
@@ -8,7 +8,7 @@ namespace MessageService.WebApi.Application.Conversation
|
||||
public ConversationMapperConfig()
|
||||
{
|
||||
CreateMap<Domain.Entities.Conversation, ConversationResponse>()
|
||||
.ForMember(dest => dest.DateTime, opt => opt.MapFrom(src => src.ModificationTime))
|
||||
.ForMember(dest => dest.DateTime, opt => opt.MapFrom(src => src.LastMessageTime ?? src.CreationTime))
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using AutoMapper;
|
||||
using IM.Commons;
|
||||
using System.Diagnostics;
|
||||
using MessageService.Domain.IReposities;
|
||||
using MessageService.WebApi.Application.Dtos;
|
||||
|
||||
@@ -9,22 +10,53 @@ namespace MessageService.WebApi.Application.Conversation
|
||||
{
|
||||
private readonly IConversationReposity reposity;
|
||||
private readonly IMapper mapper;
|
||||
private readonly ILogger<ConversationService> logger;
|
||||
|
||||
public ConversationService(IConversationReposity reposity, IMapper mapper)
|
||||
public ConversationService(IConversationReposity reposity, IMapper mapper, ILogger<ConversationService> logger)
|
||||
{
|
||||
this.reposity = reposity;
|
||||
this.mapper = mapper;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Result<List<ConversationResponse>>> GetByOwnerIdAsync(Guid userId)
|
||||
public async Task<Result<List<ConversationResponse>>> GetByOwnerIdAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var list = await reposity.FindByUserIdAsync(userId);
|
||||
return Result.Success(mapper.Map<List<ConversationResponse>>(list.ToList()));
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var list = await reposity.ListByUserIdAsync(userId, cancellationToken);
|
||||
return Result.Success(list.Select(item => new ConversationResponse
|
||||
{
|
||||
Id = item.Id,
|
||||
UserId = item.UserId,
|
||||
TargetId = item.TargetId,
|
||||
TargetAvatar = item.TargetAvatar,
|
||||
TargetName = item.TargetName,
|
||||
LastReadSequenceId = item.LastReadSequenceId,
|
||||
UnreadCount = item.UnreadCount,
|
||||
ChatType = item.ChatType,
|
||||
LastMessage = item.LastMessage,
|
||||
DateTime = item.DateTime
|
||||
}).ToList());
|
||||
}
|
||||
finally
|
||||
{
|
||||
stopwatch.Stop();
|
||||
var traceId = Activity.Current?.TraceId.ToString() ?? string.Empty;
|
||||
if (stopwatch.ElapsedMilliseconds > 1000)
|
||||
{
|
||||
logger.LogWarning("Conversation list query was slow. TraceId={TraceId} UserId={UserId} ElapsedMs={ElapsedMs}", traceId, userId, stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("Conversation list query completed. TraceId={TraceId} UserId={UserId} ElapsedMs={ElapsedMs}", traceId, userId, stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<ConversationResponse>> GetByIdAsync(Guid id, Guid userId)
|
||||
public async Task<Result<ConversationResponse>> GetByIdAsync(Guid id, Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var conversation = await reposity.FindByIdAsync(id);
|
||||
var conversation = await reposity.FindByIdAsync(id, cancellationToken);
|
||||
|
||||
if (conversation is null || conversation.UserId != userId)
|
||||
{
|
||||
@@ -34,21 +66,21 @@ namespace MessageService.WebApi.Application.Conversation
|
||||
return Result.Success(mapper.Map<ConversationResponse>(conversation));
|
||||
}
|
||||
|
||||
public async Task<Result<List<string>>> GetStreamkeysAsync(Guid userId)
|
||||
public async Task<Result<List<string>>> GetStreamkeysAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var list = await reposity.FindAllStreamKeyAsync(userId);
|
||||
var list = await reposity.FindAllStreamKeyAsync(userId, cancellationToken);
|
||||
return Result.Success(list.ToList());
|
||||
}
|
||||
|
||||
public async Task<Result<object>> MarkAsReadAsync(Guid conversationId, Guid userId)
|
||||
public async Task<Result<object>> MarkAsReadAsync(Guid conversationId, Guid userId, long? lastReadSequenceId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var conversation = await reposity.FindByIdAsync(conversationId);
|
||||
var conversation = await reposity.FindByIdAsync(conversationId, cancellationToken);
|
||||
if (conversation is null || conversation.UserId != userId)
|
||||
{
|
||||
return Result.Fail<object>(ResultCode.CONVERSATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
conversation.MarkAsRead(lastReadSequenceId: null);
|
||||
conversation.MarkAsRead(lastReadSequenceId);
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,6 @@ namespace MessageService.WebApi.Application.Dtos
|
||||
/// 最后一条最新消息
|
||||
/// </summary>
|
||||
public string LastMessage { get; set; }
|
||||
public DateTime DateTime { get; set; }
|
||||
public DateTimeOffset DateTime { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
using MassTransit;
|
||||
using MessageService.Domain.IReposities;
|
||||
using MessageService.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MySqlConnector;
|
||||
|
||||
namespace MessageService.WebApi.Application.EventHandlers
|
||||
{
|
||||
public class ConversationAddHandler : IConsumer<GroupMemberJoinedEvent>,
|
||||
IConsumer<FriendAddedEvent>
|
||||
IConsumer<FriendAddedEvent>, IConsumer<GroupMemberLeftEvent>
|
||||
{
|
||||
|
||||
private readonly IConversationReposity reposity;
|
||||
@@ -21,6 +23,11 @@ namespace MessageService.WebApi.Application.EventHandlers
|
||||
public async Task Consume(ConsumeContext<GroupMemberJoinedEvent> context)
|
||||
{
|
||||
var @event = context.Message;
|
||||
var existing = await reposity.FindActiveAsync(@event.UserId, @event.GroupId, Domain.Enums.ChatType.GROUP, context.CancellationToken);
|
||||
if (existing is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
reposity.Create(new Domain.Entities.Conversation(
|
||||
userId: @event.UserId,
|
||||
targetId: @event.GroupId,
|
||||
@@ -32,12 +39,17 @@ namespace MessageService.WebApi.Application.EventHandlers
|
||||
lastMessage: string.Empty
|
||||
));
|
||||
|
||||
await messageDb.SaveChangesAsync();
|
||||
await SaveIdempotentlyAsync(context.CancellationToken);
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<FriendAddedEvent> context)
|
||||
{
|
||||
var @event = context.Message;
|
||||
var existing = await reposity.FindActiveAsync(@event.OwnerId, @event.TargetId, Domain.Enums.ChatType.PRIVATE, context.CancellationToken);
|
||||
if (existing is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
reposity.Create(new Domain.Entities.Conversation(
|
||||
userId: @event.OwnerId,
|
||||
targetId: @event.TargetId,
|
||||
@@ -49,7 +61,33 @@ namespace MessageService.WebApi.Application.EventHandlers
|
||||
lastMessage: string.Empty
|
||||
));
|
||||
|
||||
await messageDb.SaveChangesAsync();
|
||||
await SaveIdempotentlyAsync(context.CancellationToken);
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<GroupMemberLeftEvent> context)
|
||||
{
|
||||
var conversation = await reposity.FindActiveAsync(
|
||||
context.Message.UserId,
|
||||
context.Message.GroupId,
|
||||
Domain.Enums.ChatType.GROUP,
|
||||
context.CancellationToken);
|
||||
if (conversation is not null)
|
||||
{
|
||||
conversation.SoftDelete();
|
||||
}
|
||||
await messageDb.SaveChangesAsync(context.CancellationToken);
|
||||
}
|
||||
|
||||
private async Task SaveIdempotentlyAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await messageDb.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
catch (DbUpdateException exception) when (exception.InnerException is MySqlException { Number: 1062 })
|
||||
{
|
||||
messageDb.ChangeTracker.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,20 +24,24 @@ namespace MessageService.WebApi.Application.EventHandlers
|
||||
{
|
||||
var message = notification.Message;
|
||||
|
||||
if(message.ChatType == Domain.Enums.ChatType.PRIVATE)
|
||||
var conversations = await reposity.FindByStreamKeyAsync(message.StreamKey, cancellationToken);
|
||||
foreach (var conversation in conversations)
|
||||
{
|
||||
var list = await reposity.FindByStreamKeyAsync(message.StreamKey);
|
||||
var owner = list.First(x => x.UserId == message.SenderId);
|
||||
var target = list.First(x => x.UserId == message.TargetId);
|
||||
|
||||
owner.Update(message.SequenceId, 0, message.Content.Fallback);
|
||||
target.Update(target.LastReadSequenceId, target.UnreadCount + 1, message.Content.Fallback);
|
||||
|
||||
messageDb.Conversations.UpdateRange(owner,target);
|
||||
await messageDb.SaveChangesAsync(cancellationToken);
|
||||
conversation.UpdateLastMessage(message.Content.Fallback, message.CreationTime);
|
||||
if (conversation.UserId == message.SenderId)
|
||||
{
|
||||
conversation.SetLastReadSequence(message.SequenceId);
|
||||
}
|
||||
else
|
||||
{
|
||||
conversation.IncrementUnread();
|
||||
}
|
||||
}
|
||||
|
||||
await endpoint.Publish(message.ToIntegrationEvent());
|
||||
messageDb.Conversations.UpdateRange(conversations);
|
||||
await messageDb.SaveChangesAsync(cancellationToken);
|
||||
|
||||
await endpoint.Publish(message.ToIntegrationEvent(), cancellationToken);
|
||||
}
|
||||
|
||||
public async Task Handle(MessageWithdrawDomainEvent notification, CancellationToken cancellationToken)
|
||||
|
||||
@@ -19,12 +19,12 @@ namespace MessageService.WebApi.Application.EventHandlers
|
||||
public async Task Consume(ConsumeContext<UserProfileUpdateEvent> context)
|
||||
{
|
||||
var @event = context.Message;
|
||||
var conversations = await reposity.FindByTargetIdAsync(@event.UserId);
|
||||
var conversations = await reposity.FindByTargetIdAsync(@event.UserId, context.CancellationToken);
|
||||
foreach (var conversation in conversations)
|
||||
{
|
||||
conversation.UpdateProfile(@event.NickName, @event.Avatar);
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
await db.SaveChangesAsync(context.CancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using AutoMapper;
|
||||
using AutoMapper;
|
||||
using IM.Commons;
|
||||
using MessageService.Domain.Enums;
|
||||
using MessageService.Domain.IReposities;
|
||||
@@ -16,20 +16,22 @@ namespace MessageService.WebApi.Application.Message
|
||||
private readonly IMapper mapper;
|
||||
private readonly IGroupMemberIntegrationService memberService;
|
||||
private readonly IContactIntegrationService contactService;
|
||||
private readonly SquenceService squenceService;
|
||||
private readonly SquenceService squenceService; private readonly IM.InitCommon.Management.RuntimePolicy runtime;
|
||||
|
||||
public MessageService(IMessageReposity reposity, IConversationReposity conversationReposity, IMapper mapper, IGroupMemberIntegrationService memberService, IContactIntegrationService contactService, SquenceService squenceService)
|
||||
public MessageService(IMessageReposity reposity, IConversationReposity conversationReposity, IMapper mapper, IGroupMemberIntegrationService memberService, IContactIntegrationService contactService, SquenceService squenceService, IM.InitCommon.Management.RuntimePolicy runtime)
|
||||
{
|
||||
this.reposity = reposity;
|
||||
this.conversationReposity = conversationReposity;
|
||||
this.mapper = mapper;
|
||||
this.memberService = memberService;
|
||||
this.contactService = contactService;
|
||||
this.squenceService = squenceService;
|
||||
this.squenceService = squenceService; this.runtime = runtime;
|
||||
}
|
||||
|
||||
public async Task<Result<MessageResponse>> SendMsgAsync(SendMsgCommand command)
|
||||
{
|
||||
if (runtime.Current.TextLimit > 0 && command.MsgType == MessageType.Text && command.Text?.Length > runtime.Current.TextLimit)
|
||||
return Result.Fail<MessageResponse>(ResultCode.PARAMETER_ERROR, "文本超过平台长度限制");
|
||||
if (command.ChatType == Domain.Enums.ChatType.PRIVATE)
|
||||
{
|
||||
bool passed = await contactService.CheckContactAsync(command.SenderId, command.TargetId);
|
||||
@@ -52,13 +54,16 @@ namespace MessageService.WebApi.Application.Message
|
||||
{
|
||||
MessageType.Text => Domain.Entities.Message.BuildTxt(ctx, command.Text!, sequenceId),
|
||||
|
||||
MessageType.Image => Domain.Entities.Message.BuildImg(ctx, command.Url!,
|
||||
command.Width ?? 0, command.Height ?? 0, command.Thumb!, sequenceId),
|
||||
MessageType.Image => Domain.Entities.Message.BuildImg(ctx, command.Url,
|
||||
command.Width ?? 0, command.Height ?? 0, command.Thumb!, sequenceId, command.FileId),
|
||||
|
||||
MessageType.Video => Domain.Entities.Message.BuildVideo(ctx, command.Url!,
|
||||
command.Width ?? 0, command.Height ?? 0, command.Thumb!, sequenceId),
|
||||
MessageType.Video => Domain.Entities.Message.BuildVideo(ctx, command.Url,
|
||||
command.Width ?? 0, command.Height ?? 0, command.Thumb!, sequenceId, command.FileId),
|
||||
|
||||
MessageType.Voice => Domain.Entities.Message.BuildVoice(ctx, command.Url!, command.Duration ?? 0, sequenceId),
|
||||
MessageType.Voice => Domain.Entities.Message.BuildVoice(ctx, command.Url, command.Duration ?? 0, sequenceId, command.FileId),
|
||||
|
||||
MessageType.File => Domain.Entities.Message.BuildFile(ctx, command.FileId!.Value, command.Url,
|
||||
command.FileName!, command.FileSize!.Value, command.FileFormat!, sequenceId),
|
||||
|
||||
_ => null
|
||||
};
|
||||
@@ -102,14 +107,21 @@ namespace MessageService.WebApi.Application.Message
|
||||
return Result.Fail<object>(ResultCode.MESSAGE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (runtime.Current.RecallMinutes > 0 && DateTimeOffset.UtcNow - msg.CreationTime > TimeSpan.FromMinutes(runtime.Current.RecallMinutes))
|
||||
return Result.Fail<object>(ResultCode.PERMISSION_DENIED, "已超过平台撤回时限");
|
||||
msg.Withdraw();
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
public async Task<Result<GetMessagesResponse>> GetMessagesAsync(GetMessageCommand command)
|
||||
public async Task<Result<GetMessagesResponse>> GetMessagesAsync(GetMessageCommand command, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var conversation = await conversationReposity.FindByIdAsync(command.conversationId);
|
||||
if (command.direction is not 0 and not 1 || command.limit is < 1 or > 100)
|
||||
{
|
||||
return Result.Fail<GetMessagesResponse>(ResultCode.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
var conversation = await conversationReposity.FindByIdAsync(command.conversationId, cancellationToken);
|
||||
|
||||
if(conversation is null || conversation.UserId != command.userId)
|
||||
{
|
||||
@@ -117,9 +129,37 @@ namespace MessageService.WebApi.Application.Message
|
||||
}
|
||||
|
||||
|
||||
var messages = await reposity.GetAsync(conversation.StreamKey, command.cusor, command.direction, command.limit);
|
||||
var messages = await reposity.GetAsync(conversation.StreamKey, command.cusor, command.direction, command.limit, cancellationToken);
|
||||
|
||||
return Result.Success(new GetMessagesResponse(mapper.Map<List<MessageResponse>>(messages.messages.ToList()),messages.hasMore));
|
||||
}
|
||||
|
||||
public async Task<Result<GetMessagesResponse>> SearchMessagesAsync(
|
||||
SearchMessageCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var keyword = command.Keyword?.Trim() ?? string.Empty;
|
||||
if (keyword.Length is < 1 or > 50 || command.Limit is < 1 or > 50)
|
||||
{
|
||||
return Result.Fail<GetMessagesResponse>(ResultCode.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
var conversation = await conversationReposity.FindByIdAsync(command.ConversationId, cancellationToken);
|
||||
if (conversation is null || conversation.UserId != command.UserId)
|
||||
{
|
||||
return Result.Fail<GetMessagesResponse>(ResultCode.PERMISSION_DENIED);
|
||||
}
|
||||
|
||||
var messages = await reposity.SearchAsync(
|
||||
conversation.StreamKey,
|
||||
keyword,
|
||||
command.Cursor,
|
||||
command.Limit,
|
||||
cancellationToken);
|
||||
|
||||
return Result.Success(new GetMessagesResponse(
|
||||
mapper.Map<List<MessageResponse>>(messages.messages.ToList()),
|
||||
messages.hasMore));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace MessageService.WebApi.Application.Message
|
||||
{
|
||||
public record SearchMessageCommand(
|
||||
Guid ConversationId,
|
||||
Guid UserId,
|
||||
string Keyword,
|
||||
long? Cursor,
|
||||
int Limit);
|
||||
}
|
||||
@@ -22,8 +22,12 @@ namespace MessageService.WebApi.Application.Message
|
||||
public int? Height { get; init; }
|
||||
public string? Thumb { get; init; }
|
||||
public int? Duration { get; init; }
|
||||
public Guid? FileId { get; init; }
|
||||
public string? FileName { get; init; }
|
||||
public long? FileSize { get; init; }
|
||||
public string? FileFormat { get; init; }
|
||||
|
||||
public SendMsgCommand(Guid senderId, Guid targetId, ChatType chatType, MessageType msgType, Guid clientMsgId, Guid? quoteMessageId, Dictionary<string, string>? ext, string? text, string? url, int? width, int? height, string? thumb, int? duration)
|
||||
public SendMsgCommand(Guid senderId, Guid targetId, ChatType chatType, MessageType msgType, Guid clientMsgId, Guid? quoteMessageId, Dictionary<string, string>? ext, string? text, string? url, int? width, int? height, string? thumb, int? duration, Guid? fileId, string? fileName, long? fileSize, string? fileFormat)
|
||||
{
|
||||
SenderId = senderId;
|
||||
TargetId = targetId;
|
||||
@@ -38,6 +42,10 @@ namespace MessageService.WebApi.Application.Message
|
||||
Height = height;
|
||||
Thumb = thumb;
|
||||
Duration = duration;
|
||||
FileId = fileId;
|
||||
FileName = fileName;
|
||||
FileSize = fileSize;
|
||||
FileFormat = fileFormat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,24 +20,24 @@ namespace MessageService.WebApi.Controllers.Conversation
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List()
|
||||
public async Task<IActionResult> List(CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
return Ok(await service.GetByOwnerIdAsync(Guid.Parse(userId)));
|
||||
return Ok(await service.GetByOwnerIdAsync(Guid.Parse(userId!), cancellationToken));
|
||||
}
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Get([FromQuery]Guid id)
|
||||
public async Task<IActionResult> Get([FromQuery]Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
return Ok(await service.GetByIdAsync(id, Guid.Parse(userId)));
|
||||
return Ok(await service.GetByIdAsync(id, Guid.Parse(userId!), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[UnitOfWork(typeof(MessageDbContext))]
|
||||
public async Task<IActionResult> MarkRead([FromQuery] Guid conversationId)
|
||||
public async Task<IActionResult> MarkRead([FromQuery] Guid conversationId, long? lastReadSequenceId, CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
return Ok(await service.MarkAsReadAsync(conversationId, Guid.Parse(userId)));
|
||||
return Ok(await service.MarkAsReadAsync(conversationId, Guid.Parse(userId!), lastReadSequenceId, cancellationToken));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Text.Json;
|
||||
using System.Security.Claims;
|
||||
using IM.InitCommon.Management;
|
||||
using IM.Commons;
|
||||
using MessageService.Infrastructure;
|
||||
using MessageService.Domain.Enums;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MessageService.WebApi.Controllers;
|
||||
[ApiController]
|
||||
public sealed class ManagementController(MessageDbContext db, InternalClient client) : ControllerBase
|
||||
{
|
||||
[HttpPost("api/message/report"), Authorize]
|
||||
public async Task<IActionResult> Report(ClientReport input, CancellationToken ct)
|
||||
{
|
||||
var reporter = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
JsonElement result;
|
||||
try { result = await client.Send<JsonElement>("admin", "/internal/management/reports", new { ReporterId = reporter, input.Type, input.TargetId, input.Reason, input.Description, input.MessageIds }, ct); }
|
||||
catch (InternalServiceException e) { return StatusCode(e.Status, ManagementResult.Fail(e.Status switch { 429 => "举报次数已达上限或仍在重复举报冷却期", 403 => "没有访问举报对象或消息的权限", 400 => "举报内容无效,请检查分类和关联消息", _ => "举报服务暂不可用,请重试" })); }
|
||||
return Ok(ManagementResult.Ok(result));
|
||||
}
|
||||
[HttpPost("internal/management/evidence")]
|
||||
public async Task<IActionResult> Evidence(EvidenceRequest input, CancellationToken ct)
|
||||
{
|
||||
if (input.Type is not "user" and not "group" || input.MessageIds.Length > 20 || input.TargetId == input.ReporterId) return BadRequest();
|
||||
string name;
|
||||
if (input.Type == "group") {
|
||||
var access = await client.Send<JsonElement>("group", $"/internal/management/access/{input.TargetId}/{input.ReporterId}", ct: ct);
|
||||
if (!access.GetProperty("member").GetBoolean()) return Forbid();
|
||||
var group = await client.Send<JsonElement>("group", $"/internal/management/detail/{input.TargetId}", ct: ct); name = group.GetProperty("name").GetString()!;
|
||||
} else {
|
||||
var relation = await client.Send<JsonElement>("contact", $"/internal/management/relation/{input.ReporterId}/{input.TargetId}", ct: ct);
|
||||
if (!relation.GetProperty("related").GetBoolean() && input.MessageIds.Length == 0) return Forbid();
|
||||
var user = await client.Send<JsonElement>("user", $"/internal/management/list?q={input.TargetId}&size=1", ct: ct);
|
||||
if (user.GetProperty("items").GetArrayLength() == 0) return NotFound(); name = user.GetProperty("items")[0].GetProperty("name").GetString()!;
|
||||
}
|
||||
var ids = input.MessageIds.Distinct().ToArray(); var messages = await db.Messages.AsNoTracking().Where(x => ids.Contains(x.Id)).ToListAsync(ct);
|
||||
if (messages.Count != ids.Length) return BadRequest();
|
||||
var evidence = new List<EvidenceSnapshot>();
|
||||
foreach (var m in messages) {
|
||||
var ownConversation = await db.Conversations.AnyAsync(x => x.UserId == input.ReporterId && x.StreamKey == m.StreamKey, ct);
|
||||
if (!ownConversation || (input.Type == "user" && m.SenderId != input.TargetId) || (input.Type == "group" && (m.ChatType != ChatType.GROUP || m.TargetId != input.TargetId))) return Forbid();
|
||||
if (m.ChatType == ChatType.GROUP) { var access = await client.Send<JsonElement>("group", $"/internal/management/access/{m.TargetId}/{input.ReporterId}", ct: ct); if (!access.GetProperty("member").GetBoolean()) return Forbid(); }
|
||||
using var body = JsonDocument.Parse(m.Content.RawBody ?? "{}"); Guid? fileId = null;
|
||||
if ((body.RootElement.TryGetProperty("FileId", out var file) || body.RootElement.TryGetProperty("fileId", out file)) && file.ValueKind == JsonValueKind.String && file.TryGetGuid(out var fid)) fileId = fid;
|
||||
evidence.Add(new(m.Id, m.SenderId, m.SenderId.ToString(), m.Content.Fallback, m.MsgType.ToString(), fileId, m.CreationTime));
|
||||
}
|
||||
return Ok(new SubjectEvidence(name, evidence));
|
||||
}
|
||||
}
|
||||
public record ClientReport(string Type, Guid TargetId, string Reason, string Description, Guid[] MessageIds);
|
||||
@@ -37,11 +37,24 @@ namespace MessageService.WebApi.Controllers.Message
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetMessages([FromQuery]Guid conversationId, long? cursor, int direction, int limit)
|
||||
public async Task<IActionResult> GetMessages([FromQuery]Guid conversationId, long? cursor, int direction, int limit, CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var command = new GetMessageCommand(conversationId, Guid.Parse(userId), cursor, direction, limit);
|
||||
return Ok(await service.GetMessagesAsync(command));
|
||||
return Ok(await service.GetMessagesAsync(command, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Search(
|
||||
[FromQuery] Guid conversationId,
|
||||
[FromQuery] string keyword,
|
||||
long? cursor,
|
||||
int limit = 30,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var command = new SearchMessageCommand(conversationId, Guid.Parse(userId!), keyword, cursor, limit);
|
||||
return Ok(await service.SearchMessagesAsync(command, cancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ namespace MessageService.WebApi.Controllers.Message
|
||||
public int? Height { get; init; }
|
||||
public string? Thumb { get; init; }
|
||||
public int? Duration { get; init; }
|
||||
public Guid? FileId { get; init; }
|
||||
public string? FileName { get; init; }
|
||||
public long? FileSize { get; init; }
|
||||
public string? FileFormat { get; init; }
|
||||
|
||||
public SendMsgCommand ToCommand(Guid senderId)
|
||||
{
|
||||
@@ -39,7 +43,11 @@ namespace MessageService.WebApi.Controllers.Message
|
||||
Width,
|
||||
Height,
|
||||
Thumb,
|
||||
Duration
|
||||
Duration,
|
||||
FileId,
|
||||
FileName,
|
||||
FileSize,
|
||||
FileFormat
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -63,9 +71,19 @@ namespace MessageService.WebApi.Controllers.Message
|
||||
|
||||
// 图片/视频/语音消息:url 必填
|
||||
When(r => r.MsgType == MessageType.Image || r.MsgType == MessageType.Video
|
||||
|| r.MsgType == MessageType.Voice || r.MsgType == MessageType.File, () =>
|
||||
|| r.MsgType == MessageType.Voice, () =>
|
||||
{
|
||||
RuleFor(r => r.Url).NotEmpty().WithMessage("媒体消息的 url 字段不能为空");
|
||||
RuleFor(r => r)
|
||||
.Must(request => !string.IsNullOrWhiteSpace(request.Url) || request.FileId.HasValue)
|
||||
.WithMessage("媒体消息必须提供 url 或 fileId");
|
||||
});
|
||||
|
||||
When(r => r.MsgType == MessageType.File, () =>
|
||||
{
|
||||
RuleFor(r => r.FileId).NotNull().NotEmpty().WithMessage("文件消息的 fileId 字段不能为空");
|
||||
RuleFor(r => r.FileName).NotEmpty().WithMessage("文件消息的 fileName 字段不能为空");
|
||||
RuleFor(r => r.FileSize).NotNull().GreaterThanOrEqualTo(0).WithMessage("文件消息的 fileSize 字段不合法");
|
||||
RuleFor(r => r.FileFormat).NotEmpty().WithMessage("文件消息的 fileFormat 字段不能为空");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,16 @@ namespace MessageService.WebApi
|
||||
services.AddScoped<ConversationService>();
|
||||
services.AddScoped<SquenceService>();
|
||||
services.AddScoped<IContactIntegrationService, ContactIntegrationService>();
|
||||
services.AddHttpClient<IGroupMemberIntegrationService, GroupMemberIntegrationService>(c =>
|
||||
services.AddHttpClient<IGroupMemberIntegrationService, GroupMemberIntegrationService>((sp, c) =>
|
||||
{
|
||||
c.BaseAddress = new Uri("http://im-group-service:8080/");
|
||||
var configuration = sp.GetRequiredService<IConfiguration>();
|
||||
c.BaseAddress = new Uri(configuration["InternalServices:GroupServiceBaseUrl"]
|
||||
?? "http://im-group-service:8080/");
|
||||
var internalApiKey = configuration["InternalApiKey"];
|
||||
if (!string.IsNullOrWhiteSpace(internalApiKey))
|
||||
{
|
||||
c.DefaultRequestHeaders.Add("X-Internal-Api-Key", internalApiKey);
|
||||
}
|
||||
});
|
||||
services.AddGrpcClient<ContactInternal.ContactInternalClient>((sp, o) =>
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using IM.InitCommon.Management;
|
||||
|
||||
using IM.InitCommon;
|
||||
|
||||
@@ -19,6 +20,7 @@ namespace MessageService.WebApi
|
||||
builder.Services.AddAllGrpcServer();
|
||||
|
||||
var app = builder.Build();
|
||||
if (app.ApplyMigrationsIfRequested(args)) return;
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
@@ -28,6 +30,7 @@ namespace MessageService.WebApi
|
||||
}
|
||||
|
||||
app.UseAppDefault();
|
||||
app.MapManagementHealth();
|
||||
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"InternalApiKey": "development-only-change-me",
|
||||
"InternalServices": {
|
||||
"GroupServiceBaseUrl": "http://localhost:5070/"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
||||
@@ -5,5 +5,9 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"InternalApiKey": "",
|
||||
"InternalServices": {
|
||||
"GroupServiceBaseUrl": "http://im-group-service:8080/"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user