-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(back): types messages yarn lint!
- Loading branch information
Showing
20 changed files
with
386 additions
and
238 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
back/src/Liane/Liane.Api/Community/ILianeMessageService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
using System; | ||
using System.Collections.Immutable; | ||
using System.Threading.Tasks; | ||
using Liane.Api.Util.Pagination; | ||
using Liane.Api.Util.Ref; | ||
|
||
namespace Liane.Api.Community; | ||
|
||
public interface ILianeMessageService | ||
{ | ||
Task<PaginatedResponse<LianeMessage>> GetMessages(Ref<Liane> liane) => GetMessages(liane, Pagination.Empty); | ||
Task<PaginatedResponse<LianeMessage>> GetMessages(Ref<Liane> liane, Pagination pagination); | ||
Task<LianeMessage> SendMessage(Ref<Liane> liane, MessageContent content); | ||
Task MarkAsRead(Ref<Liane> liane, DateTime timestamp); | ||
Task<ImmutableDictionary<Ref<Liane>, int>> GetUnreadLianes(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
206 changes: 206 additions & 0 deletions
206
back/src/Liane/Liane.Service/Internal/Community/LianeMessageServiceImpl.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
using System; | ||
using System.Collections.Immutable; | ||
using System.Data; | ||
using System.Globalization; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Dapper; | ||
using Liane.Api.Auth; | ||
using Liane.Api.Community; | ||
using Liane.Api.Util.Pagination; | ||
using Liane.Api.Util.Ref; | ||
using Liane.Service.Internal.Event; | ||
using Liane.Service.Internal.Postgis.Db; | ||
using Liane.Service.Internal.Util; | ||
using Liane.Service.Internal.Util.Sql; | ||
using Microsoft.IdentityModel.Tokens; | ||
using UuidExtensions; | ||
|
||
namespace Liane.Service.Internal.Community; | ||
|
||
public sealed class LianeMessageServiceImpl( | ||
PostgisDatabase db, | ||
ICurrentContext currentContext, | ||
LianeFetcher lianeFetcher, | ||
IPushService pushService, | ||
IUserService userService | ||
) : ILianeMessageService | ||
{ | ||
private static readonly TimeZoneInfo TimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Paris"); | ||
private static readonly CultureInfo Culture = new("fr-FR"); | ||
|
||
public async Task<LianeMessage> SendMessage(Ref<Api.Community.Liane> liane, MessageContent content) | ||
{ | ||
using var connection = db.NewConnection(); | ||
using var tx = connection.BeginTransaction(); | ||
var userId = currentContext.CurrentUser().Id; | ||
var lianeId = Guid.Parse(liane.Id); | ||
var resolvedLiane = await lianeFetcher.FetchLiane(connection, lianeId, tx); | ||
if (!resolvedLiane.IsMember(userId)) | ||
{ | ||
throw new UnauthorizedAccessException("User is not part of the liane"); | ||
} | ||
|
||
var id = Uuid7.Guid(); | ||
var now = DateTime.UtcNow; | ||
|
||
content = content with { Value = await FormatMessage(userId, content) }; | ||
|
||
await connection.MergeAsync(new LianeMessageDb(id, lianeId, content, userId, now), tx); | ||
var lianeMessage = new LianeMessage(id.ToString(), userId, now, content); | ||
|
||
await pushService.PushMessage(lianeId, lianeMessage); | ||
tx.Commit(); | ||
return lianeMessage; | ||
} | ||
|
||
private async Task<string> FormatMessage(Ref<Api.Auth.User> sender, MessageContent content) | ||
{ | ||
var value = content.Value.Trim(); | ||
if (!value.IsNullOrEmpty()) | ||
{ | ||
return value; | ||
} | ||
|
||
var sentBy = await FormatUser(await sender.Resolve(userService.Get)); | ||
return content switch | ||
{ | ||
MessageContent.LianeRequestModified => $"{sentBy} a modifié son annonce.", | ||
MessageContent.TripAdded m => $"{sentBy} lance un covoit pour le {FormatDate(m.Trip.Value?.DepartureTime)}", | ||
MessageContent.TripRemoved m => $"{sentBy} a supprimé son covoit pour le {FormatDate(m.Trip.Value?.DepartureTime)}", | ||
MessageContent.MemberRequested => $"{sentBy} souhaite rejoindre la liane.", | ||
MessageContent.MemberAdded m => $"{sentBy} a accepté {await FormatUser(m.User)} dans la liane.", | ||
MessageContent.MemberRejected m => $"{sentBy} a rejeté la demande de {await FormatUser(m.User)} pour rejoindre la liane.", | ||
MessageContent.MemberLeft m => $"{await FormatUser(m.User)} a quitté la liane", | ||
MessageContent.MemberJoinedTrip m => await FormatJoinedTrip(m), | ||
MessageContent.MemberLeftTrip m => $"{await FormatUser(m.User)} a quitté le trajet du {FormatDate(m.Trip.Value?.DepartureTime)}", | ||
_ => throw new ArgumentOutOfRangeException(nameof(content)) | ||
}; | ||
} | ||
|
||
private async Task<string> FormatJoinedTrip(MessageContent.MemberJoinedTrip m) | ||
{ | ||
var msg = $"{await FormatUser(m.User)} a rejoint le trajet du {FormatDate(m.Trip.Value?.DepartureTime)}"; | ||
if (m.TakeReturn) | ||
{ | ||
return msg + " (retour inclus)"; | ||
} | ||
|
||
return msg; | ||
} | ||
|
||
private static string FormatDate(DateTime? dateTime) => | ||
dateTime is null | ||
? "???" | ||
: TimeZoneInfo.ConvertTime(dateTime.Value, TimeZone).ToString("dddd d MMMM", Culture); | ||
|
||
private async Task<string> FormatUser(Ref<Api.Auth.User>? user) | ||
{ | ||
if (user is null) | ||
{ | ||
return "???"; | ||
} | ||
|
||
var resolved = await user.Resolve(userService.Get); | ||
|
||
return resolved.Pseudo; | ||
} | ||
|
||
public async Task<PaginatedResponse<LianeMessage>> GetMessages(Ref<Api.Community.Liane> liane, Pagination pagination) | ||
{ | ||
using var connection = db.NewConnection(); | ||
using var tx = connection.BeginTransaction(); | ||
|
||
var lianeId = liane.IdAsGuid(); | ||
var member = await MarkAsRead(connection, lianeId, tx, DateTime.UtcNow); | ||
|
||
var filter = Filter<LianeMessageDb>.Where(m => m.LianeId, ComparisonOperator.Eq, lianeId) | ||
& Filter<LianeMessageDb>.Where(m => m.CreatedAt, ComparisonOperator.Gt, member.JoinedAt); | ||
|
||
var query = Query.Select<LianeMessageDb>() | ||
.Where(filter) | ||
.And(pagination.ToFilter<LianeMessageDb>()) | ||
.OrderBy(m => m.Id, false) | ||
.OrderBy(m => m.CreatedAt, false) | ||
.Take(pagination.Limit + 1); | ||
|
||
var total = await connection.QuerySingleAsync<int>(Query.Count<LianeMessageDb>().Where(filter), tx); | ||
var result = await connection.QueryAsync(query, tx); | ||
|
||
tx.Commit(); | ||
|
||
var hasNext = result.Count > pagination.Limit; | ||
var cursor = hasNext ? result.Last().ToCursor() : null; | ||
return new PaginatedResponse<LianeMessage>( | ||
Math.Min(result.Count, pagination.Limit), | ||
cursor, | ||
result.Take(pagination.Limit) | ||
.Select(m => new LianeMessage(m.Id.ToString(), m.CreatedBy, m.CreatedAt, m.Content)) | ||
.ToImmutableList(), | ||
total); | ||
} | ||
|
||
public async Task<ImmutableDictionary<Ref<Api.Community.Liane>, int>> GetUnreadLianes() | ||
{ | ||
using var connection = db.NewConnection(); | ||
var userId = currentContext.CurrentUser().Id; | ||
var unread = await connection.QueryAsync<(Guid, int)>(""" | ||
SELECT m.liane_id, COUNT(msg.id) AS unread | ||
FROM liane_member m | ||
INNER JOIN liane_request r ON m.liane_request_id = r.id | ||
LEFT JOIN liane_message msg ON msg.liane_id = m.liane_id AND msg.created_at > m.joined_at | ||
WHERE m.joined_at IS NOT NULL AND r.created_by = @userId | ||
AND (m.last_read_at IS NULL OR msg.created_at > m.last_read_at) | ||
GROUP BY m.liane_id | ||
""", | ||
new { userId } | ||
); | ||
return unread.ToImmutableDictionary(m => (Ref<Api.Community.Liane>)m.Item1.ToString(), m => m.Item2); | ||
} | ||
|
||
public async Task MarkAsRead(Ref<Api.Community.Liane> liane, DateTime timestamp) | ||
{ | ||
using var connection = db.NewConnection(); | ||
using var tx = connection.BeginTransaction(); | ||
var lianeId = Guid.Parse(liane.Id); | ||
await MarkAsRead(connection, lianeId, tx, timestamp); | ||
tx.Commit(); | ||
} | ||
|
||
public async Task<LianeMemberDb?> TryGetMember(IDbConnection connection, Guid lianeId, string? userId, IDbTransaction? tx) | ||
{ | ||
var userIdValue = userId ?? currentContext.CurrentUser().Id; | ||
var lianeMemberDb = await connection.QueryFirstOrDefaultAsync<LianeMemberDb>(""" | ||
SELECT liane_member.liane_request_id, liane_member.liane_id, liane_member.requested_at, liane_member.joined_at, liane_member.last_read_at | ||
FROM liane_member | ||
INNER JOIN liane_request ON liane_member.liane_request_id = liane_request.id | ||
WHERE liane_member.liane_id = @lianeId AND liane_request.created_by = @userId | ||
""", new { userId = userIdValue, lianeId }, tx); | ||
return lianeMemberDb; | ||
} | ||
|
||
private async Task<LianeMemberDb> MarkAsRead(IDbConnection connection, Guid lianeId, IDbTransaction tx, DateTime now) | ||
{ | ||
var lianeMemberDb = await CheckIsMember(connection, lianeId, tx: tx); | ||
|
||
var update = Query.Update<LianeMemberDb>() | ||
.Set(m => m.LastReadAt, now) | ||
.Where(l => l.LianeRequestId, ComparisonOperator.Eq, lianeMemberDb.LianeRequestId) | ||
.And(l => l.LianeId, ComparisonOperator.Eq, lianeMemberDb.LianeId); | ||
await connection.UpdateAsync(update, tx); | ||
|
||
return lianeMemberDb with { LastReadAt = now }; | ||
} | ||
|
||
private async Task<LianeMemberDb> CheckIsMember(IDbConnection connection, Guid lianeId, string? userId = null, IDbTransaction? tx = null) | ||
{ | ||
var lianeMemberDb = await TryGetMember(connection, lianeId, userId, tx); | ||
|
||
if (lianeMemberDb is null) | ||
{ | ||
throw new UnauthorizedAccessException("User is not part of the liane"); | ||
} | ||
|
||
return lianeMemberDb; | ||
} | ||
} |
Oops, something went wrong.