Правки
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Relations.Application.Abstractions;
|
||||
using Knot.Modules.Relations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Knot.Modules.Relations.Application.Contacts;
|
||||
|
||||
public record DeclineContactRequestCommand(Guid UserId, Guid RequestId) : ICommand<bool>;
|
||||
|
||||
internal sealed class DeclineContactRequestCommandHandler : ICommandHandler<DeclineContactRequestCommand, bool>
|
||||
{
|
||||
private readonly IContactsDbContext _context;
|
||||
|
||||
public DeclineContactRequestCommandHandler(IContactsDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Result<bool>> Handle(DeclineContactRequestCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == request.RequestId, cancellationToken);
|
||||
if (contact == null || contact.ContactId != request.UserId)
|
||||
{
|
||||
return Result.Failure<bool>(new Error("Contacts.NotFound", "Contact request not found."));
|
||||
}
|
||||
|
||||
contact.Decline();
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Success(true);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
using Carter;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Infrastructure;
|
||||
using Knot.Contracts.Relations.Domain;
|
||||
using Knot.Modules.Relations.Application.Contacts;
|
||||
using Knot.Shared.Infrastructure;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Knot.Modules.Relations.Presentation.Endpoints;
|
||||
|
||||
@@ -22,6 +24,51 @@ public sealed class ContactsEndpoints : ICarterModule
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error);
|
||||
});
|
||||
|
||||
group.MapGet("requests", async (ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new GetIncomingRequestsQuery(userContext.UserId), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error);
|
||||
});
|
||||
|
||||
group.MapGet("status/{userId:guid}", async (Guid userId, ISender sender, IUserContext userContext, IContactsDbContext context, CancellationToken ct) =>
|
||||
{
|
||||
var contact = await context.Contacts
|
||||
.FirstOrDefaultAsync(c =>
|
||||
(c.UserId == userContext.UserId && c.ContactId == userId) ||
|
||||
(c.UserId == userId && c.ContactId == userContext.UserId), ct);
|
||||
|
||||
if (contact == null)
|
||||
{
|
||||
return Results.Ok(new { status = "none", friendshipId = (string?)null });
|
||||
}
|
||||
|
||||
string status;
|
||||
string? friendshipId = contact.Id.ToString();
|
||||
|
||||
if (contact.UserId == userContext.UserId && contact.Status == ContactStatus.Pending)
|
||||
{
|
||||
status = "outgoing";
|
||||
}
|
||||
else if (contact.Status == ContactStatus.Pending)
|
||||
{
|
||||
status = "pending";
|
||||
}
|
||||
else if (contact.Status == ContactStatus.Accepted)
|
||||
{
|
||||
status = "accepted";
|
||||
}
|
||||
else if (contact.Status == ContactStatus.Declined)
|
||||
{
|
||||
status = "declined";
|
||||
}
|
||||
else
|
||||
{
|
||||
status = "none";
|
||||
}
|
||||
|
||||
return Results.Ok(new { status, friendshipId });
|
||||
});
|
||||
|
||||
group.MapPost("request", async ([FromBody] SendContactRequest request, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new SendContactRequestCommand(userContext.UserId, request.ContactId), ct);
|
||||
@@ -34,6 +81,12 @@ public sealed class ContactsEndpoints : ICarterModule
|
||||
return result.IsSuccess ? Results.Ok(new { id = result.Value }) : Results.NotFound(result.Error.Description);
|
||||
});
|
||||
|
||||
group.MapPost("{id:guid}/decline", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new DeclineContactRequestCommand(userContext.UserId, id), ct);
|
||||
return result.IsSuccess ? Results.Ok(new { success = true }) : Results.NotFound(result.Error.Description);
|
||||
});
|
||||
|
||||
group.MapDelete("{id:guid}", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new RemoveContactCommand(userContext.UserId, id), ct);
|
||||
|
||||
@@ -1 +1,33 @@
|
||||
// file removed
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Stories.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Stories.Application.Stories.Commands.AddStoryReaction;
|
||||
|
||||
public record AddStoryReactionCommand(Guid UserId, Guid StoryId, string Emoji) : ICommand<bool>;
|
||||
|
||||
internal sealed class AddStoryReactionCommandHandler : ICommandHandler<AddStoryReactionCommand, bool>
|
||||
{
|
||||
private readonly IStoryRepository _storyRepository;
|
||||
|
||||
public AddStoryReactionCommandHandler(IStoryRepository storyRepository)
|
||||
{
|
||||
_storyRepository = storyRepository;
|
||||
}
|
||||
|
||||
public async Task<Result<bool>> Handle(AddStoryReactionCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var story = await _storyRepository.GetByIdAsync(request.StoryId, cancellationToken);
|
||||
if (story == null)
|
||||
{
|
||||
return Result.Failure<bool>(new Error("Stories.NotFound", "Story not found."));
|
||||
}
|
||||
|
||||
story.AddReaction(request.UserId, request.Emoji);
|
||||
await _storyRepository.UpdateAsync(story, cancellationToken);
|
||||
|
||||
return Result.Success(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,33 @@
|
||||
// file removed
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Stories.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Stories.Application.Stories.Commands.AddStoryReply;
|
||||
|
||||
public record AddStoryReplyCommand(Guid UserId, Guid StoryId, string Content) : ICommand<bool>;
|
||||
|
||||
internal sealed class AddStoryReplyCommandHandler : ICommandHandler<AddStoryReplyCommand, bool>
|
||||
{
|
||||
private readonly IStoryRepository _storyRepository;
|
||||
|
||||
public AddStoryReplyCommandHandler(IStoryRepository storyRepository)
|
||||
{
|
||||
_storyRepository = storyRepository;
|
||||
}
|
||||
|
||||
public async Task<Result<bool>> Handle(AddStoryReplyCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var story = await _storyRepository.GetByIdAsync(request.StoryId, cancellationToken);
|
||||
if (story == null)
|
||||
{
|
||||
return Result.Failure<bool>(new Error("Stories.NotFound", "Story not found."));
|
||||
}
|
||||
|
||||
story.AddReply(request.UserId, request.Content);
|
||||
await _storyRepository.UpdateAsync(story, cancellationToken);
|
||||
|
||||
return Result.Success(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,33 @@
|
||||
// file removed
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Stories.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Stories.Application.Stories.Commands.RemoveStoryReaction;
|
||||
|
||||
public record RemoveStoryReactionCommand(Guid UserId, Guid StoryId, string Emoji) : ICommand<bool>;
|
||||
|
||||
internal sealed class RemoveStoryReactionCommandHandler : ICommandHandler<RemoveStoryReactionCommand, bool>
|
||||
{
|
||||
private readonly IStoryRepository _storyRepository;
|
||||
|
||||
public RemoveStoryReactionCommandHandler(IStoryRepository storyRepository)
|
||||
{
|
||||
_storyRepository = storyRepository;
|
||||
}
|
||||
|
||||
public async Task<Result<bool>> Handle(RemoveStoryReactionCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var story = await _storyRepository.GetByIdAsync(request.StoryId, cancellationToken);
|
||||
if (story == null)
|
||||
{
|
||||
return Result.Failure<bool>(new Error("Stories.NotFound", "Story not found."));
|
||||
}
|
||||
|
||||
story.RemoveReaction(request.UserId, request.Emoji);
|
||||
await _storyRepository.UpdateAsync(story, cancellationToken);
|
||||
|
||||
return Result.Success(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Knot.Modules.Stories.Application.Stories.DTOs;
|
||||
|
||||
public record StoryReplyDto(
|
||||
Guid Id,
|
||||
Guid UserId,
|
||||
string Username,
|
||||
string DisplayName,
|
||||
string? Avatar,
|
||||
string Content,
|
||||
DateTime CreatedAt
|
||||
);
|
||||
@@ -1 +1,42 @@
|
||||
// file removed
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Stories.Application.Abstractions;
|
||||
using Knot.Modules.Stories.Application.Stories.DTOs;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Stories.Application.Stories.Queries.GetStoryReplies;
|
||||
|
||||
public record GetStoryRepliesQuery(Guid UserId, Guid StoryId) : IQuery<List<StoryReplyDto>>;
|
||||
|
||||
internal sealed class GetStoryRepliesQueryHandler : IQueryHandler<GetStoryRepliesQuery, List<StoryReplyDto>>
|
||||
{
|
||||
private readonly IStoryRepository _storyRepository;
|
||||
|
||||
public GetStoryRepliesQueryHandler(IStoryRepository storyRepository)
|
||||
{
|
||||
_storyRepository = storyRepository;
|
||||
}
|
||||
|
||||
public async Task<Result<List<StoryReplyDto>>> Handle(GetStoryRepliesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var story = await _storyRepository.GetByIdAsync(request.StoryId, cancellationToken);
|
||||
if (story == null)
|
||||
{
|
||||
return Result.Failure<List<StoryReplyDto>>(new Error("Stories.NotFound", "Story not found."));
|
||||
}
|
||||
|
||||
var replies = story.Replies.ConvertAll(r => new StoryReplyDto(
|
||||
r.Id,
|
||||
r.UserId,
|
||||
r.Username,
|
||||
r.DisplayName,
|
||||
r.Avatar,
|
||||
r.Content,
|
||||
r.CreatedAt
|
||||
));
|
||||
|
||||
return Result.Success(replies);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Stories.Domain;
|
||||
|
||||
public class StoryReaction
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public string Emoji { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class StoryReply
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string? Avatar { get; set; }
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class Story : Entity<Guid>
|
||||
{
|
||||
public Guid UserId { get; private set; }
|
||||
@@ -11,6 +29,8 @@ public class Story : Entity<Guid>
|
||||
public string? BgColor { get; private set; }
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public int ViewsCount { get; private set; }
|
||||
public List<StoryReaction> Reactions { get; private set; } = new();
|
||||
public List<StoryReply> Replies { get; private set; } = new();
|
||||
|
||||
protected Story() : base(Guid.NewGuid()) { }
|
||||
|
||||
@@ -33,4 +53,33 @@ public class Story : Entity<Guid>
|
||||
{
|
||||
ViewsCount++;
|
||||
}
|
||||
|
||||
public void AddReaction(Guid userId, string emoji)
|
||||
{
|
||||
var existing = Reactions.FirstOrDefault(r => r.UserId == userId && r.Emoji == emoji);
|
||||
if (existing == null)
|
||||
{
|
||||
Reactions.Add(new StoryReaction { UserId = userId, Emoji = emoji });
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveReaction(Guid userId, string emoji)
|
||||
{
|
||||
var reaction = Reactions.FirstOrDefault(r => r.UserId == userId && r.Emoji == emoji);
|
||||
if (reaction != null)
|
||||
{
|
||||
Reactions.Remove(reaction);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddReply(Guid userId, string content)
|
||||
{
|
||||
Replies.Add(new StoryReply
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Content = content,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
using Carter;
|
||||
using Knot.Modules.Stories.Application.DTOs;
|
||||
using Knot.Modules.Stories.Application.Stories.Commands.AddStoryReaction;
|
||||
using Knot.Modules.Stories.Application.Stories.Commands.AddStoryReply;
|
||||
using Knot.Modules.Stories.Application.Stories.Commands.CreateStory;
|
||||
using Knot.Modules.Stories.Application.Stories.Commands.DeleteStory;
|
||||
using Knot.Modules.Stories.Application.Stories.Commands.RemoveStoryReaction;
|
||||
using Knot.Modules.Stories.Application.Stories.Commands.ViewStory;
|
||||
using Knot.Modules.Stories.Application.Stories.Queries.GetStories;
|
||||
using Knot.Modules.Stories.Application.Stories.Queries.GetStoryReplies;
|
||||
using Knot.Modules.Stories.Application.Stories.Queries.GetStoryViewers;
|
||||
using Knot.Modules.Stories.Application.Stories.Queries.GetUserStories;
|
||||
using Knot.Modules.Stories.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Knot.Modules.Stories.Application.Stories.Queries.GetStories;
|
||||
using Knot.Modules.Stories.Application.Stories.Commands.CreateStory;
|
||||
using Knot.Modules.Stories.Application.Stories.Queries.GetUserStories;
|
||||
using Knot.Modules.Stories.Application.Stories.Commands.ViewStory;
|
||||
using Knot.Modules.Stories.Application.Stories.Queries.GetStoryViewers;
|
||||
using Knot.Modules.Stories.Application.Stories.Commands.DeleteStory;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Knot.Modules.Stories.Presentation.Endpoints;
|
||||
|
||||
@@ -33,6 +39,18 @@ public sealed class StoriesEndpoints : ICarterModule
|
||||
return Results.Ok(new { id = result.Value });
|
||||
});
|
||||
|
||||
group.MapPost("video", async (HttpRequest req, ISender sender, IUserContext userContext, IFileStorageService fileStorage, CancellationToken ct) =>
|
||||
{
|
||||
if (!req.HasFormContentType) return Results.BadRequest("No file uploaded");
|
||||
var form = await req.ReadFormAsync(ct);
|
||||
var file = form.Files.FirstOrDefault();
|
||||
if (file == null || file.Length == 0) return Results.BadRequest("No file uploaded");
|
||||
|
||||
using var stream = file.OpenReadStream();
|
||||
var fileId = await fileStorage.UploadFileAsync(stream, file.FileName, file.ContentType);
|
||||
return Results.Ok(new { url = $"/api/files/{fileId}" });
|
||||
}).DisableAntiforgery();
|
||||
|
||||
group.MapGet("user/{userId:guid}", async (Guid userId, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new GetUserStoriesQuery(userContext.UserId, userId), ct);
|
||||
@@ -58,6 +76,30 @@ public sealed class StoriesEndpoints : ICarterModule
|
||||
if (result.IsFailure) return result.Error.Code == "Unauthorized" ? Results.Forbid() : Results.NotFound();
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapPost("{id:guid}/reaction", async (Guid id, [FromBody] AddStoryReactionRequest request, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new AddStoryReactionCommand(userContext.UserId, id, request.Emoji), ct);
|
||||
return result.IsSuccess ? Results.Ok(new { message = "Reaction added" }) : Results.NotFound();
|
||||
});
|
||||
|
||||
group.MapDelete("{id:guid}/reaction", async (Guid id, [FromBody] RemoveStoryReactionRequest request, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new RemoveStoryReactionCommand(userContext.UserId, id, request.Emoji), ct);
|
||||
return result.IsSuccess ? Results.Ok(new { message = "Reaction removed" }) : Results.NotFound();
|
||||
});
|
||||
|
||||
group.MapPost("{id:guid}/reply", async (Guid id, [FromBody] AddStoryReplyRequest request, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new AddStoryReplyCommand(userContext.UserId, id, request.Content), ct);
|
||||
return result.IsSuccess ? Results.Ok(new { message = "Reply added" }) : Results.NotFound();
|
||||
});
|
||||
|
||||
group.MapGet("{id:guid}/replies", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new GetStoryRepliesQuery(userContext.UserId, id), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,37 +3,33 @@ import type { FriendshipStatus, FriendRequest, FriendWithId } from '../../../cor
|
||||
|
||||
export class FriendApi {
|
||||
static async getFriends() {
|
||||
return httpClient.request<FriendWithId[]>('/friends');
|
||||
return httpClient.request<FriendWithId[]>('/contacts');
|
||||
}
|
||||
|
||||
static async getFriendRequests() {
|
||||
return httpClient.request<FriendRequest[]>('/friends/requests');
|
||||
}
|
||||
|
||||
static async getOutgoingRequests() {
|
||||
return httpClient.request<FriendRequest[]>('/friends/outgoing');
|
||||
return httpClient.request<FriendRequest[]>('/contacts/requests');
|
||||
}
|
||||
|
||||
static async getFriendshipStatus(userId: string) {
|
||||
return httpClient.request<FriendshipStatus>(`/friends/status/${userId}`);
|
||||
return httpClient.request<FriendshipStatus>(`/contacts/status/${userId}`);
|
||||
}
|
||||
|
||||
static async sendFriendRequest(friendId: string) {
|
||||
return httpClient.request<{ status: string }>('/friends/request', {
|
||||
return httpClient.request<{ status: string }>('/contacts/request', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ friendId }),
|
||||
body: JSON.stringify({ contactId: friendId }),
|
||||
});
|
||||
}
|
||||
|
||||
static async acceptFriendRequest(friendshipId: string) {
|
||||
return httpClient.request<{ id: string }>(`/friends/${friendshipId}/accept`, { method: 'POST' });
|
||||
return httpClient.request<{ id: string }>(`/contacts/${friendshipId}/accept`, { method: 'POST' });
|
||||
}
|
||||
|
||||
static async declineFriendRequest(friendshipId: string) {
|
||||
return httpClient.request<{ success: boolean }>(`/friends/${friendshipId}/decline`, { method: 'POST' });
|
||||
return httpClient.request<{ success: boolean }>(`/contacts/${friendshipId}/decline`, { method: 'POST' });
|
||||
}
|
||||
|
||||
static async removeFriend(friendshipId: string) {
|
||||
return httpClient.request<{ success: boolean }>(`/friends/${friendshipId}`, { method: 'DELETE' });
|
||||
return httpClient.request<{ success: boolean }>(`/contacts/${friendshipId}`, { method: 'DELETE' });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user