Compare commits
16 Commits
32977d9580
...
master
Author | SHA1 | Date | |
---|---|---|---|
46ebd92dc1
|
|||
7f8521bb40
|
|||
f01226d91a
|
|||
6cb6dee6be
|
|||
0e9caf67ff
|
|||
ca70bb5487
|
|||
59ed135f20
|
|||
6077f91529
|
|||
5c485bb1c3
|
|||
27d979d77b
|
|||
15687a0c32
|
|||
37ea882ef7
|
|||
e624c2bb3e
|
|||
9631cd3edd
|
|||
f4a659fce5
|
|||
1ded811b36
|
@@ -1,30 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.0"/>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId>
|
||||
<RootNamespace>DysonNetwork.Control</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.0" />
|
||||
<PackageReference Include="Aspire.Hosting.Docker" Version="9.4.2-preview.1.25428.12" />
|
||||
<PackageReference Include="Aspire.Hosting.Nats" Version="9.5.0" />
|
||||
<PackageReference Include="Aspire.Hosting.Redis" Version="9.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Drive\DysonNetwork.Drive.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Ring\DysonNetwork.Ring.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Gateway\DysonNetwork.Gateway.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.1" />
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId>
|
||||
<RootNamespace>DysonNetwork.Control</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.1" />
|
||||
<PackageReference Include="Aspire.Hosting.Docker" Version="9.4.2-preview.1.25428.12" />
|
||||
<PackageReference Include="Aspire.Hosting.Nats" Version="9.5.1" />
|
||||
<PackageReference Include="Aspire.Hosting.Redis" Version="9.5.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Drive\DysonNetwork.Drive.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Ring\DysonNetwork.Ring.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Gateway\DysonNetwork.Gateway.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
@@ -10,7 +10,9 @@
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21175",
|
||||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22189"
|
||||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22189",
|
||||
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21260",
|
||||
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22052"
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
@@ -22,8 +24,9 @@
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19163",
|
||||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20185"
|
||||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20185",
|
||||
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:22108"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -80,7 +80,7 @@ public class AccountCurrentController(
|
||||
[MaxLength(1024)] public string? TimeZone { get; set; }
|
||||
[MaxLength(1024)] public string? Location { 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 List<ProfileLink>? Links { get; set; }
|
||||
|
||||
@@ -933,4 +933,4 @@ public class AccountCurrentController(
|
||||
.ToListAsync();
|
||||
return Ok(records);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -160,6 +160,26 @@ public class AccountServiceGrpc(
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<GetAccountBatchResponse> SearchAccount(SearchAccountRequest request, ServerCallContext context)
|
||||
{
|
||||
var accounts = await _db.Accounts
|
||||
.AsNoTracking()
|
||||
.Where(a => EF.Functions.ILike(a.Name, $"%{request.Query}%"))
|
||||
.Include(a => a.Profile)
|
||||
.ToListAsync();
|
||||
|
||||
var perks = await subscriptions.GetPerkSubscriptionsAsync(
|
||||
accounts.Select(x => x.Id).ToList()
|
||||
);
|
||||
foreach (var account in accounts)
|
||||
if (perks.TryGetValue(account.Id, out var perk))
|
||||
account.PerkSubscription = perk?.ToReference();
|
||||
|
||||
var response = new GetAccountBatchResponse();
|
||||
response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue()));
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<ListAccountsResponse> ListAccounts(ListAccountsRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
|
@@ -197,7 +197,8 @@ public class SubscriptionGiftController(
|
||||
|
||||
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;
|
||||
|
@@ -250,6 +250,14 @@ public class SubscriptionService(
|
||||
: null;
|
||||
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(
|
||||
null,
|
||||
subscriptionInfo.Currency,
|
||||
@@ -684,6 +692,9 @@ public class SubscriptionService(
|
||||
if (now > gift.ExpiresAt)
|
||||
throw new InvalidOperationException("Gift has expired.");
|
||||
|
||||
if (gift.GifterId == redeemer.Id)
|
||||
throw new InvalidOperationException("You cannot redeem your own gift.");
|
||||
|
||||
// Validate redeemer permissions
|
||||
if (!gift.IsOpenGift && gift.RecipientId != redeemer.Id)
|
||||
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? Direction { get; set; } // e.g. "to right"
|
||||
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
|
||||
@@ -218,6 +244,7 @@ public class SnAccountProfile : ModelBase, IIdentifiedResource
|
||||
AccountId = AccountId.ToString(),
|
||||
Verification = Verification?.ToProtoValue(),
|
||||
ActiveBadge = ActiveBadge?.ToProtoValue(),
|
||||
UsernameColor = UsernameColor?.ToProtoValue(),
|
||||
CreatedAt = CreatedAt.ToTimestamp(),
|
||||
UpdatedAt = UpdatedAt.ToTimestamp()
|
||||
};
|
||||
@@ -247,6 +274,7 @@ public class SnAccountProfile : ModelBase, IIdentifiedResource
|
||||
Picture = proto.Picture is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Picture),
|
||||
Background = proto.Background is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Background),
|
||||
AccountId = Guid.Parse(proto.AccountId),
|
||||
UsernameColor = proto.UsernameColor is not null ? UsernameColor.FromProtoValue(proto.UsernameColor) : null,
|
||||
CreatedAt = proto.CreatedAt.ToInstant(),
|
||||
UpdatedAt = proto.UpdatedAt.ToInstant()
|
||||
};
|
||||
|
19
DysonNetwork.Shared/Models/ActivityHeatmap.cs
Normal file
19
DysonNetwork.Shared/Models/ActivityHeatmap.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
public class ActivityHeatmap
|
||||
{
|
||||
public string Unit { get; set; } = "posts";
|
||||
|
||||
public Instant PeriodStart { get; set; }
|
||||
public Instant PeriodEnd { get; set; }
|
||||
|
||||
public List<ActivityHeatmapItem> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ActivityHeatmapItem
|
||||
{
|
||||
public Instant Date { get; set; }
|
||||
public int Count { get; set; }
|
||||
}
|
15
DysonNetwork.Shared/Models/Autocompletion.cs
Normal file
15
DysonNetwork.Shared/Models/Autocompletion.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
public class AutocompletionRequest
|
||||
{
|
||||
[Required] public string Content { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class Autocompletion
|
||||
{
|
||||
public string Type { get; set; } = null!;
|
||||
public string Keyword { get; set; } = null!;
|
||||
public object Data { get; set; } = null!;
|
||||
}
|
@@ -123,7 +123,7 @@ public class SnPostCategorySubscription : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
|
||||
public Guid? CategoryId { get; set; }
|
||||
public SnPostCategory? Category { get; set; }
|
||||
public Guid? TagId { get; set; }
|
||||
@@ -168,6 +168,7 @@ public class SnPostReaction : ModelBase
|
||||
public Guid PostId { get; set; }
|
||||
[JsonIgnore] public SnPost Post { get; set; } = null!;
|
||||
public Guid AccountId { get; set; }
|
||||
[NotMapped] public SnAccount? Account { get; set; }
|
||||
}
|
||||
|
||||
public class SnPostAward : ModelBase
|
||||
@@ -176,7 +177,7 @@ public class SnPostAward : ModelBase
|
||||
public decimal Amount { get; set; }
|
||||
public PostReactionAttitude Attitude { get; set; }
|
||||
[MaxLength(4096)] public string? Message { get; set; }
|
||||
|
||||
|
||||
public Guid PostId { get; set; }
|
||||
[JsonIgnore] public SnPost Post { get; set; } = null!;
|
||||
public Guid AccountId { get; set; }
|
||||
|
@@ -14,232 +14,240 @@ import 'wallet.proto';
|
||||
|
||||
// Account represents a user account in the system
|
||||
message Account {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string nick = 3;
|
||||
string language = 4;
|
||||
string region = 18;
|
||||
google.protobuf.Timestamp activated_at = 5;
|
||||
bool is_superuser = 6;
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string nick = 3;
|
||||
string language = 4;
|
||||
string region = 18;
|
||||
google.protobuf.Timestamp activated_at = 5;
|
||||
bool is_superuser = 6;
|
||||
|
||||
AccountProfile profile = 7;
|
||||
optional SubscriptionReferenceObject perk_subscription = 16;
|
||||
repeated AccountContact contacts = 8;
|
||||
repeated AccountBadge badges = 9;
|
||||
repeated AccountAuthFactor auth_factors = 10;
|
||||
repeated AccountConnection connections = 11;
|
||||
repeated Relationship outgoing_relationships = 12;
|
||||
repeated Relationship incoming_relationships = 13;
|
||||
AccountProfile profile = 7;
|
||||
optional SubscriptionReferenceObject perk_subscription = 16;
|
||||
repeated AccountContact contacts = 8;
|
||||
repeated AccountBadge badges = 9;
|
||||
repeated AccountAuthFactor auth_factors = 10;
|
||||
repeated AccountConnection connections = 11;
|
||||
repeated Relationship outgoing_relationships = 12;
|
||||
repeated Relationship incoming_relationships = 13;
|
||||
|
||||
google.protobuf.Timestamp created_at = 14;
|
||||
google.protobuf.Timestamp updated_at = 15;
|
||||
|
||||
google.protobuf.StringValue automated_id = 17;
|
||||
google.protobuf.Timestamp created_at = 14;
|
||||
google.protobuf.Timestamp updated_at = 15;
|
||||
|
||||
google.protobuf.StringValue automated_id = 17;
|
||||
}
|
||||
|
||||
// Enum for status attitude
|
||||
enum StatusAttitude {
|
||||
STATUS_ATTITUDE_UNSPECIFIED = 0;
|
||||
POSITIVE = 1;
|
||||
NEGATIVE = 2;
|
||||
NEUTRAL = 3;
|
||||
STATUS_ATTITUDE_UNSPECIFIED = 0;
|
||||
POSITIVE = 1;
|
||||
NEGATIVE = 2;
|
||||
NEUTRAL = 3;
|
||||
}
|
||||
|
||||
// AccountStatus represents the status of an account
|
||||
message AccountStatus {
|
||||
string id = 1;
|
||||
StatusAttitude attitude = 2;
|
||||
bool is_online = 3;
|
||||
bool is_customized = 4;
|
||||
bool is_invisible = 5;
|
||||
bool is_not_disturb = 6;
|
||||
google.protobuf.StringValue label = 7;
|
||||
google.protobuf.Timestamp cleared_at = 8;
|
||||
string account_id = 9;
|
||||
bytes meta = 10;
|
||||
string id = 1;
|
||||
StatusAttitude attitude = 2;
|
||||
bool is_online = 3;
|
||||
bool is_customized = 4;
|
||||
bool is_invisible = 5;
|
||||
bool is_not_disturb = 6;
|
||||
google.protobuf.StringValue label = 7;
|
||||
google.protobuf.Timestamp cleared_at = 8;
|
||||
string account_id = 9;
|
||||
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
|
||||
message AccountProfile {
|
||||
string id = 1;
|
||||
google.protobuf.StringValue first_name = 2;
|
||||
google.protobuf.StringValue middle_name = 3;
|
||||
google.protobuf.StringValue last_name = 4;
|
||||
google.protobuf.StringValue bio = 5;
|
||||
google.protobuf.StringValue gender = 6;
|
||||
google.protobuf.StringValue pronouns = 7;
|
||||
google.protobuf.StringValue time_zone = 8;
|
||||
google.protobuf.StringValue location = 9;
|
||||
google.protobuf.Timestamp birthday = 10;
|
||||
google.protobuf.Timestamp last_seen_at = 11;
|
||||
string id = 1;
|
||||
google.protobuf.StringValue first_name = 2;
|
||||
google.protobuf.StringValue middle_name = 3;
|
||||
google.protobuf.StringValue last_name = 4;
|
||||
google.protobuf.StringValue bio = 5;
|
||||
google.protobuf.StringValue gender = 6;
|
||||
google.protobuf.StringValue pronouns = 7;
|
||||
google.protobuf.StringValue time_zone = 8;
|
||||
google.protobuf.StringValue location = 9;
|
||||
google.protobuf.Timestamp birthday = 10;
|
||||
google.protobuf.Timestamp last_seen_at = 11;
|
||||
|
||||
VerificationMark verification = 12;
|
||||
BadgeReferenceObject active_badge = 13;
|
||||
VerificationMark verification = 12;
|
||||
BadgeReferenceObject active_badge = 13;
|
||||
|
||||
int32 experience = 14;
|
||||
int32 level = 15;
|
||||
double leveling_progress = 16;
|
||||
double social_credits = 17;
|
||||
int32 social_credits_level = 18;
|
||||
int32 experience = 14;
|
||||
int32 level = 15;
|
||||
double leveling_progress = 16;
|
||||
double social_credits = 17;
|
||||
int32 social_credits_level = 18;
|
||||
|
||||
CloudFile picture = 19;
|
||||
CloudFile background = 20;
|
||||
CloudFile picture = 19;
|
||||
CloudFile background = 20;
|
||||
|
||||
string account_id = 21;
|
||||
string account_id = 21;
|
||||
|
||||
google.protobuf.Timestamp created_at = 22;
|
||||
google.protobuf.Timestamp updated_at = 23;
|
||||
google.protobuf.Timestamp created_at = 22;
|
||||
google.protobuf.Timestamp updated_at = 23;
|
||||
optional UsernameColor username_color = 24;
|
||||
}
|
||||
|
||||
// AccountContact represents a contact method for an account
|
||||
message AccountContact {
|
||||
string id = 1;
|
||||
AccountContactType type = 2;
|
||||
google.protobuf.Timestamp verified_at = 3;
|
||||
bool is_primary = 4;
|
||||
string content = 5;
|
||||
string account_id = 6;
|
||||
string id = 1;
|
||||
AccountContactType type = 2;
|
||||
google.protobuf.Timestamp verified_at = 3;
|
||||
bool is_primary = 4;
|
||||
string content = 5;
|
||||
string account_id = 6;
|
||||
|
||||
google.protobuf.Timestamp created_at = 7;
|
||||
google.protobuf.Timestamp updated_at = 8;
|
||||
google.protobuf.Timestamp created_at = 7;
|
||||
google.protobuf.Timestamp updated_at = 8;
|
||||
}
|
||||
|
||||
// Enum for contact types
|
||||
enum AccountContactType {
|
||||
ACCOUNT_CONTACT_TYPE_UNSPECIFIED = 0;
|
||||
EMAIL = 1;
|
||||
PHONE_NUMBER = 2;
|
||||
ADDRESS = 3;
|
||||
ACCOUNT_CONTACT_TYPE_UNSPECIFIED = 0;
|
||||
EMAIL = 1;
|
||||
PHONE_NUMBER = 2;
|
||||
ADDRESS = 3;
|
||||
}
|
||||
|
||||
// AccountAuthFactor represents an authentication factor for an account
|
||||
message AccountAuthFactor {
|
||||
string id = 1;
|
||||
AccountAuthFactorType type = 2;
|
||||
google.protobuf.StringValue secret = 3; // Omitted from JSON serialization in original
|
||||
map<string, google.protobuf.Value> config = 4; // Omitted from JSON serialization in original
|
||||
int32 trustworthy = 5;
|
||||
google.protobuf.Timestamp enabled_at = 6;
|
||||
google.protobuf.Timestamp expired_at = 7;
|
||||
string account_id = 8;
|
||||
map<string, google.protobuf.Value> created_response = 9; // For initial setup
|
||||
string id = 1;
|
||||
AccountAuthFactorType type = 2;
|
||||
google.protobuf.StringValue secret = 3; // Omitted from JSON serialization in original
|
||||
map<string, google.protobuf.Value> config = 4; // Omitted from JSON serialization in original
|
||||
int32 trustworthy = 5;
|
||||
google.protobuf.Timestamp enabled_at = 6;
|
||||
google.protobuf.Timestamp expired_at = 7;
|
||||
string account_id = 8;
|
||||
map<string, google.protobuf.Value> created_response = 9; // For initial setup
|
||||
|
||||
google.protobuf.Timestamp created_at = 10;
|
||||
google.protobuf.Timestamp updated_at = 11;
|
||||
google.protobuf.Timestamp created_at = 10;
|
||||
google.protobuf.Timestamp updated_at = 11;
|
||||
}
|
||||
|
||||
// Enum for authentication factor types
|
||||
enum AccountAuthFactorType {
|
||||
AUTH_FACTOR_TYPE_UNSPECIFIED = 0;
|
||||
PASSWORD = 1;
|
||||
EMAIL_CODE = 2;
|
||||
IN_APP_CODE = 3;
|
||||
TIMED_CODE = 4;
|
||||
PIN_CODE = 5;
|
||||
AUTH_FACTOR_TYPE_UNSPECIFIED = 0;
|
||||
PASSWORD = 1;
|
||||
EMAIL_CODE = 2;
|
||||
IN_APP_CODE = 3;
|
||||
TIMED_CODE = 4;
|
||||
PIN_CODE = 5;
|
||||
}
|
||||
|
||||
// AccountBadge represents a badge associated with an account
|
||||
message AccountBadge {
|
||||
string id = 1; // Unique identifier for the badge
|
||||
string type = 2; // Type/category of the badge
|
||||
google.protobuf.StringValue label = 3; // Display name of the badge
|
||||
google.protobuf.StringValue caption = 4; // Optional description of the badge
|
||||
map<string, google.protobuf.Value> meta = 5; // Additional metadata for the badge
|
||||
google.protobuf.Timestamp activated_at = 6; // When the badge was activated
|
||||
google.protobuf.Timestamp expired_at = 7; // Optional expiration time
|
||||
string account_id = 8; // ID of the account this badge belongs to
|
||||
string id = 1; // Unique identifier for the badge
|
||||
string type = 2; // Type/category of the badge
|
||||
google.protobuf.StringValue label = 3; // Display name of the badge
|
||||
google.protobuf.StringValue caption = 4; // Optional description of the badge
|
||||
map<string, google.protobuf.Value> meta = 5; // Additional metadata for the badge
|
||||
google.protobuf.Timestamp activated_at = 6; // When the badge was activated
|
||||
google.protobuf.Timestamp expired_at = 7; // Optional expiration time
|
||||
string account_id = 8; // ID of the account this badge belongs to
|
||||
|
||||
google.protobuf.Timestamp created_at = 9;
|
||||
google.protobuf.Timestamp updated_at = 10;
|
||||
google.protobuf.Timestamp created_at = 9;
|
||||
google.protobuf.Timestamp updated_at = 10;
|
||||
}
|
||||
|
||||
// AccountConnection represents a third-party connection for an account
|
||||
message AccountConnection {
|
||||
string id = 1;
|
||||
string provider = 2;
|
||||
string provided_identifier = 3;
|
||||
map<string, google.protobuf.Value> meta = 4;
|
||||
google.protobuf.StringValue access_token = 5; // Omitted from JSON serialization
|
||||
google.protobuf.StringValue refresh_token = 6; // Omitted from JSON serialization
|
||||
google.protobuf.Timestamp last_used_at = 7;
|
||||
string account_id = 8;
|
||||
string id = 1;
|
||||
string provider = 2;
|
||||
string provided_identifier = 3;
|
||||
map<string, google.protobuf.Value> meta = 4;
|
||||
google.protobuf.StringValue access_token = 5; // Omitted from JSON serialization
|
||||
google.protobuf.StringValue refresh_token = 6; // Omitted from JSON serialization
|
||||
google.protobuf.Timestamp last_used_at = 7;
|
||||
string account_id = 8;
|
||||
|
||||
google.protobuf.Timestamp created_at = 9;
|
||||
google.protobuf.Timestamp updated_at = 10;
|
||||
google.protobuf.Timestamp created_at = 9;
|
||||
google.protobuf.Timestamp updated_at = 10;
|
||||
}
|
||||
|
||||
// VerificationMark represents verification status
|
||||
message VerificationMark {
|
||||
VerificationMarkType type = 1;
|
||||
string title = 2;
|
||||
string description = 3;
|
||||
string verified_by = 4;
|
||||
VerificationMarkType type = 1;
|
||||
string title = 2;
|
||||
string description = 3;
|
||||
string verified_by = 4;
|
||||
|
||||
google.protobuf.Timestamp created_at = 5;
|
||||
google.protobuf.Timestamp updated_at = 6;
|
||||
google.protobuf.Timestamp created_at = 5;
|
||||
google.protobuf.Timestamp updated_at = 6;
|
||||
}
|
||||
|
||||
enum VerificationMarkType {
|
||||
VERIFICATION_MARK_TYPE_UNSPECIFIED = 0;
|
||||
OFFICIAL = 1;
|
||||
INDIVIDUAL = 2;
|
||||
ORGANIZATION = 3;
|
||||
GOVERNMENT = 4;
|
||||
CREATOR = 5;
|
||||
DEVELOPER = 6;
|
||||
PARODY = 7;
|
||||
VERIFICATION_MARK_TYPE_UNSPECIFIED = 0;
|
||||
OFFICIAL = 1;
|
||||
INDIVIDUAL = 2;
|
||||
ORGANIZATION = 3;
|
||||
GOVERNMENT = 4;
|
||||
CREATOR = 5;
|
||||
DEVELOPER = 6;
|
||||
PARODY = 7;
|
||||
}
|
||||
|
||||
// BadgeReferenceObject represents a reference to a badge with minimal information
|
||||
message BadgeReferenceObject {
|
||||
string id = 1; // Unique identifier for the badge
|
||||
string type = 2; // Type/category of the badge
|
||||
google.protobuf.StringValue label = 3; // Display name of the badge
|
||||
google.protobuf.StringValue caption = 4; // Optional description of the badge
|
||||
map<string, google.protobuf.Value> meta = 5; // Additional metadata for the badge
|
||||
google.protobuf.Timestamp activated_at = 6; // When the badge was activated
|
||||
google.protobuf.Timestamp expired_at = 7; // Optional expiration time
|
||||
string account_id = 8; // ID of the account this badge belongs to
|
||||
string id = 1; // Unique identifier for the badge
|
||||
string type = 2; // Type/category of the badge
|
||||
google.protobuf.StringValue label = 3; // Display name of the badge
|
||||
google.protobuf.StringValue caption = 4; // Optional description of the badge
|
||||
map<string, google.protobuf.Value> meta = 5; // Additional metadata for the badge
|
||||
google.protobuf.Timestamp activated_at = 6; // When the badge was activated
|
||||
google.protobuf.Timestamp expired_at = 7; // Optional expiration time
|
||||
string account_id = 8; // ID of the account this badge belongs to
|
||||
}
|
||||
|
||||
// Relationship represents a connection between two accounts
|
||||
message Relationship {
|
||||
string account_id = 1;
|
||||
string related_id = 2;
|
||||
optional Account account = 3;
|
||||
optional Account related = 4;
|
||||
int32 status = 5;
|
||||
string account_id = 1;
|
||||
string related_id = 2;
|
||||
optional Account account = 3;
|
||||
optional Account related = 4;
|
||||
int32 status = 5;
|
||||
|
||||
google.protobuf.Timestamp created_at = 6;
|
||||
google.protobuf.Timestamp updated_at = 7;
|
||||
google.protobuf.Timestamp created_at = 6;
|
||||
google.protobuf.Timestamp updated_at = 7;
|
||||
}
|
||||
|
||||
// Leveling information
|
||||
message LevelingInfo {
|
||||
int32 current_level = 1;
|
||||
int32 current_experience = 2;
|
||||
int32 next_level_experience = 3;
|
||||
int32 previous_level_experience = 4;
|
||||
double level_progress = 5;
|
||||
repeated int32 experience_per_level = 6;
|
||||
int32 current_level = 1;
|
||||
int32 current_experience = 2;
|
||||
int32 next_level_experience = 3;
|
||||
int32 previous_level_experience = 4;
|
||||
double level_progress = 5;
|
||||
repeated int32 experience_per_level = 6;
|
||||
}
|
||||
|
||||
// ActionLog represents a record of an action taken by a user
|
||||
message ActionLog {
|
||||
string id = 1; // Unique identifier for the log entry
|
||||
string action = 2; // The action that was performed, e.g., "user.login"
|
||||
map<string, google.protobuf.Value> meta = 3; // Metadata associated with the action
|
||||
google.protobuf.StringValue user_agent = 4; // User agent of the client
|
||||
google.protobuf.StringValue ip_address = 5; // IP address of the client
|
||||
google.protobuf.StringValue location = 6; // Geographic location of the client, derived from IP
|
||||
string account_id = 7; // The account that performed the action
|
||||
google.protobuf.StringValue session_id = 8; // The session in which the action was performed
|
||||
string id = 1; // Unique identifier for the log entry
|
||||
string action = 2; // The action that was performed, e.g., "user.login"
|
||||
map<string, google.protobuf.Value> meta = 3; // Metadata associated with the action
|
||||
google.protobuf.StringValue user_agent = 4; // User agent of the client
|
||||
google.protobuf.StringValue ip_address = 5; // IP address of the client
|
||||
google.protobuf.StringValue location = 6; // Geographic location of the client, derived from IP
|
||||
string account_id = 7; // The account that performed the action
|
||||
google.protobuf.StringValue session_id = 8; // The session in which the action was performed
|
||||
|
||||
google.protobuf.Timestamp created_at = 9; // When the action log was created
|
||||
google.protobuf.Timestamp created_at = 9; // When the action log was created
|
||||
}
|
||||
|
||||
message GetAccountStatusBatchResponse {
|
||||
repeated AccountStatus statuses = 1;
|
||||
repeated AccountStatus statuses = 1;
|
||||
}
|
||||
|
||||
// ====================================
|
||||
@@ -248,45 +256,46 @@ message GetAccountStatusBatchResponse {
|
||||
|
||||
// AccountService provides CRUD operations for user accounts and related entities
|
||||
service AccountService {
|
||||
// Account Operations
|
||||
rpc GetAccount(GetAccountRequest) returns (Account) {}
|
||||
rpc GetBotAccount(GetBotAccountRequest) returns (Account) {}
|
||||
rpc GetAccountBatch(GetAccountBatchRequest) returns (GetAccountBatchResponse) {}
|
||||
rpc GetBotAccountBatch(GetBotAccountBatchRequest) returns (GetAccountBatchResponse) {}
|
||||
rpc LookupAccountBatch(LookupAccountBatchRequest) returns (GetAccountBatchResponse) {}
|
||||
rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse) {}
|
||||
// Account Operations
|
||||
rpc GetAccount(GetAccountRequest) returns (Account) {}
|
||||
rpc GetBotAccount(GetBotAccountRequest) returns (Account) {}
|
||||
rpc GetAccountBatch(GetAccountBatchRequest) returns (GetAccountBatchResponse) {}
|
||||
rpc GetBotAccountBatch(GetBotAccountBatchRequest) returns (GetAccountBatchResponse) {}
|
||||
rpc LookupAccountBatch(LookupAccountBatchRequest) returns (GetAccountBatchResponse) {}
|
||||
rpc SearchAccount(SearchAccountRequest) returns (GetAccountBatchResponse) {}
|
||||
rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse) {}
|
||||
|
||||
rpc GetAccountStatus(GetAccountRequest) returns (AccountStatus) {}
|
||||
rpc GetAccountStatusBatch(GetAccountBatchRequest) returns (GetAccountStatusBatchResponse) {}
|
||||
rpc GetAccountStatus(GetAccountRequest) returns (AccountStatus) {}
|
||||
rpc GetAccountStatusBatch(GetAccountBatchRequest) returns (GetAccountStatusBatchResponse) {}
|
||||
|
||||
// Profile Operations
|
||||
rpc GetProfile(GetProfileRequest) returns (AccountProfile) {}
|
||||
// Profile Operations
|
||||
rpc GetProfile(GetProfileRequest) returns (AccountProfile) {}
|
||||
|
||||
// Contact Operations
|
||||
rpc ListContacts(ListContactsRequest) returns (ListContactsResponse) {}
|
||||
// Contact Operations
|
||||
rpc ListContacts(ListContactsRequest) returns (ListContactsResponse) {}
|
||||
|
||||
// Badge Operations
|
||||
rpc ListBadges(ListBadgesRequest) returns (ListBadgesResponse) {}
|
||||
// Badge Operations
|
||||
rpc ListBadges(ListBadgesRequest) returns (ListBadgesResponse) {}
|
||||
|
||||
// Authentication Factor Operations
|
||||
rpc ListAuthFactors(ListAuthFactorsRequest) returns (ListAuthFactorsResponse) {}
|
||||
// Authentication Factor Operations
|
||||
rpc ListAuthFactors(ListAuthFactorsRequest) returns (ListAuthFactorsResponse) {}
|
||||
|
||||
// Connection Operations
|
||||
rpc ListConnections(ListConnectionsRequest) returns (ListConnectionsResponse) {}
|
||||
// Connection Operations
|
||||
rpc ListConnections(ListConnectionsRequest) returns (ListConnectionsResponse) {}
|
||||
|
||||
// Relationship Operations
|
||||
rpc ListRelationships(ListRelationshipsRequest) returns (ListRelationshipsResponse) {}
|
||||
// Relationship Operations
|
||||
rpc ListRelationships(ListRelationshipsRequest) returns (ListRelationshipsResponse) {}
|
||||
|
||||
rpc GetRelationship(GetRelationshipRequest) returns (GetRelationshipResponse) {}
|
||||
rpc HasRelationship(GetRelationshipRequest) returns (google.protobuf.BoolValue) {}
|
||||
rpc ListFriends(ListRelationshipSimpleRequest) returns (ListRelationshipSimpleResponse) {}
|
||||
rpc ListBlocked(ListRelationshipSimpleRequest) returns (ListRelationshipSimpleResponse) {}
|
||||
rpc GetRelationship(GetRelationshipRequest) returns (GetRelationshipResponse) {}
|
||||
rpc HasRelationship(GetRelationshipRequest) returns (google.protobuf.BoolValue) {}
|
||||
rpc ListFriends(ListRelationshipSimpleRequest) returns (ListRelationshipSimpleResponse) {}
|
||||
rpc ListBlocked(ListRelationshipSimpleRequest) returns (ListRelationshipSimpleResponse) {}
|
||||
}
|
||||
|
||||
// ActionLogService provides operations for action logs
|
||||
service ActionLogService {
|
||||
rpc CreateActionLog(CreateActionLogRequest) returns (CreateActionLogResponse) {}
|
||||
rpc ListActionLogs(ListActionLogsRequest) returns (ListActionLogsResponse) {}
|
||||
rpc CreateActionLog(CreateActionLogRequest) returns (CreateActionLogResponse) {}
|
||||
rpc ListActionLogs(ListActionLogsRequest) returns (ListActionLogsResponse) {}
|
||||
}
|
||||
|
||||
// ====================================
|
||||
@@ -295,184 +304,188 @@ service ActionLogService {
|
||||
|
||||
// ActionLog Requests/Responses
|
||||
message CreateActionLogRequest {
|
||||
string action = 1;
|
||||
map<string, google.protobuf.Value> meta = 2;
|
||||
google.protobuf.StringValue user_agent = 3;
|
||||
google.protobuf.StringValue ip_address = 4;
|
||||
google.protobuf.StringValue location = 5;
|
||||
string account_id = 6;
|
||||
google.protobuf.StringValue session_id = 7;
|
||||
string action = 1;
|
||||
map<string, google.protobuf.Value> meta = 2;
|
||||
google.protobuf.StringValue user_agent = 3;
|
||||
google.protobuf.StringValue ip_address = 4;
|
||||
google.protobuf.StringValue location = 5;
|
||||
string account_id = 6;
|
||||
google.protobuf.StringValue session_id = 7;
|
||||
}
|
||||
|
||||
message CreateActionLogResponse {
|
||||
ActionLog action_log = 1;
|
||||
ActionLog action_log = 1;
|
||||
}
|
||||
|
||||
message ListActionLogsRequest {
|
||||
string account_id = 1;
|
||||
string action = 2;
|
||||
int32 page_size = 3;
|
||||
string page_token = 4;
|
||||
string order_by = 5;
|
||||
string account_id = 1;
|
||||
string action = 2;
|
||||
int32 page_size = 3;
|
||||
string page_token = 4;
|
||||
string order_by = 5;
|
||||
}
|
||||
|
||||
message ListActionLogsResponse {
|
||||
repeated ActionLog action_logs = 1;
|
||||
string next_page_token = 2;
|
||||
int32 total_size = 3;
|
||||
repeated ActionLog action_logs = 1;
|
||||
string next_page_token = 2;
|
||||
int32 total_size = 3;
|
||||
}
|
||||
|
||||
// Account Requests/Responses
|
||||
message GetAccountRequest {
|
||||
string id = 1; // Account ID to retrieve
|
||||
string id = 1; // Account ID to retrieve
|
||||
}
|
||||
|
||||
message GetBotAccountRequest {
|
||||
string automated_id = 1;
|
||||
string automated_id = 1;
|
||||
}
|
||||
|
||||
message GetAccountBatchRequest {
|
||||
repeated string id = 1; // Account ID to retrieve
|
||||
repeated string id = 1; // Account ID to retrieve
|
||||
}
|
||||
|
||||
message GetBotAccountBatchRequest {
|
||||
repeated string automated_id = 1;
|
||||
repeated string automated_id = 1;
|
||||
}
|
||||
|
||||
message LookupAccountBatchRequest {
|
||||
repeated string names = 1;
|
||||
repeated string names = 1;
|
||||
}
|
||||
|
||||
message SearchAccountRequest {
|
||||
string query = 1;
|
||||
}
|
||||
|
||||
message GetAccountBatchResponse {
|
||||
repeated Account accounts = 1; // List of accounts
|
||||
repeated Account accounts = 1; // List of accounts
|
||||
}
|
||||
|
||||
message CreateAccountRequest {
|
||||
string name = 1; // Required: Unique username
|
||||
string nick = 2; // Optional: Display name
|
||||
string language = 3; // Default language
|
||||
bool is_superuser = 4; // Admin flag
|
||||
AccountProfile profile = 5; // Initial profile data
|
||||
string name = 1; // Required: Unique username
|
||||
string nick = 2; // Optional: Display name
|
||||
string language = 3; // Default language
|
||||
bool is_superuser = 4; // Admin flag
|
||||
AccountProfile profile = 5; // Initial profile data
|
||||
}
|
||||
|
||||
message UpdateAccountRequest {
|
||||
string id = 1; // Account ID to update
|
||||
google.protobuf.StringValue name = 2; // New username if changing
|
||||
google.protobuf.StringValue nick = 3; // New display name
|
||||
google.protobuf.StringValue language = 4; // New language
|
||||
google.protobuf.BoolValue is_superuser = 5; // Admin status
|
||||
string id = 1; // Account ID to update
|
||||
google.protobuf.StringValue name = 2; // New username if changing
|
||||
google.protobuf.StringValue nick = 3; // New display name
|
||||
google.protobuf.StringValue language = 4; // New language
|
||||
google.protobuf.BoolValue is_superuser = 5; // Admin status
|
||||
}
|
||||
|
||||
message DeleteAccountRequest {
|
||||
string id = 1; // Account ID to delete
|
||||
bool purge = 2; // If true, permanently delete instead of soft delete
|
||||
string id = 1; // Account ID to delete
|
||||
bool purge = 2; // If true, permanently delete instead of soft delete
|
||||
}
|
||||
|
||||
message ListAccountsRequest {
|
||||
int32 page_size = 1; // Number of results per page
|
||||
string page_token = 2; // Token for pagination
|
||||
string filter = 3; // Filter expression
|
||||
string order_by = 4; // Sort order
|
||||
int32 page_size = 1; // Number of results per page
|
||||
string page_token = 2; // Token for pagination
|
||||
string filter = 3; // Filter expression
|
||||
string order_by = 4; // Sort order
|
||||
}
|
||||
|
||||
message ListAccountsResponse {
|
||||
repeated Account accounts = 1; // List of accounts
|
||||
string next_page_token = 2; // Token for next page
|
||||
int32 total_size = 3; // Total number of accounts
|
||||
repeated Account accounts = 1; // List of accounts
|
||||
string next_page_token = 2; // Token for next page
|
||||
int32 total_size = 3; // Total number of accounts
|
||||
}
|
||||
|
||||
// Profile Requests/Responses
|
||||
message GetProfileRequest {
|
||||
string account_id = 1; // Account ID to get profile for
|
||||
string account_id = 1; // Account ID to get profile for
|
||||
}
|
||||
|
||||
message UpdateProfileRequest {
|
||||
string account_id = 1; // Account ID to update profile for
|
||||
AccountProfile profile = 2; // Profile data to update
|
||||
google.protobuf.FieldMask update_mask = 3; // Fields to update
|
||||
string account_id = 1; // Account ID to update profile for
|
||||
AccountProfile profile = 2; // Profile data to update
|
||||
google.protobuf.FieldMask update_mask = 3; // Fields to update
|
||||
}
|
||||
|
||||
// Contact Requests/Responses
|
||||
message AddContactRequest {
|
||||
string account_id = 1; // Account to add contact to
|
||||
AccountContactType type = 2; // Type of contact
|
||||
string content = 3; // Contact content (email, phone, etc.)
|
||||
bool is_primary = 4; // If this should be the primary contact
|
||||
string account_id = 1; // Account to add contact to
|
||||
AccountContactType type = 2; // Type of contact
|
||||
string content = 3; // Contact content (email, phone, etc.)
|
||||
bool is_primary = 4; // If this should be the primary contact
|
||||
}
|
||||
|
||||
message ListContactsRequest {
|
||||
string account_id = 1; // Account ID to list contacts for
|
||||
AccountContactType type = 2; // Optional: filter by type
|
||||
bool verified_only = 3; // Only return verified contacts
|
||||
string account_id = 1; // Account ID to list contacts for
|
||||
AccountContactType type = 2; // Optional: filter by type
|
||||
bool verified_only = 3; // Only return verified contacts
|
||||
}
|
||||
|
||||
message ListContactsResponse {
|
||||
repeated AccountContact contacts = 1; // List of contacts
|
||||
repeated AccountContact contacts = 1; // List of contacts
|
||||
}
|
||||
|
||||
message VerifyContactRequest {
|
||||
string id = 1; // Contact ID to verify
|
||||
string account_id = 2; // Account ID (for validation)
|
||||
string code = 3; // Verification code
|
||||
string id = 1; // Contact ID to verify
|
||||
string account_id = 2; // Account ID (for validation)
|
||||
string code = 3; // Verification code
|
||||
}
|
||||
|
||||
// Badge Requests/Responses
|
||||
message ListBadgesRequest {
|
||||
string account_id = 1; // Account to list badges for
|
||||
string type = 2; // Optional: filter by type
|
||||
bool active_only = 3; // Only return active (non-expired) badges
|
||||
string account_id = 1; // Account to list badges for
|
||||
string type = 2; // Optional: filter by type
|
||||
bool active_only = 3; // Only return active (non-expired) badges
|
||||
}
|
||||
|
||||
message ListBadgesResponse {
|
||||
repeated AccountBadge badges = 1; // List of badges
|
||||
repeated AccountBadge badges = 1; // List of badges
|
||||
}
|
||||
|
||||
message ListAuthFactorsRequest {
|
||||
string account_id = 1; // Account to list factors for
|
||||
bool active_only = 2; // Only return active (non-expired) factors
|
||||
string account_id = 1; // Account to list factors for
|
||||
bool active_only = 2; // Only return active (non-expired) factors
|
||||
}
|
||||
|
||||
message ListAuthFactorsResponse {
|
||||
repeated AccountAuthFactor factors = 1; // List of auth factors
|
||||
repeated AccountAuthFactor factors = 1; // List of auth factors
|
||||
}
|
||||
|
||||
message ListConnectionsRequest {
|
||||
string account_id = 1; // Account to list connections for
|
||||
string provider = 2; // Optional: filter by provider
|
||||
string account_id = 1; // Account to list connections for
|
||||
string provider = 2; // Optional: filter by provider
|
||||
}
|
||||
|
||||
message ListConnectionsResponse {
|
||||
repeated AccountConnection connections = 1; // List of connections
|
||||
repeated AccountConnection connections = 1; // List of connections
|
||||
}
|
||||
|
||||
// Relationship Requests/Responses
|
||||
message ListRelationshipsRequest {
|
||||
string account_id = 1; // Account to list relationships for
|
||||
optional int32 status = 2; // Filter by status
|
||||
int32 page_size = 5; // Number of results per page
|
||||
string page_token = 6; // Token for pagination
|
||||
string account_id = 1; // Account to list relationships for
|
||||
optional int32 status = 2; // Filter by status
|
||||
int32 page_size = 5; // Number of results per page
|
||||
string page_token = 6; // Token for pagination
|
||||
}
|
||||
|
||||
message ListRelationshipsResponse {
|
||||
repeated Relationship relationships = 1; // List of relationships
|
||||
string next_page_token = 2; // Token for next page
|
||||
int32 total_size = 3; // Total number of relationships
|
||||
repeated Relationship relationships = 1; // List of relationships
|
||||
string next_page_token = 2; // Token for next page
|
||||
int32 total_size = 3; // Total number of relationships
|
||||
}
|
||||
|
||||
message GetRelationshipRequest {
|
||||
string account_id = 1;
|
||||
string related_id = 2;
|
||||
optional int32 status = 3;
|
||||
string account_id = 1;
|
||||
string related_id = 2;
|
||||
optional int32 status = 3;
|
||||
}
|
||||
|
||||
message GetRelationshipResponse {
|
||||
optional Relationship relationship = 1;
|
||||
optional Relationship relationship = 1;
|
||||
}
|
||||
|
||||
message ListRelationshipSimpleRequest {
|
||||
string account_id = 1;
|
||||
string account_id = 1;
|
||||
}
|
||||
|
||||
message ListRelationshipSimpleResponse {
|
||||
repeated string accounts_id = 1;
|
||||
repeated string accounts_id = 1;
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ public class AccountClientHelper(AccountService.AccountServiceClient accounts)
|
||||
var response = await accounts.GetAccountAsync(request);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
public async Task<Account> GetBotAccount(Guid automatedId)
|
||||
{
|
||||
var request = new GetBotAccountRequest { AutomatedId = automatedId.ToString() };
|
||||
@@ -26,7 +26,14 @@ public class AccountClientHelper(AccountService.AccountServiceClient accounts)
|
||||
var response = await accounts.GetAccountBatchAsync(request);
|
||||
return response.Accounts.ToList();
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<Account>> SearchAccounts(string query)
|
||||
{
|
||||
var request = new SearchAccountRequest { Query = query };
|
||||
var response = await accounts.SearchAccountAsync(request);
|
||||
return response.Accounts.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<Account>> GetBotAccountBatch(List<Guid> automatedIds)
|
||||
{
|
||||
var request = new GetBotAccountBatchRequest();
|
||||
|
@@ -0,0 +1,20 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DysonNetwork.Sphere.Autocompletion;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/autocomplete")]
|
||||
public class AutocompletionController(AutocompletionService aus) : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<List<DysonNetwork.Shared.Models.Autocompletion>>> TextAutocomplete([FromBody] AutocompletionRequest request, Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var result = await aus.GetAutocompletion(request.Content, chatId: roomId, limit: 10);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
146
DysonNetwork.Sphere/Autocompletion/AutocompletionService.cs
Normal file
146
DysonNetwork.Sphere/Autocompletion/AutocompletionService.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Sphere.Autocompletion;
|
||||
|
||||
public class AutocompletionService(AppDatabase db, AccountClientHelper accountsHelper)
|
||||
{
|
||||
public async Task<List<DysonNetwork.Shared.Models.Autocompletion>> GetAutocompletion(string content, Guid? chatId = null, Guid? realmId = null, int limit = 10)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
return [];
|
||||
|
||||
if (content.StartsWith('@'))
|
||||
{
|
||||
var afterAt = content[1..];
|
||||
string type;
|
||||
string query;
|
||||
var hadSlash = afterAt.Contains('/');
|
||||
if (hadSlash)
|
||||
{
|
||||
var parts = afterAt.Split('/', 2);
|
||||
type = parts[0];
|
||||
query = parts.Length > 1 ? parts[1] : "";
|
||||
}
|
||||
else
|
||||
{
|
||||
type = "u";
|
||||
query = afterAt;
|
||||
}
|
||||
|
||||
return await AutocompleteAt(type, query, chatId, realmId, hadSlash, limit);
|
||||
}
|
||||
|
||||
if (!content.StartsWith(':')) return [];
|
||||
{
|
||||
var query = content[1..];
|
||||
return await AutocompleteSticker(query, limit);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<DysonNetwork.Shared.Models.Autocompletion>> AutocompleteAt(string type, string query, Guid? chatId, Guid? realmId, bool hadSlash,
|
||||
int limit)
|
||||
{
|
||||
var results = new List<DysonNetwork.Shared.Models.Autocompletion>();
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "u":
|
||||
var allAccounts = await accountsHelper.SearchAccounts(query);
|
||||
var filteredAccounts = allAccounts;
|
||||
|
||||
if (chatId.HasValue)
|
||||
{
|
||||
var chatMemberIds = await db.ChatMembers
|
||||
.Where(m => m.ChatRoomId == chatId.Value && m.JoinedAt != null && m.LeaveAt == null)
|
||||
.Select(m => m.AccountId)
|
||||
.ToListAsync();
|
||||
var chatMemberIdStrings = chatMemberIds.Select(id => id.ToString()).ToHashSet();
|
||||
filteredAccounts = allAccounts.Where(a => chatMemberIdStrings.Contains(a.Id)).ToList();
|
||||
}
|
||||
else if (realmId.HasValue)
|
||||
{
|
||||
var realmMemberIds = await db.RealmMembers
|
||||
.Where(m => m.RealmId == realmId.Value && m.LeaveAt == null)
|
||||
.Select(m => m.AccountId)
|
||||
.ToListAsync();
|
||||
var realmMemberIdStrings = realmMemberIds.Select(id => id.ToString()).ToHashSet();
|
||||
filteredAccounts = allAccounts.Where(a => realmMemberIdStrings.Contains(a.Id)).ToList();
|
||||
}
|
||||
|
||||
var users = filteredAccounts
|
||||
.Take(limit)
|
||||
.Select(a => new DysonNetwork.Shared.Models.Autocompletion
|
||||
{
|
||||
Type = "user",
|
||||
Keyword = "@" + (hadSlash ? "u/" : "") + a.Name,
|
||||
Data = SnAccount.FromProtoValue(a)
|
||||
})
|
||||
.ToList();
|
||||
results.AddRange(users);
|
||||
break;
|
||||
case "p":
|
||||
var publishers = await db.Publishers
|
||||
.Where(p => EF.Functions.Like(p.Name, $"{query}%") || EF.Functions.Like(p.Nick, $"{query}%"))
|
||||
.Take(limit)
|
||||
.Select(p => new DysonNetwork.Shared.Models.Autocompletion
|
||||
{
|
||||
Type = "publisher",
|
||||
Keyword = "@p/" + p.Name,
|
||||
Data = p
|
||||
})
|
||||
.ToListAsync();
|
||||
results.AddRange(publishers);
|
||||
break;
|
||||
|
||||
case "r":
|
||||
var realms = await db.Realms
|
||||
.Where(r => EF.Functions.Like(r.Slug, $"{query}%") || EF.Functions.Like(r.Name, $"{query}%"))
|
||||
.Take(limit)
|
||||
.Select(r => new DysonNetwork.Shared.Models.Autocompletion
|
||||
{
|
||||
Type = "realm",
|
||||
Keyword = "@r/" + r.Slug,
|
||||
Data = r
|
||||
})
|
||||
.ToListAsync();
|
||||
results.AddRange(realms);
|
||||
break;
|
||||
|
||||
case "c":
|
||||
var chats = await db.ChatRooms
|
||||
.Where(c => c.Name != null && EF.Functions.Like(c.Name, $"{query}%"))
|
||||
.Take(limit)
|
||||
.Select(c => new DysonNetwork.Shared.Models.Autocompletion
|
||||
{
|
||||
Type = "chat",
|
||||
Keyword = "@c/" + c.Name,
|
||||
Data = c
|
||||
})
|
||||
.ToListAsync();
|
||||
results.AddRange(chats);
|
||||
break;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private async Task<List<DysonNetwork.Shared.Models.Autocompletion>> AutocompleteSticker(string query, int limit)
|
||||
{
|
||||
var stickers = await db.Stickers
|
||||
.Include(s => s.Pack)
|
||||
.Where(s => EF.Functions.Like(s.Pack.Prefix + "+" + s.Slug, $"{query}%"))
|
||||
.Take(limit)
|
||||
.Select(s => new DysonNetwork.Shared.Models.Autocompletion
|
||||
{
|
||||
Type = "sticker",
|
||||
Keyword = $":{s.Pack.Prefix}+{s.Slug}:",
|
||||
Data = s
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var results = stickers.ToList();
|
||||
return results;
|
||||
}
|
||||
}
|
@@ -4,6 +4,7 @@ using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Sphere.Autocompletion;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -17,7 +18,8 @@ public partial class ChatController(
|
||||
ChatService cs,
|
||||
ChatRoomService crs,
|
||||
FileService.FileServiceClient files,
|
||||
AccountService.AccountServiceClient accounts
|
||||
AccountService.AccountServiceClient accounts,
|
||||
AutocompletionService aus
|
||||
) : ControllerBase
|
||||
{
|
||||
public class MarkMessageReadRequest
|
||||
@@ -85,7 +87,8 @@ public partial class ChatController(
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
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();
|
||||
if (member == null || member.Role < ChatMemberRole.Member)
|
||||
return StatusCode(403, "You are not a member of this chat room.");
|
||||
@@ -101,10 +104,10 @@ public partial class ChatController(
|
||||
.Skip(offset)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
var members = messages.Select(m => m.Sender).DistinctBy(x => x.Id).ToList();
|
||||
members = await crs.LoadMemberAccounts(members);
|
||||
|
||||
|
||||
foreach (var message in messages)
|
||||
message.Sender = members.First(x => x.Id == message.SenderId);
|
||||
|
||||
@@ -127,7 +130,8 @@ public partial class ChatController(
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
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();
|
||||
if (member == null || member.Role < ChatMemberRole.Member)
|
||||
return StatusCode(403, "You are not a member of this chat room.");
|
||||
@@ -139,16 +143,81 @@ public partial class ChatController(
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (message is null) return NotFound();
|
||||
|
||||
|
||||
message.Sender = await crs.LoadMemberAccount(message.Sender);
|
||||
|
||||
return Ok(message);
|
||||
}
|
||||
|
||||
|
||||
[GeneratedRegex("@([A-Za-z0-9_-]+)")]
|
||||
[GeneratedRegex(@"@(?:u/)?([A-Za-z0-9_-]+)")]
|
||||
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")]
|
||||
[Authorize]
|
||||
[RequiredPermission("global", "chat.messages.create")]
|
||||
@@ -186,6 +255,7 @@ public partial class ChatController(
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Validate reply and forward message IDs exist
|
||||
if (request.RepliedMessageId.HasValue)
|
||||
{
|
||||
var repliedMessage = await db.ChatMessages
|
||||
@@ -206,28 +276,9 @@ public partial class ChatController(
|
||||
message.ForwardedMessageId = forwardedMessage.Id;
|
||||
}
|
||||
|
||||
if (request.Content is not null)
|
||||
{
|
||||
var mentioned = MentionRegex()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
// Extract mentioned users
|
||||
message.MembersMentioned = await ExtractMentionedUsersAsync(request.Content, request.RepliedMessageId,
|
||||
request.ForwardedMessageId, roomId);
|
||||
|
||||
var result = await cs.SendMessageAsync(message, member, member.ChatRoom);
|
||||
|
||||
@@ -257,6 +308,7 @@ public partial class ChatController(
|
||||
(request.AttachmentsId == null || request.AttachmentsId.Count == 0))
|
||||
return BadRequest("You cannot send an empty message.");
|
||||
|
||||
// Validate reply and forward message IDs exist
|
||||
if (request.RepliedMessageId.HasValue)
|
||||
{
|
||||
var repliedMessage = await db.ChatMessages
|
||||
@@ -273,6 +325,11 @@ public partial class ChatController(
|
||||
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
|
||||
await cs.UpdateMessageAsync(
|
||||
message,
|
||||
@@ -322,11 +379,30 @@ public partial class ChatController(
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
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)
|
||||
return StatusCode(403, "You are not a member of this chat room.");
|
||||
|
||||
var response = await cs.GetSyncDataAsync(roomId, request.LastSyncTimestamp);
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpPost("{roomId:guid}/autocomplete")]
|
||||
public async Task<ActionResult<List<DysonNetwork.Shared.Models.Autocompletion>>> ChatAutoComplete(
|
||||
[FromBody] AutocompletionRequest request, Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
var isMember = await db.ChatMembers
|
||||
.AnyAsync(m =>
|
||||
m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null);
|
||||
if (!isMember)
|
||||
return StatusCode(403, "You are not a member of this chat room.");
|
||||
|
||||
var result = await aus.GetAutocompletion(request.Content, chatId: roomId, limit: 10);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
@@ -45,7 +45,8 @@ public class ChatRoomService(
|
||||
if (member is not null) return member;
|
||||
|
||||
member = await db.ChatMembers
|
||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == chatRoomId && m.JoinedAt != null && m.LeaveAt == null)
|
||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == chatRoomId && m.JoinedAt != null &&
|
||||
m.LeaveAt == null)
|
||||
.Include(m => m.ChatRoom)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
@@ -95,7 +96,7 @@ public class ChatRoomService(
|
||||
? await db.ChatMembers
|
||||
.Where(m => directRoomsId.Contains(m.ChatRoomId))
|
||||
.Where(m => m.AccountId != userId)
|
||||
.Where(m => m.JoinedAt != null && m.LeaveAt == null)
|
||||
// Ignored the joined at condition here to keep showing userinfo when other didn't accept the invite of DM
|
||||
.ToListAsync()
|
||||
: [];
|
||||
members = await LoadMemberAccounts(members);
|
||||
@@ -156,12 +157,15 @@ public class ChatRoomService(
|
||||
var accountIds = members.Select(m => m.AccountId).ToList();
|
||||
var accounts = (await accountsHelper.GetAccountBatch(accountIds)).ToDictionary(a => Guid.Parse(a.Id), a => a);
|
||||
|
||||
return [.. members.Select(m =>
|
||||
{
|
||||
if (accounts.TryGetValue(m.AccountId, out var account))
|
||||
m.Account = SnAccount.FromProtoValue(account);
|
||||
return m;
|
||||
})];
|
||||
return
|
||||
[
|
||||
.. members.Select(m =>
|
||||
{
|
||||
if (accounts.TryGetValue(m.AccountId, out var account))
|
||||
m.Account = SnAccount.FromProtoValue(account);
|
||||
return m;
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
private const string ChatRoomSubscribeKeyPrefix = "chatroom:subscribe:";
|
||||
@@ -192,4 +196,4 @@ public class ChatRoomService(
|
||||
var keys = await cache.GetGroupKeysAsync(group);
|
||||
return keys.Select(k => Guid.Parse(k.Split(':').Last())).ToList();
|
||||
}
|
||||
}
|
||||
}
|
@@ -198,8 +198,6 @@ public partial class ChatService(
|
||||
public async Task<SnChatMessage> SendMessageAsync(SnChatMessage message, SnChatMember sender, SnChatRoom room)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message.Nonce)) message.Nonce = Guid.NewGuid().ToString();
|
||||
message.CreatedAt = SystemClock.Instance.GetCurrentInstant();
|
||||
message.UpdatedAt = message.CreatedAt;
|
||||
|
||||
// First complete the save operation
|
||||
db.ChatMessages.Add(message);
|
||||
@@ -209,20 +207,25 @@ public partial class ChatService(
|
||||
await CreateFileReferencesForMessageAsync(message);
|
||||
|
||||
// Then start the delivery process
|
||||
var localMessage = message;
|
||||
var localSender = sender;
|
||||
var localRoom = room;
|
||||
var localLogger = logger;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await DeliverMessageAsync(message, sender, room);
|
||||
await DeliverMessageAsync(localMessage, localSender, localRoom);
|
||||
}
|
||||
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
|
||||
_ = Task.Run(async () => await ProcessMessageLinkPreviewAsync(message));
|
||||
var localMessageForPreview = message;
|
||||
_ = Task.Run(async () => await ProcessMessageLinkPreviewAsync(localMessageForPreview));
|
||||
|
||||
message.Sender = sender;
|
||||
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.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using DysonNetwork.Sphere.Poll;
|
||||
using DysonNetwork.Sphere.Realm;
|
||||
using DysonNetwork.Sphere.WebReader;
|
||||
@@ -23,6 +24,7 @@ public class PostController(
|
||||
AppDatabase db,
|
||||
PostService ps,
|
||||
PublisherService pub,
|
||||
AccountClientHelper accountsHelper,
|
||||
AccountService.AccountServiceClient accounts,
|
||||
ActionLogService.ActionLogServiceClient als,
|
||||
PaymentService.PaymentServiceClient payments,
|
||||
@@ -97,7 +99,7 @@ public class PostController(
|
||||
if (currentUser != null)
|
||||
{
|
||||
var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
|
||||
{ AccountId = currentUser.Id });
|
||||
{ AccountId = currentUser.Id });
|
||||
userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
|
||||
}
|
||||
|
||||
@@ -197,7 +199,7 @@ public class PostController(
|
||||
if (currentUser != null)
|
||||
{
|
||||
var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
|
||||
{ AccountId = currentUser.Id });
|
||||
{ AccountId = currentUser.Id });
|
||||
userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
|
||||
}
|
||||
|
||||
@@ -228,7 +230,7 @@ public class PostController(
|
||||
if (currentUser != null)
|
||||
{
|
||||
var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
|
||||
{ AccountId = currentUser.Id });
|
||||
{ AccountId = currentUser.Id });
|
||||
userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
|
||||
}
|
||||
|
||||
@@ -271,6 +273,14 @@ public class PostController(
|
||||
.Take(take)
|
||||
.Skip(offset)
|
||||
.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);
|
||||
}
|
||||
|
||||
@@ -283,7 +293,7 @@ public class PostController(
|
||||
if (currentUser != null)
|
||||
{
|
||||
var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
|
||||
{ AccountId = currentUser.Id });
|
||||
{ AccountId = currentUser.Id });
|
||||
userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
|
||||
}
|
||||
|
||||
@@ -314,7 +324,7 @@ public class PostController(
|
||||
if (currentUser != null)
|
||||
{
|
||||
var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
|
||||
{ AccountId = currentUser.Id });
|
||||
{ AccountId = currentUser.Id });
|
||||
userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
|
||||
}
|
||||
|
||||
@@ -342,7 +352,7 @@ public class PostController(
|
||||
if (currentUser != null)
|
||||
{
|
||||
var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
|
||||
{ AccountId = currentUser.Id });
|
||||
{ AccountId = currentUser.Id });
|
||||
userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
|
||||
}
|
||||
|
||||
@@ -448,7 +458,10 @@ public class PostController(
|
||||
|
||||
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.");
|
||||
post.RepliedPost = repliedPost;
|
||||
post.RepliedPostId = repliedPost.Id;
|
||||
@@ -456,7 +469,10 @@ public class PostController(
|
||||
|
||||
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.");
|
||||
post.ForwardedPost = forwardedPost;
|
||||
post.ForwardedPostId = forwardedPost.Id;
|
||||
@@ -514,7 +530,7 @@ public class PostController(
|
||||
});
|
||||
|
||||
post.Publisher = publisher;
|
||||
|
||||
|
||||
return post;
|
||||
}
|
||||
|
||||
@@ -536,7 +552,7 @@ public class PostController(
|
||||
|
||||
var friendsResponse =
|
||||
await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
|
||||
{ AccountId = currentUser.Id.ToString() });
|
||||
{ AccountId = currentUser.Id.ToString() });
|
||||
var userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
|
||||
var userPublishers = await pub.GetUserPublishers(Guid.Parse(currentUser.Id));
|
||||
|
||||
@@ -632,7 +648,7 @@ public class PostController(
|
||||
|
||||
var friendsResponse =
|
||||
await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
|
||||
{ AccountId = currentUser.Id.ToString() });
|
||||
{ AccountId = currentUser.Id.ToString() });
|
||||
var userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
|
||||
var userPublishers = await pub.GetUserPublishers(Guid.Parse(currentUser.Id));
|
||||
|
||||
@@ -883,7 +899,7 @@ public class PostController(
|
||||
UserAgent = Request.Headers.UserAgent,
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
|
||||
|
||||
return Ok(post);
|
||||
}
|
||||
|
||||
@@ -915,4 +931,4 @@ public class PostController(
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ builder.ConfigureAppKestrel(builder.Configuration);
|
||||
|
||||
// Add application services
|
||||
|
||||
builder.Services.AddAppServices(builder.Configuration);
|
||||
builder.Services.AddAppServices();
|
||||
builder.Services.AddAppRateLimiting();
|
||||
builder.Services.AddAppAuthentication();
|
||||
builder.Services.AddDysonAuth();
|
||||
|
@@ -37,6 +37,14 @@ public class PublisherController(
|
||||
|
||||
return Ok(publisher);
|
||||
}
|
||||
|
||||
[HttpGet("{name}/heatmap")]
|
||||
public async Task<ActionResult<ActivityHeatmap>> GetPublisherHeatmap(string name)
|
||||
{
|
||||
var heatmap = await ps.GetPublisherHeatmap(name);
|
||||
if (heatmap is null) return NotFound();
|
||||
return Ok(heatmap);
|
||||
}
|
||||
|
||||
[HttpGet("{name}/stats")]
|
||||
public async Task<ActionResult<PublisherService.PublisherStats>> GetPublisherStats(string name)
|
||||
@@ -693,4 +701,4 @@ public class PublisherController(
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -282,8 +282,9 @@ public class PublisherService(
|
||||
public int SubscribersCount { get; set; }
|
||||
}
|
||||
|
||||
private const string PublisherStatsCacheKey = "PublisherStats_{0}";
|
||||
private const string PublisherFeatureCacheKey = "PublisherFeature_{0}_{1}";
|
||||
private const string PublisherStatsCacheKey = "publisher:{0}:stats";
|
||||
private const string PublisherHeatmapCacheKey = "publisher:{0}:heatmap";
|
||||
private const string PublisherFeatureCacheKey = "publisher:{0}:feature:{1}";
|
||||
|
||||
public async Task<PublisherStats?> GetPublisherStats(string name)
|
||||
{
|
||||
@@ -325,6 +326,45 @@ public class PublisherService(
|
||||
return stats;
|
||||
}
|
||||
|
||||
public async Task<ActivityHeatmap?> GetPublisherHeatmap(string name)
|
||||
{
|
||||
var cacheKey = string.Format(PublisherHeatmapCacheKey, name);
|
||||
var heatmap = await cache.GetAsync<ActivityHeatmap?>(cacheKey);
|
||||
if (heatmap is not null)
|
||||
return heatmap;
|
||||
|
||||
var publisher = await db.Publishers.FirstOrDefaultAsync(e => e.Name == name);
|
||||
if (publisher is null) return null;
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var periodStart = now.Minus(Duration.FromDays(365));
|
||||
var periodEnd = now;
|
||||
|
||||
var postGroups = await db.Posts
|
||||
.Where(p => p.PublisherId == publisher.Id && p.CreatedAt >= periodStart && p.CreatedAt <= periodEnd)
|
||||
.Select(p => p.CreatedAt.InUtc().Date)
|
||||
.GroupBy(d => d)
|
||||
.Select(g => new { Date = g.Key, Count = g.Count() })
|
||||
.ToListAsync();
|
||||
|
||||
var items = postGroups.Select(p => new ActivityHeatmapItem
|
||||
{
|
||||
Date = p.Date.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant(),
|
||||
Count = p.Count
|
||||
}).ToList();
|
||||
|
||||
heatmap = new ActivityHeatmap
|
||||
{
|
||||
Unit = "posts",
|
||||
PeriodStart = periodStart,
|
||||
PeriodEnd = periodEnd,
|
||||
Items = items.OrderBy(i => i.Date).ToList()
|
||||
};
|
||||
|
||||
await cache.SetAsync(cacheKey, heatmap, TimeSpan.FromMinutes(5));
|
||||
return heatmap;
|
||||
}
|
||||
|
||||
public async Task SetFeatureFlag(Guid publisherId, string flag)
|
||||
{
|
||||
var featureFlag = await db.PublisherFeatures
|
||||
@@ -397,4 +437,4 @@ public class PublisherService(
|
||||
return m;
|
||||
})];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,8 @@ using System.Text.Json.Serialization;
|
||||
using System.Threading.RateLimiting;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.GeoIp;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using DysonNetwork.Sphere.Autocompletion;
|
||||
using DysonNetwork.Sphere.WebReader;
|
||||
using DysonNetwork.Sphere.Discovery;
|
||||
using DysonNetwork.Sphere.Poll;
|
||||
@@ -24,7 +26,7 @@ namespace DysonNetwork.Sphere.Startup;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration)
|
||||
public static IServiceCollection AddAppServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddLocalization(options => options.ResourcesPath = "Resources");
|
||||
|
||||
@@ -39,7 +41,6 @@ public static class ServiceCollectionExtensions
|
||||
{
|
||||
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
|
||||
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||
}).AddDataAnnotationsLocalization(options =>
|
||||
@@ -118,6 +119,8 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<WebFeedService>();
|
||||
services.AddScoped<DiscoveryService>();
|
||||
services.AddScoped<PollService>();
|
||||
services.AddScoped<AccountClientHelper>();
|
||||
services.AddScoped<AutocompletionService>();
|
||||
|
||||
var translationProvider = configuration["Translation:Provider"]?.ToLower();
|
||||
switch (translationProvider)
|
||||
@@ -129,4 +132,4 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -237,6 +237,22 @@ public class StickerController(
|
||||
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}")]
|
||||
public async Task<ActionResult<SnSticker>> GetSticker(Guid packId, Guid id)
|
||||
{
|
||||
@@ -420,4 +436,4 @@ public class StickerController(
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user