Compare commits

...

10 Commits

13 changed files with 202 additions and 74 deletions

View File

@@ -80,7 +80,7 @@ public class AccountCurrentController(
[MaxLength(1024)] public string? TimeZone { get; set; } [MaxLength(1024)] public string? TimeZone { get; set; }
[MaxLength(1024)] public string? Location { get; set; } [MaxLength(1024)] public string? Location { get; set; }
[MaxLength(4096)] public string? Bio { get; set; } [MaxLength(4096)] public string? Bio { get; set; }
public UsernameColor? UsernameColor { get; set; } public Shared.Models.UsernameColor? UsernameColor { get; set; }
public Instant? Birthday { get; set; } public Instant? Birthday { get; set; }
public List<ProfileLink>? Links { get; set; } public List<ProfileLink>? Links { get; set; }
@@ -933,4 +933,4 @@ public class AccountCurrentController(
.ToListAsync(); .ToListAsync();
return Ok(records); return Ok(records);
} }
} }

View File

@@ -197,7 +197,8 @@ public class SubscriptionGiftController(
if (currentUser.Profile.Level < MinimumAccountLevel) if (currentUser.Profile.Level < MinimumAccountLevel)
{ {
return StatusCode(403, "Account level must be at least 60 to purchase a gift."); if (currentUser.PerkSubscription is null)
return StatusCode(403, "Account level must be at least 60 or a member of the Stellar Program to purchase a gift.");
} }
Duration? giftDuration = null; Duration? giftDuration = null;

View File

@@ -250,6 +250,14 @@ public class SubscriptionService(
: null; : null;
if (subscriptionInfo is null) throw new InvalidOperationException("No matching subscription found."); if (subscriptionInfo is null) throw new InvalidOperationException("No matching subscription found.");
if (subscriptionInfo.RequiredLevel > 0)
{
var profile = await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == subscription.AccountId);
if (profile is null) throw new InvalidOperationException("Account must have a profile");
if (profile.Level < subscriptionInfo.RequiredLevel)
throw new InvalidOperationException("Account level must be at least 60 to purchase a gift.");
}
return await payment.CreateOrderAsync( return await payment.CreateOrderAsync(
null, null,
subscriptionInfo.Currency, subscriptionInfo.Currency,
@@ -684,6 +692,9 @@ public class SubscriptionService(
if (now > gift.ExpiresAt) if (now > gift.ExpiresAt)
throw new InvalidOperationException("Gift has expired."); throw new InvalidOperationException("Gift has expired.");
if (gift.GifterId == redeemer.Id)
throw new InvalidOperationException("You cannot redeem your own gift.");
// Validate redeemer permissions // Validate redeemer permissions
if (!gift.IsOpenGift && gift.RecipientId != redeemer.Id) if (!gift.IsOpenGift && gift.RecipientId != redeemer.Id)
throw new InvalidOperationException("This gift is not intended for you."); throw new InvalidOperationException("This gift is not intended for you.");

View File

@@ -148,6 +148,32 @@ public class UsernameColor
public string? Value { get; set; } // e.g. "red" or "#ff6600" public string? Value { get; set; } // e.g. "red" or "#ff6600"
public string? Direction { get; set; } // e.g. "to right" public string? Direction { get; set; } // e.g. "to right"
public List<string>? Colors { get; set; } // e.g. ["#ff0000", "#00ff00"] public List<string>? Colors { get; set; } // e.g. ["#ff0000", "#00ff00"]
public Proto.UsernameColor ToProtoValue()
{
var proto = new Proto.UsernameColor
{
Type = Type,
Value = Value,
Direction = Direction,
};
if (Colors is not null)
{
proto.Colors.AddRange(Colors);
}
return proto;
}
public static UsernameColor FromProtoValue(Proto.UsernameColor proto)
{
return new UsernameColor
{
Type = proto.Type,
Value = proto.Value,
Direction = proto.Direction,
Colors = proto.Colors?.ToList()
};
}
} }
public class SnAccountProfile : ModelBase, IIdentifiedResource public class SnAccountProfile : ModelBase, IIdentifiedResource
@@ -218,6 +244,7 @@ public class SnAccountProfile : ModelBase, IIdentifiedResource
AccountId = AccountId.ToString(), AccountId = AccountId.ToString(),
Verification = Verification?.ToProtoValue(), Verification = Verification?.ToProtoValue(),
ActiveBadge = ActiveBadge?.ToProtoValue(), ActiveBadge = ActiveBadge?.ToProtoValue(),
UsernameColor = UsernameColor?.ToProtoValue(),
CreatedAt = CreatedAt.ToTimestamp(), CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp() UpdatedAt = UpdatedAt.ToTimestamp()
}; };
@@ -247,6 +274,7 @@ public class SnAccountProfile : ModelBase, IIdentifiedResource
Picture = proto.Picture is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Picture), Picture = proto.Picture is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Picture),
Background = proto.Background is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Background), Background = proto.Background is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Background),
AccountId = Guid.Parse(proto.AccountId), AccountId = Guid.Parse(proto.AccountId),
UsernameColor = proto.UsernameColor is not null ? UsernameColor.FromProtoValue(proto.UsernameColor) : null,
CreatedAt = proto.CreatedAt.ToInstant(), CreatedAt = proto.CreatedAt.ToInstant(),
UpdatedAt = proto.UpdatedAt.ToInstant() UpdatedAt = proto.UpdatedAt.ToInstant()
}; };

