Compare commits
10 Commits
15687a0c32
...
master
Author | SHA1 | Date | |
---|---|---|---|
46ebd92dc1
|
|||
7f8521bb40
|
|||
f01226d91a
|
|||
6cb6dee6be
|
|||
0e9caf67ff
|
|||
ca70bb5487
|
|||
59ed135f20
|
|||
6077f91529
|
|||
5c485bb1c3
|
|||
27d979d77b
|
@@ -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; }
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
@@ -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.");
|
||||||
|
@@ -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()
|
||||||
};
|
};
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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.");
|
||||||
@@ -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.");
|
||||||
@@ -148,9 +150,74 @@ public partial class ChatController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[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,14 +389,16 @@ 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.");
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
0
DysonNetwork.Sphere/Post/AccountHelperClient.cs
Normal file
0
DysonNetwork.Sphere/Post/AccountHelperClient.cs
Normal 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,
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
@@ -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 =>
|
||||||
|
@@ -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)
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user