Add Ping backend
.NET 10 solution for Ping, a Discord style chat app: Domain, Application and Infrastructure projects plus the Api host. SignalR for chat and presence, LiveKit for call tokens, EF Core on Postgres, S3 compatible storage for attachments, and OIDC auth shared with the Musify Zitadel instance. Includes the Compose stack for local development.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Ping.Application.Channels.Responses;
|
||||
using Ping.Application.Contracts;
|
||||
using Ping.Domain.Entities;
|
||||
using Ping.Domain.ValueObjects;
|
||||
|
||||
namespace Ping.Application.Channels
|
||||
{
|
||||
public record CreateChannelCommand(long CurrentUserId, Guid ServerId, string Name, ChannelType Type)
|
||||
: ICommand<ErrorOr<ChannelResponse>>;
|
||||
|
||||
public class CreateChannelCommandHandler(IDatabase database)
|
||||
: ICommandHandler<CreateChannelCommand, ErrorOr<ChannelResponse>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<ChannelResponse>> Handle(CreateChannelCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var membership = await database.ServerMembers.FirstOrDefaultAsync(
|
||||
m => m.ServerId == request.ServerId && m.UserId == request.CurrentUserId, cancellationToken);
|
||||
|
||||
if (membership is null)
|
||||
return Error.NotFound(description: $"Server {request.ServerId} not found");
|
||||
|
||||
if (membership.Role != ServerMemberRole.Owner)
|
||||
return Error.Forbidden(description: "Only the server owner can create channels");
|
||||
|
||||
if (request.Type == ChannelType.DirectMessage)
|
||||
return Error.Validation(description: "Server channels cannot be direct messages");
|
||||
|
||||
var position = await database.Channels
|
||||
.Where(c => c.ServerId == request.ServerId)
|
||||
.CountAsync(cancellationToken);
|
||||
|
||||
var channel = new Channel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ServerId = request.ServerId,
|
||||
Name = request.Name,
|
||||
Type = request.Type,
|
||||
Position = position,
|
||||
};
|
||||
await database.Channels.AddAsync(channel, cancellationToken);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return ChannelResponse.FromEntity(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Ping.Application.Channels.Responses;
|
||||
using Ping.Application.Contracts;
|
||||
using Ping.Domain.Entities;
|
||||
using Ping.Domain.ValueObjects;
|
||||
|
||||
namespace Ping.Application.Channels
|
||||
{
|
||||
public record GetOrCreateDirectMessageChannelCommand(long CurrentUserId, long OtherUserId)
|
||||
: ICommand<ErrorOr<ChannelResponse>>;
|
||||
|
||||
public class GetOrCreateDirectMessageChannelCommandHandler(IDatabase database)
|
||||
: ICommandHandler<GetOrCreateDirectMessageChannelCommand, ErrorOr<ChannelResponse>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<ChannelResponse>> Handle(
|
||||
GetOrCreateDirectMessageChannelCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.CurrentUserId == request.OtherUserId)
|
||||
return Error.Validation(description: "You cannot open a direct message channel with yourself");
|
||||
|
||||
var areFriends = await database.Friendships.AnyAsync(f =>
|
||||
f.Status == FriendshipStatus.Accepted &&
|
||||
((f.RequesterId == request.CurrentUserId && f.AddresseeId == request.OtherUserId) ||
|
||||
(f.RequesterId == request.OtherUserId && f.AddresseeId == request.CurrentUserId)),
|
||||
cancellationToken);
|
||||
|
||||
if (!areFriends)
|
||||
return Error.Forbidden(description: "You can only message users on your friends list");
|
||||
|
||||
var existingChannelId = await database.ChannelMembers
|
||||
.Where(m => m.UserId == request.CurrentUserId)
|
||||
.Select(m => m.ChannelId)
|
||||
.Intersect(database.ChannelMembers
|
||||
.Where(m => m.UserId == request.OtherUserId)
|
||||
.Select(m => m.ChannelId))
|
||||
.Where(channelId => database.Channels.Any(c => c.Id == channelId && c.Type == ChannelType.DirectMessage))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (existingChannelId != Guid.Empty)
|
||||
{
|
||||
var existingChannel = await database.Channels.FirstAsync(c => c.Id == existingChannelId, cancellationToken);
|
||||
return ChannelResponse.FromEntity(existingChannel);
|
||||
}
|
||||
|
||||
var channel = new Channel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = ChannelType.DirectMessage,
|
||||
Position = 0,
|
||||
};
|
||||
await database.Channels.AddAsync(channel, cancellationToken);
|
||||
await database.ChannelMembers.AddAsync(new ChannelMember { ChannelId = channel.Id, UserId = request.CurrentUserId }, cancellationToken);
|
||||
await database.ChannelMembers.AddAsync(new ChannelMember { ChannelId = channel.Id, UserId = request.OtherUserId }, cancellationToken);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return ChannelResponse.FromEntity(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Ping.Application.Channels.Responses;
|
||||
using Ping.Application.Contracts;
|
||||
|
||||
namespace Ping.Application.Channels
|
||||
{
|
||||
public record GetServerChannelsQuery(long CurrentUserId, Guid ServerId) : IQuery<ErrorOr<List<ChannelResponse>>>;
|
||||
|
||||
public class GetServerChannelsQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetServerChannelsQuery, ErrorOr<List<ChannelResponse>>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<List<ChannelResponse>>> Handle(GetServerChannelsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var isMember = await database.ServerMembers.AnyAsync(
|
||||
m => m.ServerId == request.ServerId && m.UserId == request.CurrentUserId, cancellationToken);
|
||||
|
||||
if (!isMember)
|
||||
return Error.NotFound(description: $"Server {request.ServerId} not found");
|
||||
|
||||
var channels = await database.Channels.AsNoTracking()
|
||||
.Where(c => c.ServerId == request.ServerId)
|
||||
.OrderBy(c => c.Position)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return channels.Select(ChannelResponse.FromEntity).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Ping.Domain.Entities;
|
||||
using Ping.Domain.ValueObjects;
|
||||
|
||||
namespace Ping.Application.Channels.Responses
|
||||
{
|
||||
public record ChannelResponse(Guid Id, Guid? ServerId, string? Name, ChannelType Type, int Position)
|
||||
{
|
||||
public static ChannelResponse FromEntity(Channel channel) =>
|
||||
new(channel.Id, channel.ServerId, channel.Name, channel.Type, channel.Position);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user