View File

@@ -123,7 +123,7 @@ public class SnPostCategorySubscription : ModelBase
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
public Guid? CategoryId { get; set; } public Guid? CategoryId { get; set; }
public SnPostCategory? Category { get; set; } public SnPostCategory? Category { get; set; }
public Guid? TagId { get; set; } public Guid? TagId { get; set; }
@@ -168,6 +168,7 @@ public class SnPostReaction : ModelBase
public Guid PostId { get; set; } public Guid PostId { get; set; }
[JsonIgnore] public SnPost Post { get; set; } = null!; [JsonIgnore] public SnPost Post { get; set; } = null!;
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
[NotMapped] public SnAccount? Account { get; set; }
} }
public class SnPostAward : ModelBase public class SnPostAward : ModelBase
@@ -176,7 +177,7 @@ public class SnPostAward : ModelBase
public decimal Amount { get; set; } public decimal Amount { get; set; }
public PostReactionAttitude Attitude { get; set; } public PostReactionAttitude Attitude { get; set; }
[MaxLength(4096)] public string? Message { get; set; } [MaxLength(4096)] public string? Message { get; set; }
public Guid PostId { get; set; } public Guid PostId { get; set; }
[JsonIgnore] public SnPost Post { get; set; } = null!; [JsonIgnore] public SnPost Post { get; set; } = null!;
public Guid AccountId { get; set; } public Guid AccountId { get; set; }

View File

