Full featured auto complete

This commit is contained in:
2025-10-12 16:55:32 +08:00
parent e624c2bb3e
commit 37ea882ef7
8 changed files with 347 additions and 275 deletions

View File

@@ -160,6 +160,26 @@ public class AccountServiceGrpc(
return response; 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, public override async Task<ListAccountsResponse> ListAccounts(ListAccountsRequest request,
ServerCallContext context) ServerCallContext context)
{ {

View File

@@ -14,232 +14,232 @@ import 'wallet.proto';
// Account represents a user account in the system // Account represents a user account in the system
message Account { message Account {
string id = 1; string id = 1;
string name = 2; string name = 2;
string nick = 3; string nick = 3;
string language = 4; string language = 4;
string region = 18; string region = 18;
google.protobuf.Timestamp activated_at = 5; google.protobuf.Timestamp activated_at = 5;
bool is_superuser = 6; bool is_superuser = 6;
AccountProfile profile = 7; AccountProfile profile = 7;
optional SubscriptionReferenceObject perk_subscription = 16; optional SubscriptionReferenceObject perk_subscription = 16;
repeated AccountContact contacts = 8; repeated AccountContact contacts = 8;
repeated AccountBadge badges = 9; repeated AccountBadge badges = 9;
repeated AccountAuthFactor auth_factors = 10; repeated AccountAuthFactor auth_factors = 10;
repeated AccountConnection connections = 11; repeated AccountConnection connections = 11;
repeated Relationship outgoing_relationships = 12; repeated Relationship outgoing_relationships = 12;
repeated Relationship incoming_relationships = 13; repeated Relationship incoming_relationships = 13;
google.protobuf.Timestamp created_at = 14; google.protobuf.Timestamp created_at = 14;
google.protobuf.Timestamp updated_at = 15; google.protobuf.Timestamp updated_at = 15;
google.protobuf.StringValue automated_id = 17; google.protobuf.StringValue automated_id = 17;
} }
// Enum for status attitude // Enum for status attitude
enum StatusAttitude { enum StatusAttitude {
STATUS_ATTITUDE_UNSPECIFIED = 0; STATUS_ATTITUDE_UNSPECIFIED = 0;
POSITIVE = 1; POSITIVE = 1;
NEGATIVE = 2; NEGATIVE = 2;
NEUTRAL = 3; NEUTRAL = 3;
} }
// AccountStatus represents the status of an account // AccountStatus represents the status of an account
message AccountStatus { message AccountStatus {
string id = 1; string id = 1;
StatusAttitude attitude = 2; StatusAttitude attitude = 2;
bool is_online = 3; bool is_online = 3;
bool is_customized = 4; bool is_customized = 4;
bool is_invisible = 5; bool is_invisible = 5;
bool is_not_disturb = 6; bool is_not_disturb = 6;
google.protobuf.StringValue label = 7; google.protobuf.StringValue label = 7;
google.protobuf.Timestamp cleared_at = 8; google.protobuf.Timestamp cleared_at = 8;
string account_id = 9; string account_id = 9;
bytes meta = 10; bytes meta = 10;
} }
// Profile contains detailed information about a user // Profile contains detailed information about a user
message AccountProfile { message AccountProfile {
string id = 1; string id = 1;
google.protobuf.StringValue first_name = 2; google.protobuf.StringValue first_name = 2;
google.protobuf.StringValue middle_name = 3; google.protobuf.StringValue middle_name = 3;
google.protobuf.StringValue last_name = 4; google.protobuf.StringValue last_name = 4;
google.protobuf.StringValue bio = 5; google.protobuf.StringValue bio = 5;
google.protobuf.StringValue gender = 6; google.protobuf.StringValue gender = 6;
google.protobuf.StringValue pronouns = 7; google.protobuf.StringValue pronouns = 7;
google.protobuf.StringValue time_zone = 8; google.protobuf.StringValue time_zone = 8;
google.protobuf.StringValue location = 9; google.protobuf.StringValue location = 9;
google.protobuf.Timestamp birthday = 10; google.protobuf.Timestamp birthday = 10;
google.protobuf.Timestamp last_seen_at = 11; google.protobuf.Timestamp last_seen_at = 11;
VerificationMark verification = 12; VerificationMark verification = 12;
BadgeReferenceObject active_badge = 13; BadgeReferenceObject active_badge = 13;
int32 experience = 14; int32 experience = 14;
int32 level = 15; int32 level = 15;
double leveling_progress = 16; double leveling_progress = 16;
double social_credits = 17; double social_credits = 17;
int32 social_credits_level = 18; int32 social_credits_level = 18;
CloudFile picture = 19; CloudFile picture = 19;
CloudFile background = 20; CloudFile background = 20;
string account_id = 21; string account_id = 21;
google.protobuf.Timestamp created_at = 22; google.protobuf.Timestamp created_at = 22;
google.protobuf.Timestamp updated_at = 23; google.protobuf.Timestamp updated_at = 23;
} }
// AccountContact represents a contact method for an account // AccountContact represents a contact method for an account
message AccountContact { message AccountContact {
string id = 1; string id = 1;
AccountContactType type = 2; AccountContactType type = 2;
google.protobuf.Timestamp verified_at = 3; google.protobuf.Timestamp verified_at = 3;
bool is_primary = 4; bool is_primary = 4;
string content = 5; string content = 5;
string account_id = 6; string account_id = 6;
google.protobuf.Timestamp created_at = 7; google.protobuf.Timestamp created_at = 7;
google.protobuf.Timestamp updated_at = 8; google.protobuf.Timestamp updated_at = 8;
} }
// Enum for contact types // Enum for contact types
enum AccountContactType { enum AccountContactType {
ACCOUNT_CONTACT_TYPE_UNSPECIFIED = 0; ACCOUNT_CONTACT_TYPE_UNSPECIFIED = 0;
EMAIL = 1; EMAIL = 1;
PHONE_NUMBER = 2; PHONE_NUMBER = 2;
ADDRESS = 3; ADDRESS = 3;
} }
// AccountAuthFactor represents an authentication factor for an account // AccountAuthFactor represents an authentication factor for an account
message AccountAuthFactor { message AccountAuthFactor {
string id = 1; string id = 1;
AccountAuthFactorType type = 2; AccountAuthFactorType type = 2;
google.protobuf.StringValue secret = 3; // Omitted from JSON serialization in original google.protobuf.StringValue secret = 3; // Omitted from JSON serialization in original
map<string, google.protobuf.Value> config = 4; // Omitted from JSON serialization in original map<string, google.protobuf.Value> config = 4; // Omitted from JSON serialization in original
int32 trustworthy = 5; int32 trustworthy = 5;
google.protobuf.Timestamp enabled_at = 6; google.protobuf.Timestamp enabled_at = 6;
google.protobuf.Timestamp expired_at = 7; google.protobuf.Timestamp expired_at = 7;
string account_id = 8; string account_id = 8;
map<string, google.protobuf.Value> created_response = 9; // For initial setup map<string, google.protobuf.Value> created_response = 9; // For initial setup
google.protobuf.Timestamp created_at = 10; google.protobuf.Timestamp created_at = 10;
google.protobuf.Timestamp updated_at = 11; google.protobuf.Timestamp updated_at = 11;
} }
// Enum for authentication factor types // Enum for authentication factor types
enum AccountAuthFactorType { enum AccountAuthFactorType {
AUTH_FACTOR_TYPE_UNSPECIFIED = 0; AUTH_FACTOR_TYPE_UNSPECIFIED = 0;
PASSWORD = 1; PASSWORD = 1;
EMAIL_CODE = 2; EMAIL_CODE = 2;
IN_APP_CODE = 3; IN_APP_CODE = 3;
TIMED_CODE = 4; TIMED_CODE = 4;
PIN_CODE = 5; PIN_CODE = 5;
} }
// AccountBadge represents a badge associated with an account // AccountBadge represents a badge associated with an account
message AccountBadge { message AccountBadge {
string id = 1; // Unique identifier for the badge string id = 1; // Unique identifier for the badge
string type = 2; // Type/category of the badge string type = 2; // Type/category of the badge
google.protobuf.StringValue label = 3; // Display name of the badge google.protobuf.StringValue label = 3; // Display name of the badge
google.protobuf.StringValue caption = 4; // Optional description 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 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 activated_at = 6; // When the badge was activated
google.protobuf.Timestamp expired_at = 7; // Optional expiration time google.protobuf.Timestamp expired_at = 7; // Optional expiration time
string account_id = 8; // ID of the account this badge belongs to string account_id = 8; // ID of the account this badge belongs to
google.protobuf.Timestamp created_at = 9; google.protobuf.Timestamp created_at = 9;
google.protobuf.Timestamp updated_at = 10; google.protobuf.Timestamp updated_at = 10;
} }
// AccountConnection represents a third-party connection for an account // AccountConnection represents a third-party connection for an account
message AccountConnection { message AccountConnection {
string id = 1; string id = 1;
string provider = 2; string provider = 2;
string provided_identifier = 3; string provided_identifier = 3;
map<string, google.protobuf.Value> meta = 4; map<string, google.protobuf.Value> meta = 4;
google.protobuf.StringValue access_token = 5; // Omitted from JSON serialization google.protobuf.StringValue access_token = 5; // Omitted from JSON serialization
google.protobuf.StringValue refresh_token = 6; // Omitted from JSON serialization google.protobuf.StringValue refresh_token = 6; // Omitted from JSON serialization
google.protobuf.Timestamp last_used_at = 7; google.protobuf.Timestamp last_used_at = 7;
string account_id = 8; string account_id = 8;
google.protobuf.Timestamp created_at = 9; google.protobuf.Timestamp created_at = 9;
google.protobuf.Timestamp updated_at = 10; google.protobuf.Timestamp updated_at = 10;
} }
// VerificationMark represents verification status // VerificationMark represents verification status
message VerificationMark { message VerificationMark {
VerificationMarkType type = 1; VerificationMarkType type = 1;
string title = 2; string title = 2;
string description = 3; string description = 3;
string verified_by = 4; string verified_by = 4;
google.protobuf.Timestamp created_at = 5; google.protobuf.Timestamp created_at = 5;
google.protobuf.Timestamp updated_at = 6; google.protobuf.Timestamp updated_at = 6;
} }
enum VerificationMarkType { enum VerificationMarkType {
VERIFICATION_MARK_TYPE_UNSPECIFIED = 0; VERIFICATION_MARK_TYPE_UNSPECIFIED = 0;
OFFICIAL = 1; OFFICIAL = 1;
INDIVIDUAL = 2; INDIVIDUAL = 2;
ORGANIZATION = 3; ORGANIZATION = 3;
GOVERNMENT = 4; GOVERNMENT = 4;
CREATOR = 5; CREATOR = 5;
DEVELOPER = 6; DEVELOPER = 6;
PARODY = 7; PARODY = 7;
} }
// BadgeReferenceObject represents a reference to a badge with minimal information // BadgeReferenceObject represents a reference to a badge with minimal information
message BadgeReferenceObject { message BadgeReferenceObject {
string id = 1; // Unique identifier for the badge string id = 1; // Unique identifier for the badge
string type = 2; // Type/category of the badge string type = 2; // Type/category of the badge
google.protobuf.StringValue label = 3; // Display name of the badge google.protobuf.StringValue label = 3; // Display name of the badge
google.protobuf.StringValue caption = 4; // Optional description 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 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 activated_at = 6; // When the badge was activated
google.protobuf.Timestamp expired_at = 7; // Optional expiration time google.protobuf.Timestamp expired_at = 7; // Optional expiration time
string account_id = 8; // ID of the account this badge belongs to string account_id = 8; // ID of the account this badge belongs to
} }
// Relationship represents a connection between two accounts // Relationship represents a connection between two accounts
message Relationship { message Relationship {
string account_id = 1; string account_id = 1;
string related_id = 2; string related_id = 2;
optional Account account = 3; optional Account account = 3;
optional Account related = 4; optional Account related = 4;
int32 status = 5; int32 status = 5;
google.protobuf.Timestamp created_at = 6; google.protobuf.Timestamp created_at = 6;
google.protobuf.Timestamp updated_at = 7; google.protobuf.Timestamp updated_at = 7;
} }
// Leveling information // Leveling information
message LevelingInfo { message LevelingInfo {
int32 current_level = 1; int32 current_level = 1;
int32 current_experience = 2; int32 current_experience = 2;
int32 next_level_experience = 3; int32 next_level_experience = 3;
int32 previous_level_experience = 4; int32 previous_level_experience = 4;
double level_progress = 5; double level_progress = 5;
repeated int32 experience_per_level = 6; repeated int32 experience_per_level = 6;
} }
// ActionLog represents a record of an action taken by a user // ActionLog represents a record of an action taken by a user
message ActionLog { message ActionLog {
string id = 1; // Unique identifier for the log entry string id = 1; // Unique identifier for the log entry
string action = 2; // The action that was performed, e.g., "user.login" string action = 2; // The action that was performed, e.g., "user.login"
map<string, google.protobuf.Value> meta = 3; // Metadata associated with the action 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 user_agent = 4; // User agent of the client
google.protobuf.StringValue ip_address = 5; // IP address 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 google.protobuf.StringValue location = 6; // Geographic location of the client, derived from IP
string account_id = 7; // The account that performed the action 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.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 { message GetAccountStatusBatchResponse {
repeated AccountStatus statuses = 1; repeated AccountStatus statuses = 1;
} }
// ==================================== // ====================================
@@ -248,45 +248,46 @@ message GetAccountStatusBatchResponse {
// AccountService provides CRUD operations for user accounts and related entities // AccountService provides CRUD operations for user accounts and related entities
service AccountService { service AccountService {
// Account Operations // Account Operations
rpc GetAccount(GetAccountRequest) returns (Account) {} rpc GetAccount(GetAccountRequest) returns (Account) {}
rpc GetBotAccount(GetBotAccountRequest) returns (Account) {} rpc GetBotAccount(GetBotAccountRequest) returns (Account) {}
rpc GetAccountBatch(GetAccountBatchRequest) returns (GetAccountBatchResponse) {} rpc GetAccountBatch(GetAccountBatchRequest) returns (GetAccountBatchResponse) {}
rpc GetBotAccountBatch(GetBotAccountBatchRequest) returns (GetAccountBatchResponse) {} rpc GetBotAccountBatch(GetBotAccountBatchRequest) returns (GetAccountBatchResponse) {}
rpc LookupAccountBatch(LookupAccountBatchRequest) returns (GetAccountBatchResponse) {} rpc LookupAccountBatch(LookupAccountBatchRequest) returns (GetAccountBatchResponse) {}
rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse) {} rpc SearchAccount(SearchAccountRequest) returns (GetAccountBatchResponse) {}
rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse) {}
rpc GetAccountStatus(GetAccountRequest) returns (AccountStatus) {} rpc GetAccountStatus(GetAccountRequest) returns (AccountStatus) {}
rpc GetAccountStatusBatch(GetAccountBatchRequest) returns (GetAccountStatusBatchResponse) {} rpc GetAccountStatusBatch(GetAccountBatchRequest) returns (GetAccountStatusBatchResponse) {}
// Profile Operations // Profile Operations
rpc GetProfile(GetProfileRequest) returns (AccountProfile) {} rpc GetProfile(GetProfileRequest) returns (AccountProfile) {}
// Contact Operations // Contact Operations
rpc ListContacts(ListContactsRequest) returns (ListContactsResponse) {} rpc ListContacts(ListContactsRequest) returns (ListContactsResponse) {}
// Badge Operations // Badge Operations
rpc ListBadges(ListBadgesRequest) returns (ListBadgesResponse) {} rpc ListBadges(ListBadgesRequest) returns (ListBadgesResponse) {}
// Authentication Factor Operations // Authentication Factor Operations
rpc ListAuthFactors(ListAuthFactorsRequest) returns (ListAuthFactorsResponse) {} rpc ListAuthFactors(ListAuthFactorsRequest) returns (ListAuthFactorsResponse) {}
// Connection Operations // Connection Operations
rpc ListConnections(ListConnectionsRequest) returns (ListConnectionsResponse) {} rpc ListConnections(ListConnectionsRequest) returns (ListConnectionsResponse) {}
// Relationship Operations // Relationship Operations
rpc ListRelationships(ListRelationshipsRequest) returns (ListRelationshipsResponse) {} rpc ListRelationships(ListRelationshipsRequest) returns (ListRelationshipsResponse) {}
rpc GetRelationship(GetRelationshipRequest) returns (GetRelationshipResponse) {} rpc GetRelationship(GetRelationshipRequest) returns (GetRelationshipResponse) {}
rpc HasRelationship(GetRelationshipRequest) returns (google.protobuf.BoolValue) {} rpc HasRelationship(GetRelationshipRequest) returns (google.protobuf.BoolValue) {}
rpc ListFriends(ListRelationshipSimpleRequest) returns (ListRelationshipSimpleResponse) {} rpc ListFriends(ListRelationshipSimpleRequest) returns (ListRelationshipSimpleResponse) {}
rpc ListBlocked(ListRelationshipSimpleRequest) returns (ListRelationshipSimpleResponse) {} rpc ListBlocked(ListRelationshipSimpleRequest) returns (ListRelationshipSimpleResponse) {}
} }
// ActionLogService provides operations for action logs // ActionLogService provides operations for action logs
service ActionLogService { service ActionLogService {
rpc CreateActionLog(CreateActionLogRequest) returns (CreateActionLogResponse) {} rpc CreateActionLog(CreateActionLogRequest) returns (CreateActionLogResponse) {}
rpc ListActionLogs(ListActionLogsRequest) returns (ListActionLogsResponse) {} rpc ListActionLogs(ListActionLogsRequest) returns (ListActionLogsResponse) {}
} }
// ==================================== // ====================================
@@ -295,184 +296,188 @@ service ActionLogService {
// ActionLog Requests/Responses // ActionLog Requests/Responses
message CreateActionLogRequest { message CreateActionLogRequest {
string action = 1; string action = 1;
map<string, google.protobuf.Value> meta = 2; map<string, google.protobuf.Value> meta = 2;
google.protobuf.StringValue user_agent = 3; google.protobuf.StringValue user_agent = 3;
google.protobuf.StringValue ip_address = 4; google.protobuf.StringValue ip_address = 4;
google.protobuf.StringValue location = 5; google.protobuf.StringValue location = 5;
string account_id = 6; string account_id = 6;
google.protobuf.StringValue session_id = 7; google.protobuf.StringValue session_id = 7;
} }
message CreateActionLogResponse { message CreateActionLogResponse {
ActionLog action_log = 1; ActionLog action_log = 1;
} }
message ListActionLogsRequest { message ListActionLogsRequest {
string account_id = 1; string account_id = 1;
string action = 2; string action = 2;
int32 page_size = 3; int32 page_size = 3;
string page_token = 4; string page_token = 4;
string order_by = 5; string order_by = 5;
} }
message ListActionLogsResponse { message ListActionLogsResponse {
repeated ActionLog action_logs = 1; repeated ActionLog action_logs = 1;
string next_page_token = 2; string next_page_token = 2;
int32 total_size = 3; int32 total_size = 3;
} }
// Account Requests/Responses // Account Requests/Responses
message GetAccountRequest { message GetAccountRequest {
string id = 1; // Account ID to retrieve string id = 1; // Account ID to retrieve
} }
message GetBotAccountRequest { message GetBotAccountRequest {
string automated_id = 1; string automated_id = 1;
} }
message GetAccountBatchRequest { message GetAccountBatchRequest {
repeated string id = 1; // Account ID to retrieve repeated string id = 1; // Account ID to retrieve
} }
message GetBotAccountBatchRequest { message GetBotAccountBatchRequest {
repeated string automated_id = 1; repeated string automated_id = 1;
} }
message LookupAccountBatchRequest { message LookupAccountBatchRequest {
repeated string names = 1; repeated string names = 1;
}
message SearchAccountRequest {
string query = 1;
} }
message GetAccountBatchResponse { message GetAccountBatchResponse {
repeated Account accounts = 1; // List of accounts repeated Account accounts = 1; // List of accounts
} }
message CreateAccountRequest { message CreateAccountRequest {
string name = 1; // Required: Unique username string name = 1; // Required: Unique username
string nick = 2; // Optional: Display name string nick = 2; // Optional: Display name
string language = 3; // Default language string language = 3; // Default language
bool is_superuser = 4; // Admin flag bool is_superuser = 4; // Admin flag
AccountProfile profile = 5; // Initial profile data AccountProfile profile = 5; // Initial profile data
} }
message UpdateAccountRequest { message UpdateAccountRequest {
string id = 1; // Account ID to update string id = 1; // Account ID to update
google.protobuf.StringValue name = 2; // New username if changing google.protobuf.StringValue name = 2; // New username if changing
google.protobuf.StringValue nick = 3; // New display name google.protobuf.StringValue nick = 3; // New display name
google.protobuf.StringValue language = 4; // New language google.protobuf.StringValue language = 4; // New language
google.protobuf.BoolValue is_superuser = 5; // Admin status google.protobuf.BoolValue is_superuser = 5; // Admin status
} }
message DeleteAccountRequest { message DeleteAccountRequest {
string id = 1; // Account ID to delete string id = 1; // Account ID to delete
bool purge = 2; // If true, permanently delete instead of soft delete bool purge = 2; // If true, permanently delete instead of soft delete
} }
message ListAccountsRequest { message ListAccountsRequest {
int32 page_size = 1; // Number of results per page int32 page_size = 1; // Number of results per page
string page_token = 2; // Token for pagination string page_token = 2; // Token for pagination
string filter = 3; // Filter expression string filter = 3; // Filter expression
string order_by = 4; // Sort order string order_by = 4; // Sort order
} }
message ListAccountsResponse { message ListAccountsResponse {
repeated Account accounts = 1; // List of accounts repeated Account accounts = 1; // List of accounts
string next_page_token = 2; // Token for next page string next_page_token = 2; // Token for next page
int32 total_size = 3; // Total number of accounts int32 total_size = 3; // Total number of accounts
} }
// Profile Requests/Responses // Profile Requests/Responses
message GetProfileRequest { message GetProfileRequest {
string account_id = 1; // Account ID to get profile for string account_id = 1; // Account ID to get profile for
} }
message UpdateProfileRequest { message UpdateProfileRequest {
string account_id = 1; // Account ID to update profile for string account_id = 1; // Account ID to update profile for
AccountProfile profile = 2; // Profile data to update AccountProfile profile = 2; // Profile data to update
google.protobuf.FieldMask update_mask = 3; // Fields to update google.protobuf.FieldMask update_mask = 3; // Fields to update
} }
// Contact Requests/Responses // Contact Requests/Responses
message AddContactRequest { message AddContactRequest {
string account_id = 1; // Account to add contact to string account_id = 1; // Account to add contact to
AccountContactType type = 2; // Type of contact AccountContactType type = 2; // Type of contact
string content = 3; // Contact content (email, phone, etc.) string content = 3; // Contact content (email, phone, etc.)
bool is_primary = 4; // If this should be the primary contact bool is_primary = 4; // If this should be the primary contact
} }
message ListContactsRequest { message ListContactsRequest {
string account_id = 1; // Account ID to list contacts for string account_id = 1; // Account ID to list contacts for
AccountContactType type = 2; // Optional: filter by type AccountContactType type = 2; // Optional: filter by type
bool verified_only = 3; // Only return verified contacts bool verified_only = 3; // Only return verified contacts
} }
message ListContactsResponse { message ListContactsResponse {
repeated AccountContact contacts = 1; // List of contacts repeated AccountContact contacts = 1; // List of contacts
} }
message VerifyContactRequest { message VerifyContactRequest {
string id = 1; // Contact ID to verify string id = 1; // Contact ID to verify
string account_id = 2; // Account ID (for validation) string account_id = 2; // Account ID (for validation)
string code = 3; // Verification code string code = 3; // Verification code
} }
// Badge Requests/Responses // Badge Requests/Responses
message ListBadgesRequest { message ListBadgesRequest {
string account_id = 1; // Account to list badges for string account_id = 1; // Account to list badges for
string type = 2; // Optional: filter by type string type = 2; // Optional: filter by type
bool active_only = 3; // Only return active (non-expired) badges bool active_only = 3; // Only return active (non-expired) badges
} }
message ListBadgesResponse { message ListBadgesResponse {
repeated AccountBadge badges = 1; // List of badges repeated AccountBadge badges = 1; // List of badges
} }
message ListAuthFactorsRequest { message ListAuthFactorsRequest {
string account_id = 1; // Account to list factors for string account_id = 1; // Account to list factors for
bool active_only = 2; // Only return active (non-expired) factors bool active_only = 2; // Only return active (non-expired) factors
} }
message ListAuthFactorsResponse { message ListAuthFactorsResponse {
repeated AccountAuthFactor factors = 1; // List of auth factors repeated AccountAuthFactor factors = 1; // List of auth factors
} }
message ListConnectionsRequest { message ListConnectionsRequest {
string account_id = 1; // Account to list connections for string account_id = 1; // Account to list connections for
string provider = 2; // Optional: filter by provider string provider = 2; // Optional: filter by provider
} }
message ListConnectionsResponse { message ListConnectionsResponse {
repeated AccountConnection connections = 1; // List of connections repeated AccountConnection connections = 1; // List of connections
} }
// Relationship Requests/Responses // Relationship Requests/Responses
message ListRelationshipsRequest { message ListRelationshipsRequest {
string account_id = 1; // Account to list relationships for string account_id = 1; // Account to list relationships for
optional int32 status = 2; // Filter by status optional int32 status = 2; // Filter by status
int32 page_size = 5; // Number of results per page int32 page_size = 5; // Number of results per page
string page_token = 6; // Token for pagination string page_token = 6; // Token for pagination
} }
message ListRelationshipsResponse { message ListRelationshipsResponse {
repeated Relationship relationships = 1; // List of relationships repeated Relationship relationships = 1; // List of relationships
string next_page_token = 2; // Token for next page string next_page_token = 2; // Token for next page
int32 total_size = 3; // Total number of relationships int32 total_size = 3; // Total number of relationships
} }
message GetRelationshipRequest { message GetRelationshipRequest {
string account_id = 1; string account_id = 1;
string related_id = 2; string related_id = 2;
optional int32 status = 3; optional int32 status = 3;
} }
message GetRelationshipResponse { message GetRelationshipResponse {
optional Relationship relationship = 1; optional Relationship relationship = 1;
} }
message ListRelationshipSimpleRequest { message ListRelationshipSimpleRequest {
string account_id = 1; string account_id = 1;
} }
message ListRelationshipSimpleResponse { message ListRelationshipSimpleResponse {
repeated string accounts_id = 1; repeated string accounts_id = 1;
} }

View File

@@ -11,7 +11,7 @@ public class AccountClientHelper(AccountService.AccountServiceClient accounts)
var response = await accounts.GetAccountAsync(request); var response = await accounts.GetAccountAsync(request);
return response; return response;
} }
public async Task<Account> GetBotAccount(Guid automatedId) public async Task<Account> GetBotAccount(Guid automatedId)
{ {
var request = new GetBotAccountRequest { AutomatedId = automatedId.ToString() }; var request = new GetBotAccountRequest { AutomatedId = automatedId.ToString() };
@@ -26,7 +26,14 @@ public class AccountClientHelper(AccountService.AccountServiceClient accounts)
var response = await accounts.GetAccountBatchAsync(request); var response = await accounts.GetAccountBatchAsync(request);
return response.Accounts.ToList(); 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) public async Task<List<Account>> GetBotAccountBatch(List<Guid> automatedIds)
{ {
var request = new GetBotAccountBatchRequest(); var request = new GetBotAccountBatchRequest();

View File

@@ -1,10 +1,12 @@
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Registry;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Autocompletion; namespace DysonNetwork.Sphere.Autocompletion;
public class AutocompletionService(AppDatabase db) public class AutocompletionService(AppDatabase db, AccountClientHelper accountsHelper)
{ {
public async Task<List<DysonNetwork.Shared.Models.Autocompletion>> GetAutocompletion(string content, int limit = 10) public async Task<List<DysonNetwork.Shared.Models.Autocompletion>> GetAutocompletion(string content, Guid? chatId = null, Guid? realmId = null, int limit = 10)
{ {
if (string.IsNullOrWhiteSpace(content)) if (string.IsNullOrWhiteSpace(content))
return []; return [];
@@ -14,7 +16,8 @@ public class AutocompletionService(AppDatabase db)
var afterAt = content[1..]; var afterAt = content[1..];
string type; string type;
string query; string query;
if (afterAt.Contains('/')) bool hadSlash = afterAt.Contains('/');
if (hadSlash)
{ {
var parts = afterAt.Split('/', 2); var parts = afterAt.Split('/', 2);
type = parts[0]; type = parts[0];
@@ -25,7 +28,8 @@ public class AutocompletionService(AppDatabase db)
type = "u"; type = "u";
query = afterAt; query = afterAt;
} }
return await AutocompleteAt(type, query, limit);
return await AutocompleteAt(type, query, chatId, realmId, hadSlash, limit);
} }
if (!content.StartsWith(':')) return []; if (!content.StartsWith(':')) return [];
@@ -33,15 +37,49 @@ public class AutocompletionService(AppDatabase db)
var query = content[1..]; var query = content[1..];
return await AutocompleteSticker(query, limit); return await AutocompleteSticker(query, limit);
} }
} }
private async Task<List<DysonNetwork.Shared.Models.Autocompletion>> AutocompleteAt(string type, string query, int 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>(); var results = new List<DysonNetwork.Shared.Models.Autocompletion>();
switch (type) 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": case "p":
var publishers = await db.Publishers var publishers = await db.Publishers
.Where(p => EF.Functions.Like(p.Name, $"{query}%") || EF.Functions.Like(p.Nick, $"{query}%")) .Where(p => EF.Functions.Like(p.Name, $"{query}%") || EF.Functions.Like(p.Nick, $"{query}%"))
@@ -49,7 +87,7 @@ public class AutocompletionService(AppDatabase db)
.Select(p => new DysonNetwork.Shared.Models.Autocompletion .Select(p => new DysonNetwork.Shared.Models.Autocompletion
{ {
Type = "publisher", Type = "publisher",
Keyword = p.Name, Keyword = "@p/" + p.Name,
Data = p Data = p
}) })
.ToListAsync(); .ToListAsync();
@@ -63,7 +101,7 @@ public class AutocompletionService(AppDatabase db)
.Select(r => new DysonNetwork.Shared.Models.Autocompletion .Select(r => new DysonNetwork.Shared.Models.Autocompletion
{ {
Type = "realm", Type = "realm",
Keyword = r.Slug, Keyword = "@r/" + r.Slug,
Data = r Data = r
}) })
.ToListAsync(); .ToListAsync();
@@ -77,7 +115,7 @@ public class AutocompletionService(AppDatabase db)
.Select(c => new DysonNetwork.Shared.Models.Autocompletion .Select(c => new DysonNetwork.Shared.Models.Autocompletion
{ {
Type = "chat", Type = "chat",
Keyword = c.Name!, Keyword = "@c/" + c.Name,
Data = c Data = c
}) })
.ToListAsync(); .ToListAsync();

View File

@@ -332,7 +332,7 @@ public partial class ChatController(
return Ok(response); return Ok(response);
} }
[HttpGet("{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)
@@ -344,7 +344,7 @@ public partial class ChatController(
if (!isMember) if (!isMember)
return StatusCode(403, "You are not a member of this chat room."); return StatusCode(403, "You are not a member of this chat room.");
var result = await aus.GetAutocompletion(request.Content, limit: 10); var result = await aus.GetAutocompletion(request.Content, chatId: roomId, limit: 10);
return Ok(result); return Ok(result);
} }
} }