@@ -59,6 +59,13 @@ message AccountStatus {
bytes meta = 10; bytes meta = 10;
} }
message UsernameColor {
string type = 1;
google.protobuf.StringValue value = 2;
google.protobuf.StringValue direction = 3;
repeated string colors = 4;
}
// Profile contains detailed information about a user // Profile contains detailed information about a user
message AccountProfile { message AccountProfile {
string id = 1; string id = 1;
@@ -89,6 +96,7 @@ message AccountProfile {
google.protobuf.Timestamp created_at = 22; google.protobuf.Timestamp created_at = 22;
google.protobuf.Timestamp updated_at = 23; google.protobuf.Timestamp updated_at = 23;
optional UsernameColor username_color = 24;
} }
// AccountContact represents a contact method for an account // AccountContact represents a contact method for an account

View File

@@ -16,7 +16,7 @@ public class AutocompletionService(AppDatabase db, AccountClientHelper accountsH
var afterAt = content[1..]; var afterAt = content[1..];
string type; string type;
string query; string query;
bool hadSlash = afterAt.Contains('/'); var hadSlash = afterAt.Contains('/');
if (hadSlash) if (hadSlash)
{ {
var parts = afterAt.Split('/', 2); var parts = afterAt.Split('/', 2);
@@ -130,30 +130,17 @@ public class AutocompletionService(AppDatabase db, AccountClientHelper accountsH
{ {
var stickers = await db.Stickers var stickers = await db.Stickers
.Include(s => s.Pack) .Include(s => s.Pack)
.Where(s => EF.Functions.Like(s.Slug, $"{query}%")) .Where(s => EF.Functions.Like(s.Pack.Prefix + "+" + s.Slug, $"{query}%"))
.Take(limit) .Take(limit)
.Select(s => new DysonNetwork.Shared.Models.Autocompletion .Select(s => new DysonNetwork.Shared.Models.Autocompletion
{ {
Type = "sticker", Type = "sticker",
Keyword = s.Slug, Keyword = $":{s.Pack.Prefix}+{s.Slug}:",
Data = s Data = s
}) })
.ToListAsync(); .ToListAsync();
// Also possibly search by pack prefix? But user said slug after : var results = stickers.ToList();
// Perhaps combine or search packs
var packs = await db.StickerPacks
.Where(p => EF.Functions.Like(p.Prefix, $"{query}%"))
.Take(limit)
.Select(p => new DysonNetwork.Shared.Models.Autocompletion
{
Type = "sticker_pack",
Keyword = p.Prefix,
Data = p
})
.ToListAsync();
var results = stickers.Concat(packs).Take(limit).ToList();
return results; return results;
} }
} }

View File

@@ -87,7 +87,8 @@ public partial class ChatController(
var accountId = Guid.Parse(currentUser.Id); var accountId = Guid.Parse(currentUser.Id);
var member = await db.ChatMembers var member = await db.ChatMembers
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null) .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null &&
m.LeaveAt == null)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (member == null || member.Role < ChatMemberRole.Member) if (member == null || member.Role < ChatMemberRole.Member)
return StatusCode(403, "You are not a member of this chat room."); return StatusCode(403, "You are not a member of this chat room.");
@@ -103,10 +104,10 @@ public partial class ChatController(
.Skip(offset) .Skip(offset)
.Take(take) .Take(take)
.ToListAsync(); .ToListAsync();
var members = messages.Select(m => m.Sender).DistinctBy(x => x.Id).ToList(); var members = messages.Select(m => m.Sender).DistinctBy(x => x.Id).ToList();
members = await crs.LoadMemberAccounts(members); members = await crs.LoadMemberAccounts(members);
foreach (var message in messages) foreach (var message in messages)
message.Sender = members.First(x => x.Id == message.SenderId); message.Sender = members.First(x => x.Id == message.SenderId);
@@ -129,7 +130,8 @@ public partial class ChatController(
var accountId = Guid.Parse(currentUser.Id); var accountId = Guid.Parse(currentUser.Id);
var member = await db.ChatMembers var member = await db.ChatMembers
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null) .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null &&
m.LeaveAt == null)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (member == null || member.Role < ChatMemberRole.Member) if (member == null || member.Role < ChatMemberRole.Member)
return StatusCode(403, "You are not a member of this chat room."); return StatusCode(403, "You are not a member of this chat room.");
@@ -141,16 +143,81 @@ public partial class ChatController(
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (message is null) return NotFound(); if (message is null) return NotFound();
message.Sender = await crs.LoadMemberAccount(message.Sender); message.Sender = await crs.LoadMemberAccount(message.Sender);
return Ok(message); return Ok(message);
} }
[GeneratedRegex("@([A-Za-z0-9_-]+)")] [GeneratedRegex(@"@(?:u/)?([A-Za-z0-9_-]+)")]
private static partial Regex MentionRegex(); private static partial Regex MentionRegex();
/// <summary>
/// Extracts mentioned users from message content, replies, and forwards
/// </summary>
private async Task<List<Guid>> ExtractMentionedUsersAsync(string? content, Guid? repliedMessageId,
Guid? forwardedMessageId, Guid roomId, Guid? excludeSenderId = null)
{
var mentionedUsers = new List<Guid>();
// Add sender of a replied message
if (repliedMessageId.HasValue)
{
var replyingTo = await db.ChatMessages
.Where(m => m.Id == repliedMessageId.Value && m.ChatRoomId == roomId)
.Include(m => m.Sender)
.Select(m => m.Sender)
.FirstOrDefaultAsync();
if (replyingTo != null)
mentionedUsers.Add(replyingTo.AccountId);
}
// Add sender of a forwarded message
if (forwardedMessageId.HasValue)
{
var forwardedMessage = await db.ChatMessages
.Where(m => m.Id == forwardedMessageId.Value)
.Select(m => new { m.SenderId })
.FirstOrDefaultAsync();
if (forwardedMessage != null)
{
mentionedUsers.Add(forwardedMessage.SenderId);
}
}
// Extract mentions from content using regex
if (!string.IsNullOrWhiteSpace(content))
{
var mentionedNames = MentionRegex()
.Matches(content)
.Select(m => m.Groups[1].Value)
.Distinct()
.ToList();
if (mentionedNames.Count > 0)
{
var queryRequest = new LookupAccountBatchRequest();
queryRequest.Names.AddRange(mentionedNames);
var queryResponse = (await accounts.LookupAccountBatchAsync(queryRequest)).Accounts;
var mentionedIds = queryResponse.Select(a => Guid.Parse(a.Id)).ToList();
if (mentionedIds.Count > 0)
{
var mentionedMembers = await db.ChatMembers
.Where(m => m.ChatRoomId == roomId && mentionedIds.Contains(m.AccountId))
.Where(m => m.JoinedAt != null && m.LeaveAt == null)
.Where(m => excludeSenderId == null || m.AccountId != excludeSenderId.Value)
.Select(m => m.AccountId)
.ToListAsync();
mentionedUsers.AddRange(mentionedMembers);
}
}
}
return mentionedUsers.Distinct().ToList();
}
[HttpPost("{roomId:guid}/messages")] [HttpPost("{roomId:guid}/messages")]
[Authorize] [Authorize]
[RequiredPermission("global", "chat.messages.create")] [RequiredPermission("global", "chat.messages.create")]
@@ -188,6 +255,7 @@ public partial class ChatController(
.ToList(); .ToList();
} }
// Validate reply and forward message IDs exist
if (request.RepliedMessageId.HasValue) if (request.RepliedMessageId.HasValue)
{ {
var repliedMessage = await db.ChatMessages var repliedMessage = await db.ChatMessages
@@ -208,28 +276,9 @@ public partial class ChatController(
message.ForwardedMessageId = forwardedMessage.Id; message.ForwardedMessageId = forwardedMessage.Id;
} }
if (request.Content is not null) // Extract mentioned users
{ message.MembersMentioned = await ExtractMentionedUsersAsync(request.Content, request.RepliedMessageId,
var mentioned = MentionRegex() request.ForwardedMessageId, roomId);
.Matches(request.Content)
.Select(m => m.Groups[1].Value)
.ToList();
if (mentioned.Count > 0)
{
var queryRequest = new LookupAccountBatchRequest();
queryRequest.Names.AddRange(mentioned);
var queryResponse = (await accounts.LookupAccountBatchAsync(queryRequest)).Accounts;
var mentionedId = queryResponse
.Select(a => Guid.Parse(a.Id))
.ToList();
var mentionedMembers = await db.ChatMembers
.Where(m => m.ChatRoomId == roomId && mentionedId.Contains(m.AccountId))
.Where(m => m.JoinedAt != null && m.LeaveAt == null)
.Select(m => m.Id)
.ToListAsync();
message.MembersMentioned = mentionedMembers;
}
}
var result = await cs.SendMessageAsync(message, member, member.ChatRoom); var result = await cs.SendMessageAsync(message, member, member.ChatRoom);
@@ -259,6 +308,7 @@ public partial class ChatController(
(request.AttachmentsId == null || request.AttachmentsId.Count == 0)) (request.AttachmentsId == null || request.AttachmentsId.Count == 0))
return BadRequest("You cannot send an empty message."); return BadRequest("You cannot send an empty message.");
// Validate reply and forward message IDs exist
if (request.RepliedMessageId.HasValue) if (request.RepliedMessageId.HasValue)
{ {
var repliedMessage = await db.ChatMessages var repliedMessage = await db.ChatMessages
@@ -275,6 +325,11 @@ public partial class ChatController(
return BadRequest("The message you're forwarding does not exist."); return BadRequest("The message you're forwarding does not exist.");
} }
// Update mentions based on new content and references
var updatedMentions = await ExtractMentionedUsersAsync(request.Content, request.RepliedMessageId,
request.ForwardedMessageId, roomId, accountId);
message.MembersMentioned = updatedMentions;
// Call service method to update the message // Call service method to update the message
await cs.UpdateMessageAsync( await cs.UpdateMessageAsync(
message, message,
@@ -324,7 +379,8 @@ public partial class ChatController(
var accountId = Guid.Parse(currentUser.Id); var accountId = Guid.Parse(currentUser.Id);
var isMember = await db.ChatMembers var isMember = await db.ChatMembers
.AnyAsync(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null); .AnyAsync(m =>
m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null);
if (!isMember) if (!isMember)
return StatusCode(403, "You are not a member of this chat room."); return StatusCode(403, "You are not a member of this chat room.");
@@ -333,18 +389,20 @@ public partial class ChatController(
} }
[HttpPost("{roomId:guid}/autocomplete")] [HttpPost("{roomId:guid}/autocomplete")]
public async Task<ActionResult<List<DysonNetwork.Shared.Models.Autocompletion>>> ChatAutoComplete([FromBody] AutocompletionRequest request, Guid roomId) public async Task<ActionResult<List<DysonNetwork.Shared.Models.Autocompletion>>> ChatAutoComplete(
[FromBody] AutocompletionRequest request, Guid roomId)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized(); return Unauthorized();
var accountId = Guid.Parse(currentUser.Id); var accountId = Guid.Parse(currentUser.Id);
var isMember = await db.ChatMembers var isMember = await db.ChatMembers
.AnyAsync(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null); .AnyAsync(m =>
m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null);
if (!isMember) if (!isMember)
return StatusCode(403, "You are not a member of this chat room."); return StatusCode(403, "You are not a member of this chat room.");
var result = await aus.GetAutocompletion(request.Content, chatId: roomId, limit: 10); var result = await aus.GetAutocompletion(request.Content, chatId: roomId, limit: 10);
return Ok(result); return Ok(result);
} }
} }

View File

@@ -198,8 +198,6 @@ public partial class ChatService(
public async Task<SnChatMessage> SendMessageAsync(SnChatMessage message, SnChatMember sender, SnChatRoom room) public async Task<SnChatMessage> SendMessageAsync(SnChatMessage message, SnChatMember sender, SnChatRoom room)
{ {
if (string.IsNullOrWhiteSpace(message.Nonce)) message.Nonce = Guid.NewGuid().ToString(); if (string.IsNullOrWhiteSpace(message.Nonce)) message.Nonce = Guid.NewGuid().ToString();
message.CreatedAt = SystemClock.Instance.GetCurrentInstant();
message.UpdatedAt = message.CreatedAt;
// First complete the save operation // First complete the save operation
db.ChatMessages.Add(message); db.ChatMessages.Add(message);
@@ -209,20 +207,25 @@ public partial class ChatService(
await CreateFileReferencesForMessageAsync(message); await CreateFileReferencesForMessageAsync(message);
// Then start the delivery process // Then start the delivery process
var localMessage = message;
var localSender = sender;
var localRoom = room;
var localLogger = logger;
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
try try
{ {
await DeliverMessageAsync(message, sender, room); await DeliverMessageAsync(localMessage, localSender, localRoom);
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError($"Error when delivering message: {ex.Message} {ex.StackTrace}"); localLogger.LogError($"Error when delivering message: {ex.Message} {ex.StackTrace}");
} }
}); });
// Process link preview in the background to avoid delaying message sending // Process link preview in the background to avoid delaying message sending
_ = Task.Run(async () => await ProcessMessageLinkPreviewAsync(message)); var localMessageForPreview = message;
_ = Task.Run(async () => await ProcessMessageLinkPreviewAsync(localMessageForPreview));
message.Sender = sender; message.Sender = sender;
message.ChatRoom = room; message.ChatRoom = room;