View File

@@ -14,7 +14,7 @@ builder.ConfigureAppKestrel(builder.Configuration);
// Add application services // Add application services
builder.Services.AddAppServices(builder.Configuration); builder.Services.AddAppServices();
builder.Services.AddAppRateLimiting(); builder.Services.AddAppRateLimiting();
builder.Services.AddAppAuthentication(); builder.Services.AddAppAuthentication();
builder.Services.AddDysonAuth(); builder.Services.AddDysonAuth();

View File

@@ -282,9 +282,9 @@ public class PublisherService(
public int SubscribersCount { get; set; } public int SubscribersCount { get; set; }
} }
private const string PublisherStatsCacheKey = "PublisherStats_{0}"; private const string PublisherStatsCacheKey = "publisher:{0}:stats";
private const string PublisherHeatmapCacheKey = "PublisherHeatmap_{0}"; private const string PublisherHeatmapCacheKey = "publisher:{0}:heatmap";
private const string PublisherFeatureCacheKey = "PublisherFeature_{0}_{1}"; private const string PublisherFeatureCacheKey = "publisher:{0}:feature:{1}";
public async Task<PublisherStats?> GetPublisherStats(string name) public async Task<PublisherStats?> GetPublisherStats(string name)
{ {
@@ -329,7 +329,7 @@ public class PublisherService(
public async Task<ActivityHeatmap?> GetPublisherHeatmap(string name) public async Task<ActivityHeatmap?> GetPublisherHeatmap(string name)
{ {
var cacheKey = string.Format(PublisherHeatmapCacheKey, name); var cacheKey = string.Format(PublisherHeatmapCacheKey, name);
var heatmap = await cache.GetAsync<ActivityHeatmap>(cacheKey); var heatmap = await cache.GetAsync<ActivityHeatmap?>(cacheKey);
if (heatmap is not null) if (heatmap is not null)
return heatmap; return heatmap;

View File

@@ -15,6 +15,7 @@ using System.Text.Json.Serialization;
using System.Threading.RateLimiting; using System.Threading.RateLimiting;
using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.GeoIp; using DysonNetwork.Shared.GeoIp;
using DysonNetwork.Shared.Registry;
using DysonNetwork.Sphere.Autocompletion; using DysonNetwork.Sphere.Autocompletion;
using DysonNetwork.Sphere.WebReader; using DysonNetwork.Sphere.WebReader;
using DysonNetwork.Sphere.Discovery; using DysonNetwork.Sphere.Discovery;
@@ -25,7 +26,7 @@ namespace DysonNetwork.Sphere.Startup;
public static class ServiceCollectionExtensions 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"); services.AddLocalization(options => options.ResourcesPath = "Resources");
@@ -119,6 +120,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<WebFeedService>(); services.AddScoped<WebFeedService>();
services.AddScoped<DiscoveryService>(); services.AddScoped<DiscoveryService>();
services.AddScoped<PollService>(); services.AddScoped<PollService>();
services.AddScoped<AccountClientHelper>();
services.AddScoped<AutocompletionService>(); services.AddScoped<AutocompletionService>();
var translationProvider = configuration["Translation:Provider"]?.ToLower(); var translationProvider = configuration["Translation:Provider"]?.ToLower();