View File

@@ -4,6 +4,7 @@ using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry;
using DysonNetwork.Sphere.Poll; using DysonNetwork.Sphere.Poll;
using DysonNetwork.Sphere.Realm; using DysonNetwork.Sphere.Realm;
using DysonNetwork.Sphere.WebReader; using DysonNetwork.Sphere.WebReader;
@@ -23,6 +24,7 @@ public class PostController(
AppDatabase db, AppDatabase db,
PostService ps, PostService ps,
PublisherService pub, PublisherService pub,
AccountClientHelper accountsHelper,
AccountService.AccountServiceClient accounts, AccountService.AccountServiceClient accounts,
ActionLogService.ActionLogServiceClient als, ActionLogService.ActionLogServiceClient als,
PaymentService.PaymentServiceClient payments, PaymentService.PaymentServiceClient payments,
@@ -97,7 +99,7 @@ public class PostController(
if (currentUser != null) if (currentUser != null)
{ {
var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
{ AccountId = currentUser.Id }); { AccountId = currentUser.Id });
userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
} }
@@ -197,7 +199,7 @@ public class PostController(
if (currentUser != null) if (currentUser != null)
{ {
var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
{ AccountId = currentUser.Id }); { AccountId = currentUser.Id });
userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
} }
@@ -228,7 +230,7 @@ public class PostController(
if (currentUser != null) if (currentUser != null)
{ {
var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
{ AccountId = currentUser.Id }); { AccountId = currentUser.Id });
userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
} }
@@ -271,6 +273,14 @@ public class PostController(
.Take(take) .Take(take)
.Skip(offset) .Skip(offset)
.ToListAsync(); .ToListAsync();
var accountsProto = await accountsHelper.GetAccountBatch(reactions.Select(r => r.AccountId).ToList());
var accounts = accountsProto.ToDictionary(a => Guid.Parse(a.Id), a => SnAccount.FromProtoValue(a));
foreach (var reaction in reactions)
if (accounts.TryGetValue(reaction.AccountId, out var account))
reaction.Account = account;
return Ok(reactions); return Ok(reactions);
} }
@@ -283,7 +293,7 @@ public class PostController(
if (currentUser != null) if (currentUser != null)
{ {
var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
{ AccountId = currentUser.Id }); { AccountId = currentUser.Id });
userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
} }
@@ -314,7 +324,7 @@ public class PostController(
if (currentUser != null) if (currentUser != null)
{ {
var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
{ AccountId = currentUser.Id }); { AccountId = currentUser.Id });
userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
} }
@@ -342,7 +352,7 @@ public class PostController(
if (currentUser != null) if (currentUser != null)
{ {
var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
{ AccountId = currentUser.Id }); { AccountId = currentUser.Id });
userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
} }
@@ -448,7 +458,10 @@ public class PostController(
if (request.RepliedPostId is not null) if (request.RepliedPostId is not null)
{ {
var repliedPost = await db.Posts.FindAsync(request.RepliedPostId.Value); var repliedPost = await db.Posts
.Where(p => p.Id == request.RepliedPostId.Value)
.Include(p => p.Publisher)
.FirstOrDefaultAsync();
if (repliedPost is null) return BadRequest("Post replying to was not found."); if (repliedPost is null) return BadRequest("Post replying to was not found.");
post.RepliedPost = repliedPost; post.RepliedPost = repliedPost;
post.RepliedPostId = repliedPost.Id; post.RepliedPostId = repliedPost.Id;
@@ -456,7 +469,10 @@ public class PostController(
if (request.ForwardedPostId is not null) if (request.ForwardedPostId is not null)
{ {
var forwardedPost = await db.Posts.FindAsync(request.ForwardedPostId.Value); var forwardedPost = await db.Posts
.Where(p => p.Id == request.ForwardedPostId.Value)
.Include(p => p.Publisher)
.FirstOrDefaultAsync();
if (forwardedPost is null) return BadRequest("Forwarded post was not found."); if (forwardedPost is null) return BadRequest("Forwarded post was not found.");
post.ForwardedPost = forwardedPost; post.ForwardedPost = forwardedPost;
post.ForwardedPostId = forwardedPost.Id; post.ForwardedPostId = forwardedPost.Id;
@@ -514,7 +530,7 @@ public class PostController(
}); });
post.Publisher = publisher; post.Publisher = publisher;
return post; return post;
} }
@@ -536,7 +552,7 @@ public class PostController(
var friendsResponse = var friendsResponse =
await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
{ AccountId = currentUser.Id.ToString() }); { AccountId = currentUser.Id.ToString() });
var userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); var userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
var userPublishers = await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); var userPublishers = await pub.GetUserPublishers(Guid.Parse(currentUser.Id));
@@ -632,7 +648,7 @@ public class PostController(
var friendsResponse = var friendsResponse =
await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
{ AccountId = currentUser.Id.ToString() }); { AccountId = currentUser.Id.ToString() });
var userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); var userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
var userPublishers = await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); var userPublishers = await pub.GetUserPublishers(Guid.Parse(currentUser.Id));
@@ -883,7 +899,7 @@ public class PostController(
UserAgent = Request.Headers.UserAgent, UserAgent = Request.Headers.UserAgent,
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
}); });
return Ok(post); return Ok(post);
} }
@@ -915,4 +931,4 @@ public class PostController(
return NoContent(); return NoContent();
} }
} }

View File

@@ -41,7 +41,6 @@ public static class ServiceCollectionExtensions
{ {
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
}).AddDataAnnotationsLocalization(options => }).AddDataAnnotationsLocalization(options =>

View File

@@ -237,6 +237,22 @@ public class StickerController(
return Redirect($"/drive/files/{sticker.Image.Id}?original=true"); return Redirect($"/drive/files/{sticker.Image.Id}?original=true");
} }
[HttpGet("search")]
public async Task<ActionResult<List<SnSticker>>> SearchSticker([FromQuery] string query, [FromQuery] int take = 10, [FromQuery] int offset = 0)
{
var queryable = db.Stickers
.Include(s => s.Pack)
.Where(s => EF.Functions.Like(s.Pack.Prefix + "+" + s.Slug, $"{query}%"))
.OrderByDescending(s => s.CreatedAt)
.AsQueryable();
var totalCount = await queryable.CountAsync();
Response.Headers["X-Total"] = totalCount.ToString();
var stickers = await queryable.Take(take).Skip(offset).ToListAsync();
return Ok(stickers);
}
[HttpGet("{packId:guid}/content/{id:guid}")] [HttpGet("{packId:guid}/content/{id:guid}")]
public async Task<ActionResult<SnSticker>> GetSticker(Guid packId, Guid id) public async Task<ActionResult<SnSticker>> GetSticker(Guid packId, Guid id)
{ {
@@ -420,4 +436,4 @@ public class StickerController(
return NoContent(); return NoContent();
} }
} }