🐛 Fix stuff I think
This commit is contained in:
@@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Serialization.Protobuf;
|
using NodaTime.Serialization.Protobuf;
|
||||||
using OtpNet;
|
using OtpNet;
|
||||||
|
using VerificationMark = DysonNetwork.Shared.Data.VerificationMark;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Account;
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
@@ -41,7 +42,9 @@ public class Account : ModelBase
|
|||||||
Language = Language,
|
Language = Language,
|
||||||
ActivatedAt = ActivatedAt?.ToTimestamp(),
|
ActivatedAt = ActivatedAt?.ToTimestamp(),
|
||||||
IsSuperuser = IsSuperuser,
|
IsSuperuser = IsSuperuser,
|
||||||
Profile = Profile.ToProtoValue()
|
Profile = Profile.ToProtoValue(),
|
||||||
|
CreatedAt = CreatedAt.ToTimestamp(),
|
||||||
|
UpdatedAt = UpdatedAt.ToTimestamp()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add contacts
|
// Add contacts
|
||||||
@@ -54,6 +57,32 @@ public class Account : ModelBase
|
|||||||
|
|
||||||
return proto;
|
return proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Account FromProtoValue(Shared.Proto.Account proto)
|
||||||
|
{
|
||||||
|
var account = new Account
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(proto.Id),
|
||||||
|
Name = proto.Name,
|
||||||
|
Nick = proto.Nick,
|
||||||
|
Language = proto.Language,
|
||||||
|
ActivatedAt = proto.ActivatedAt?.ToInstant(),
|
||||||
|
IsSuperuser = proto.IsSuperuser,
|
||||||
|
CreatedAt = proto.CreatedAt.ToInstant(),
|
||||||
|
UpdatedAt = proto.UpdatedAt.ToInstant(),
|
||||||
|
};
|
||||||
|
|
||||||
|
account.Profile = AccountProfile.FromProtoValue(proto.Profile);
|
||||||
|
|
||||||
|
foreach (var contactProto in proto.Contacts)
|
||||||
|
account.Contacts.Add(AccountContact.FromProtoValue(contactProto));
|
||||||
|
|
||||||
|
foreach (var badgeProto in proto.Badges)
|
||||||
|
account.Badges.Add(AccountBadge.FromProtoValue(badgeProto));
|
||||||
|
|
||||||
|
return account;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class Leveling
|
public abstract class Leveling
|
||||||
@@ -132,12 +161,42 @@ public class AccountProfile : ModelBase, IIdentifiedResource
|
|||||||
Background = Background?.ToProtoValue(),
|
Background = Background?.ToProtoValue(),
|
||||||
AccountId = AccountId.ToString(),
|
AccountId = AccountId.ToString(),
|
||||||
Verification = Verification?.ToProtoValue(),
|
Verification = Verification?.ToProtoValue(),
|
||||||
ActiveBadge = ActiveBadge?.ToProtoValue()
|
ActiveBadge = ActiveBadge?.ToProtoValue(),
|
||||||
|
CreatedAt = CreatedAt.ToTimestamp(),
|
||||||
|
UpdatedAt = UpdatedAt.ToTimestamp()
|
||||||
};
|
};
|
||||||
|
|
||||||
return proto;
|
return proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static AccountProfile FromProtoValue(Shared.Proto.AccountProfile proto)
|
||||||
|
{
|
||||||
|
var profile = new AccountProfile
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(proto.Id),
|
||||||
|
FirstName = proto.FirstName,
|
||||||
|
LastName = proto.LastName,
|
||||||
|
MiddleName = proto.MiddleName,
|
||||||
|
Bio = proto.Bio,
|
||||||
|
Gender = proto.Gender,
|
||||||
|
Pronouns = proto.Pronouns,
|
||||||
|
TimeZone = proto.TimeZone,
|
||||||
|
Location = proto.Location,
|
||||||
|
Birthday = proto.Birthday?.ToInstant(),
|
||||||
|
LastSeenAt = proto.LastSeenAt?.ToInstant(),
|
||||||
|
Verification = proto.Verification is null ? null : VerificationMark.FromProtoValue(proto.Verification),
|
||||||
|
ActiveBadge = proto.ActiveBadge is null ? null : BadgeReferenceObject.FromProtoValue(proto.ActiveBadge),
|
||||||
|
Experience = proto.Experience,
|
||||||
|
Picture = proto.Picture is null ? null : CloudFileReferenceObject.FromProtoValue(proto.Picture),
|
||||||
|
Background = proto.Background is null ? null : CloudFileReferenceObject.FromProtoValue(proto.Background),
|
||||||
|
AccountId = Guid.Parse(proto.AccountId),
|
||||||
|
CreatedAt = proto.CreatedAt.ToInstant(),
|
||||||
|
UpdatedAt = proto.UpdatedAt.ToInstant()
|
||||||
|
};
|
||||||
|
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
public string ResourceIdentifier => $"account:profile:{Id}";
|
public string ResourceIdentifier => $"account:profile:{Id}";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,11 +226,36 @@ public class AccountContact : ModelBase
|
|||||||
Content = Content,
|
Content = Content,
|
||||||
IsPrimary = IsPrimary,
|
IsPrimary = IsPrimary,
|
||||||
VerifiedAt = VerifiedAt?.ToTimestamp(),
|
VerifiedAt = VerifiedAt?.ToTimestamp(),
|
||||||
AccountId = AccountId.ToString()
|
AccountId = AccountId.ToString(),
|
||||||
|
CreatedAt = CreatedAt.ToTimestamp(),
|
||||||
|
UpdatedAt = UpdatedAt.ToTimestamp()
|
||||||
};
|
};
|
||||||
|
|
||||||
return proto;
|
return proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static AccountContact FromProtoValue(Shared.Proto.AccountContact proto)
|
||||||
|
{
|
||||||
|
var contact = new AccountContact
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(proto.Id),
|
||||||
|
AccountId = Guid.Parse(proto.AccountId),
|
||||||
|
Type = proto.Type switch
|
||||||
|
{
|
||||||
|
Shared.Proto.AccountContactType.Email => AccountContactType.Email,
|
||||||
|
Shared.Proto.AccountContactType.PhoneNumber => AccountContactType.PhoneNumber,
|
||||||
|
Shared.Proto.AccountContactType.Address => AccountContactType.Address,
|
||||||
|
_ => AccountContactType.Email
|
||||||
|
},
|
||||||
|
Content = proto.Content,
|
||||||
|
IsPrimary = proto.IsPrimary,
|
||||||
|
VerifiedAt = proto.VerifiedAt?.ToInstant(),
|
||||||
|
CreatedAt = proto.CreatedAt.ToInstant(),
|
||||||
|
UpdatedAt = proto.UpdatedAt.ToInstant()
|
||||||
|
};
|
||||||
|
|
||||||
|
return contact;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum AccountContactType
|
public enum AccountContactType
|
||||||
|
@@ -49,6 +49,7 @@ public class AccountServiceGrpc(
|
|||||||
var accounts = await _db.Accounts
|
var accounts = await _db.Accounts
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(a => accountIds.Contains(a.Id))
|
.Where(a => accountIds.Contains(a.Id))
|
||||||
|
.Include(a => a.Profile)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var response = new GetAccountBatchResponse();
|
var response = new GetAccountBatchResponse();
|
||||||
|
@@ -15,7 +15,7 @@ public class AccountBadge : ModelBase
|
|||||||
[MaxLength(1024)] public string Type { get; set; } = null!;
|
[MaxLength(1024)] public string Type { get; set; } = null!;
|
||||||
[MaxLength(1024)] public string? Label { get; set; }
|
[MaxLength(1024)] public string? Label { get; set; }
|
||||||
[MaxLength(4096)] public string? Caption { get; set; }
|
[MaxLength(4096)] public string? Caption { get; set; }
|
||||||
[Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new();
|
[Column(TypeName = "jsonb")] public Dictionary<string, object?> Meta { get; set; } = new();
|
||||||
public Instant? ActivatedAt { get; set; }
|
public Instant? ActivatedAt { get; set; }
|
||||||
public Instant? ExpiredAt { get; set; }
|
public Instant? ExpiredAt { get; set; }
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ public class AccountBadge : ModelBase
|
|||||||
Meta = Meta,
|
Meta = Meta,
|
||||||
ActivatedAt = ActivatedAt,
|
ActivatedAt = ActivatedAt,
|
||||||
ExpiredAt = ExpiredAt,
|
ExpiredAt = ExpiredAt,
|
||||||
AccountId = AccountId
|
AccountId = AccountId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,11 +48,31 @@ public class AccountBadge : ModelBase
|
|||||||
ActivatedAt = ActivatedAt?.ToTimestamp(),
|
ActivatedAt = ActivatedAt?.ToTimestamp(),
|
||||||
ExpiredAt = ExpiredAt?.ToTimestamp(),
|
ExpiredAt = ExpiredAt?.ToTimestamp(),
|
||||||
AccountId = AccountId.ToString(),
|
AccountId = AccountId.ToString(),
|
||||||
|
CreatedAt = CreatedAt.ToTimestamp(),
|
||||||
|
UpdatedAt = UpdatedAt.ToTimestamp()
|
||||||
};
|
};
|
||||||
proto.Meta.Add(GrpcTypeHelper.ConvertToValueMap(Meta));
|
proto.Meta.Add(GrpcTypeHelper.ConvertToValueMap(Meta));
|
||||||
|
|
||||||
return proto;
|
return proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static AccountBadge FromProtoValue(Shared.Proto.AccountBadge proto)
|
||||||
|
{
|
||||||
|
var badge = new AccountBadge
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(proto.Id),
|
||||||
|
AccountId = Guid.Parse(proto.AccountId),
|
||||||
|
Type = proto.Type,
|
||||||
|
Label = proto.Label,
|
||||||
|
Caption = proto.Caption,
|
||||||
|
ActivatedAt = proto.ActivatedAt?.ToInstant(),
|
||||||
|
ExpiredAt = proto.ExpiredAt?.ToInstant(),
|
||||||
|
CreatedAt = proto.CreatedAt.ToInstant(),
|
||||||
|
UpdatedAt = proto.UpdatedAt.ToInstant()
|
||||||
|
};
|
||||||
|
|
||||||
|
return badge;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BadgeReferenceObject : ModelBase
|
public class BadgeReferenceObject : ModelBase
|
||||||
@@ -61,7 +81,7 @@ public class BadgeReferenceObject : ModelBase
|
|||||||
public string Type { get; set; } = null!;
|
public string Type { get; set; } = null!;
|
||||||
public string? Label { get; set; }
|
public string? Label { get; set; }
|
||||||
public string? Caption { get; set; }
|
public string? Caption { get; set; }
|
||||||
public Dictionary<string, object>? Meta { get; set; }
|
public Dictionary<string, object?> Meta { get; set; }
|
||||||
public Instant? ActivatedAt { get; set; }
|
public Instant? ActivatedAt { get; set; }
|
||||||
public Instant? ExpiredAt { get; set; }
|
public Instant? ExpiredAt { get; set; }
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
@@ -78,9 +98,26 @@ public class BadgeReferenceObject : ModelBase
|
|||||||
ExpiredAt = ExpiredAt?.ToTimestamp(),
|
ExpiredAt = ExpiredAt?.ToTimestamp(),
|
||||||
AccountId = AccountId.ToString()
|
AccountId = AccountId.ToString()
|
||||||
};
|
};
|
||||||
if (Meta is not null)
|
|
||||||
proto.Meta.Add(GrpcTypeHelper.ConvertToValueMap(Meta!));
|
proto.Meta.Add(GrpcTypeHelper.ConvertToValueMap(Meta!));
|
||||||
|
|
||||||
return proto;
|
return proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static BadgeReferenceObject FromProtoValue(Shared.Proto.BadgeReferenceObject proto)
|
||||||
|
{
|
||||||
|
var badge = new BadgeReferenceObject
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(proto.Id),
|
||||||
|
Type = proto.Type,
|
||||||
|
Label = proto.Label,
|
||||||
|
Caption = proto.Caption,
|
||||||
|
Meta = GrpcTypeHelper.ConvertFromValueMap(proto.Meta).ToDictionary(),
|
||||||
|
ActivatedAt = proto.ActivatedAt?.ToInstant(),
|
||||||
|
ExpiredAt = proto.ExpiredAt?.ToInstant(),
|
||||||
|
AccountId = Guid.Parse(proto.AccountId)
|
||||||
|
};
|
||||||
|
|
||||||
|
return badge;
|
||||||
|
}
|
||||||
}
|
}
|
@@ -36,6 +36,28 @@ public class VerificationMark
|
|||||||
|
|
||||||
return proto;
|
return proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static VerificationMark FromProtoValue(Shared.Proto.VerificationMark proto)
|
||||||
|
{
|
||||||
|
return new VerificationMark
|
||||||
|
{
|
||||||
|
Type = proto.Type switch
|
||||||
|
{
|
||||||
|
Proto.VerificationMarkType.Official => VerificationMarkType.Official,
|
||||||
|
Proto.VerificationMarkType.Individual => VerificationMarkType.Individual,
|
||||||
|
Proto.VerificationMarkType.Organization => VerificationMarkType.Organization,
|
||||||
|
Proto.VerificationMarkType.Government => VerificationMarkType.Government,
|
||||||
|
Proto.VerificationMarkType.Creator => VerificationMarkType.Creator,
|
||||||
|
Proto.VerificationMarkType.Developer => VerificationMarkType.Developer,
|
||||||
|
Proto.VerificationMarkType.Parody => VerificationMarkType.Parody,
|
||||||
|
_ => VerificationMarkType.Individual
|
||||||
|
},
|
||||||
|
Title = proto.Title,
|
||||||
|
Description = proto.Description,
|
||||||
|
VerifiedBy = proto.VerifiedBy
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum VerificationMarkType
|
public enum VerificationMarkType
|
||||||
|
@@ -6,7 +6,6 @@ option csharp_namespace = "DysonNetwork.Shared.Proto";
|
|||||||
|
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
import "google/protobuf/wrappers.proto";
|
import "google/protobuf/wrappers.proto";
|
||||||
import "google/protobuf/empty.proto";
|
|
||||||
import "google/protobuf/field_mask.proto";
|
import "google/protobuf/field_mask.proto";
|
||||||
import "google/protobuf/struct.proto";
|
import "google/protobuf/struct.proto";
|
||||||
|
|
||||||
@@ -28,6 +27,9 @@ message Account {
|
|||||||
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 updated_at = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Profile contains detailed information about a user
|
// Profile contains detailed information about a user
|
||||||
@@ -55,6 +57,9 @@ message AccountProfile {
|
|||||||
CloudFile background = 20;
|
CloudFile background = 20;
|
||||||
|
|
||||||
string account_id = 21;
|
string account_id = 21;
|
||||||
|
|
||||||
|
google.protobuf.Timestamp created_at = 22;
|
||||||
|
google.protobuf.Timestamp updated_at = 23;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountContact represents a contact method for an account
|
// AccountContact represents a contact method for an account
|
||||||
@@ -65,6 +70,9 @@ message AccountContact {
|
|||||||
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 updated_at = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enum for contact types
|
// Enum for contact types
|
||||||
@@ -85,7 +93,10 @@ message AccountAuthFactor {
|
|||||||
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, string> 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 updated_at = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enum for authentication factor types
|
// Enum for authentication factor types
|
||||||
@@ -108,6 +119,9 @@ message AccountBadge {
|
|||||||
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 updated_at = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountConnection represents a third-party connection for an account
|
// AccountConnection represents a third-party connection for an account
|
||||||
@@ -120,6 +134,9 @@ message AccountConnection {
|
|||||||
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 updated_at = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerificationMark represents verification status
|
// VerificationMark represents verification status
|
||||||
@@ -128,6 +145,9 @@ message VerificationMark {
|
|||||||
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 updated_at = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum VerificationMarkType {
|
enum VerificationMarkType {
|
||||||
@@ -160,6 +180,7 @@ message Relationship {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
@@ -184,6 +205,7 @@ message ActionLog {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
21
DysonNetwork.Shared/Registry/AccountClientHelper.cs
Normal file
21
DysonNetwork.Shared/Registry/AccountClientHelper.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Shared.Registry;
|
||||||
|
|
||||||
|
public class AccountClientHelper(AccountService.AccountServiceClient accounts)
|
||||||
|
{
|
||||||
|
public async Task<Account> GetAccount(Guid id)
|
||||||
|
{
|
||||||
|
var request = new GetAccountRequest { Id = id.ToString() };
|
||||||
|
var response = await accounts.GetAccountAsync(request);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Account>> GetAccountBatch(List<Guid> ids)
|
||||||
|
{
|
||||||
|
var request = new GetAccountBatchRequest();
|
||||||
|
request.Id.AddRange(ids.Select(id => id.ToString()));
|
||||||
|
var response = await accounts.GetAccountBatchAsync(request);
|
||||||
|
return response.Accounts.ToList();
|
||||||
|
}
|
||||||
|
}
|
@@ -41,6 +41,7 @@ public static class ServiceHelper
|
|||||||
.GetAwaiter()
|
.GetAwaiter()
|
||||||
.GetResult();
|
.GetResult();
|
||||||
});
|
});
|
||||||
|
services.AddSingleton<AccountClientHelper>();
|
||||||
|
|
||||||
services.AddSingleton<ActionLogService.ActionLogServiceClient>(sp =>
|
services.AddSingleton<ActionLogService.ActionLogServiceClient>(sp =>
|
||||||
{
|
{
|
||||||
|
@@ -97,8 +97,6 @@ public partial class ChatController(
|
|||||||
.Where(m => m.ChatRoomId == roomId)
|
.Where(m => m.ChatRoomId == roomId)
|
||||||
.OrderByDescending(m => m.CreatedAt)
|
.OrderByDescending(m => m.CreatedAt)
|
||||||
.Include(m => m.Sender)
|
.Include(m => m.Sender)
|
||||||
.Include(m => m.Sender.Account)
|
|
||||||
.Include(m => m.Sender.Account.Profile)
|
|
||||||
.Skip(offset)
|
.Skip(offset)
|
||||||
.Take(take)
|
.Take(take)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@@ -232,8 +230,6 @@ public partial class ChatController(
|
|||||||
|
|
||||||
var message = await db.ChatMessages
|
var message = await db.ChatMessages
|
||||||
.Include(m => m.Sender)
|
.Include(m => m.Sender)
|
||||||
.Include(m => m.Sender.Account)
|
|
||||||
.Include(m => m.Sender.Account.Profile)
|
|
||||||
.Include(message => message.ChatRoom)
|
.Include(message => message.ChatRoom)
|
||||||
.FirstOrDefaultAsync(m => m.Id == messageId && m.ChatRoomId == roomId);
|
.FirstOrDefaultAsync(m => m.Id == messageId && m.ChatRoomId == roomId);
|
||||||
|
|
||||||
|
@@ -2,8 +2,8 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Proto;
|
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using Account = DysonNetwork.Pass.Account.Account;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat;
|
namespace DysonNetwork.Sphere.Chat;
|
||||||
|
|
||||||
|
@@ -194,7 +194,7 @@ public class ChatRoomController(
|
|||||||
{
|
{
|
||||||
Role = ChatMemberRole.Owner,
|
Role = ChatMemberRole.Owner,
|
||||||
AccountId = Guid.Parse(currentUser.Id),
|
AccountId = Guid.Parse(currentUser.Id),
|
||||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -452,21 +452,23 @@ public class ChatRoomController(
|
|||||||
|
|
||||||
var member = await db.ChatMembers
|
var member = await db.ChatMembers
|
||||||
.Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.ChatRoomId == roomId)
|
.Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.ChatRoomId == roomId)
|
||||||
.Include(m => m.Account)
|
|
||||||
.Include(m => m.Account.Profile)
|
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (member == null)
|
if (member == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
return Ok(member);
|
return Ok(await crs.LoadMemberAccount(member));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{roomId:guid}/members")]
|
[HttpGet("{roomId:guid}/members")]
|
||||||
public async Task<ActionResult<List<ChatMember>>> ListMembers(Guid roomId, [FromQuery] int take = 20,
|
public async Task<ActionResult<List<ChatMember>>> ListMembers(Guid roomId,
|
||||||
[FromQuery] int skip = 0, [FromQuery] bool withStatus = false, [FromQuery] string? status = null)
|
[FromQuery] int take = 20,
|
||||||
|
[FromQuery] int skip = 0,
|
||||||
|
[FromQuery] bool withStatus = false,
|
||||||
|
[FromQuery] string? status = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var currentUser = HttpContext.Items["CurrentUser"] as Shared.Proto.Account;
|
var currentUser = HttpContext.Items["CurrentUser"] as Account;
|
||||||
|
|
||||||
var room = await db.ChatRooms
|
var room = await db.ChatRooms
|
||||||
.FirstOrDefaultAsync(r => r.Id == roomId);
|
.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||||
@@ -480,11 +482,9 @@ public class ChatRoomController(
|
|||||||
if (member is null) return StatusCode(403, "You need to be a member to see members of private chat room.");
|
if (member is null) return StatusCode(403, "You need to be a member to see members of private chat room.");
|
||||||
}
|
}
|
||||||
|
|
||||||
IQueryable<ChatMember> query = db.ChatMembers
|
var query = db.ChatMembers
|
||||||
.Where(m => m.ChatRoomId == roomId)
|
.Where(m => m.ChatRoomId == roomId)
|
||||||
.Where(m => m.LeaveAt == null) // Add this condition to exclude left members
|
.Where(m => m.LeaveAt == null);
|
||||||
.Include(m => m.Account)
|
|
||||||
.Include(m => m.Account.Profile);
|
|
||||||
|
|
||||||
// if (withStatus)
|
// if (withStatus)
|
||||||
// {
|
// {
|
||||||
@@ -509,7 +509,7 @@ public class ChatRoomController(
|
|||||||
//
|
//
|
||||||
// var result = members.Skip(skip).Take(take).ToList();
|
// var result = members.Skip(skip).Take(take).ToList();
|
||||||
//
|
//
|
||||||
// return Ok(result);
|
// return Ok(await crs.LoadMemberAccounts(result));
|
||||||
// }
|
// }
|
||||||
// else
|
// else
|
||||||
// {
|
// {
|
||||||
@@ -522,7 +522,7 @@ public class ChatRoomController(
|
|||||||
.Take(take)
|
.Take(take)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
return Ok(members);
|
return Ok(await crs.LoadMemberAccounts(members));
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -952,7 +952,7 @@ public class ChatRoomController(
|
|||||||
? localizer["ChatInviteDirectBody", sender.Nick]
|
? localizer["ChatInviteDirectBody", sender.Nick]
|
||||||
: localizer["ChatInviteBody", member.ChatRoom.Name ?? "Unnamed"];
|
: localizer["ChatInviteBody", member.ChatRoom.Name ?? "Unnamed"];
|
||||||
|
|
||||||
CultureService.SetCultureInfo(member.Account);
|
CultureService.SetCultureInfo(member.Account.Language);
|
||||||
await pusher.SendPushNotificationToUserAsync(
|
await pusher.SendPushNotificationToUserAsync(
|
||||||
new SendPushNotificationToUserRequest
|
new SendPushNotificationToUserRequest
|
||||||
{
|
{
|
||||||
|
@@ -1,12 +1,18 @@
|
|||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
|
using DysonNetwork.Shared.Registry;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using Account = DysonNetwork.Pass.Account.Account;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat;
|
namespace DysonNetwork.Sphere.Chat;
|
||||||
|
|
||||||
public class ChatRoomService(AppDatabase db, ICacheService cache)
|
public class ChatRoomService(
|
||||||
|
AppDatabase db,
|
||||||
|
ICacheService cache,
|
||||||
|
AccountClientHelper accountsHelper
|
||||||
|
)
|
||||||
{
|
{
|
||||||
public const string ChatRoomGroupPrefix = "chatroom:";
|
private const string ChatRoomGroupPrefix = "chatroom:";
|
||||||
private const string RoomMembersCacheKeyPrefix = "chatroom:members:";
|
private const string RoomMembersCacheKeyPrefix = "chatroom:members:";
|
||||||
private const string ChatMemberCacheKey = "chatroom:{0}:member:{1}";
|
private const string ChatMemberCacheKey = "chatroom:{0}:member:{1}";
|
||||||
|
|
||||||
@@ -18,12 +24,11 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
|||||||
return cachedMembers;
|
return cachedMembers;
|
||||||
|
|
||||||
var members = await db.ChatMembers
|
var members = await db.ChatMembers
|
||||||
.Include(m => m.Account)
|
|
||||||
.ThenInclude(m => m.Profile)
|
|
||||||
.Where(m => m.ChatRoomId == roomId)
|
.Where(m => m.ChatRoomId == roomId)
|
||||||
.Where(m => m.JoinedAt != null)
|
.Where(m => m.JoinedAt != null)
|
||||||
.Where(m => m.LeaveAt == null)
|
.Where(m => m.LeaveAt == null)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
members = await LoadMemberAccounts(members);
|
||||||
|
|
||||||
var chatRoomGroup = ChatRoomGroupPrefix + roomId;
|
var chatRoomGroup = ChatRoomGroupPrefix + roomId;
|
||||||
await cache.SetWithGroupsAsync(cacheKey, members,
|
await cache.SetWithGroupsAsync(cacheKey, members,
|
||||||
@@ -40,14 +45,13 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
|||||||
if (member is not null) return member;
|
if (member is not null) return member;
|
||||||
|
|
||||||
member = await db.ChatMembers
|
member = await db.ChatMembers
|
||||||
.Include(m => m.Account)
|
|
||||||
.ThenInclude(m => m.Profile)
|
|
||||||
.Include(m => m.ChatRoom)
|
|
||||||
.ThenInclude(m => m.Realm)
|
|
||||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == chatRoomId)
|
.Where(m => m.AccountId == accountId && m.ChatRoomId == chatRoomId)
|
||||||
|
.Include(m => m.ChatRoom)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (member == null) return member;
|
if (member == null) return member;
|
||||||
|
|
||||||
|
member = await LoadMemberAccount(member);
|
||||||
var chatRoomGroup = ChatRoomGroupPrefix + chatRoomId;
|
var chatRoomGroup = ChatRoomGroupPrefix + chatRoomId;
|
||||||
await cache.SetWithGroupsAsync(cacheKey, member,
|
await cache.SetWithGroupsAsync(cacheKey, member,
|
||||||
[chatRoomGroup],
|
[chatRoomGroup],
|
||||||
@@ -92,8 +96,6 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
|||||||
.Where(m => directRoomsId.Contains(m.ChatRoomId))
|
.Where(m => directRoomsId.Contains(m.ChatRoomId))
|
||||||
.Where(m => m.AccountId != userId)
|
.Where(m => m.AccountId != userId)
|
||||||
.Where(m => m.LeaveAt == null)
|
.Where(m => m.LeaveAt == null)
|
||||||
.Include(m => m.Account)
|
|
||||||
.Include(m => m.Account.Profile)
|
|
||||||
.GroupBy(m => m.ChatRoomId)
|
.GroupBy(m => m.ChatRoomId)
|
||||||
.ToDictionaryAsync(g => g.Key, g => g.ToList())
|
.ToDictionaryAsync(g => g.Key, g => g.ToList())
|
||||||
: new Dictionary<Guid, List<ChatMember>>();
|
: new Dictionary<Guid, List<ChatMember>>();
|
||||||
@@ -112,12 +114,13 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
|||||||
var members = await db.ChatMembers
|
var members = await db.ChatMembers
|
||||||
.Where(m => m.ChatRoomId == room.Id && m.AccountId != userId)
|
.Where(m => m.ChatRoomId == room.Id && m.AccountId != userId)
|
||||||
.Where(m => m.LeaveAt == null)
|
.Where(m => m.LeaveAt == null)
|
||||||
.Include(m => m.Account)
|
|
||||||
.Include(m => m.Account.Profile)
|
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
if (members.Count > 0)
|
if (members.Count <= 0) return room;
|
||||||
|
|
||||||
|
members = await LoadMemberAccounts(members);
|
||||||
room.DirectMembers = members.Select(ChatMemberTransmissionObject.FromEntity).ToList();
|
room.DirectMembers = members.Select(ChatMemberTransmissionObject.FromEntity).ToList();
|
||||||
|
|
||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,4 +134,24 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
|||||||
.FirstOrDefaultAsync(m => m.ChatRoomId == roomId && m.AccountId == accountId);
|
.FirstOrDefaultAsync(m => m.ChatRoomId == roomId && m.AccountId == accountId);
|
||||||
return member?.Role >= maxRequiredRole;
|
return member?.Role >= maxRequiredRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ChatMember> LoadMemberAccount(ChatMember member)
|
||||||
|
{
|
||||||
|
var account = await accountsHelper.GetAccount(member.AccountId);
|
||||||
|
member.Account = Account.FromProtoValue(account);
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<ChatMember>> LoadMemberAccounts(ICollection<ChatMember> members)
|
||||||
|
{
|
||||||
|
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 = Account.FromProtoValue(account);
|
||||||
|
return m;
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
}
|
}
|
@@ -9,6 +9,7 @@ namespace DysonNetwork.Sphere.Chat;
|
|||||||
|
|
||||||
public partial class ChatService(
|
public partial class ChatService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
|
ChatRoomService crs,
|
||||||
FileService.FileServiceClient filesClient,
|
FileService.FileServiceClient filesClient,
|
||||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||||
IServiceScopeFactory scopeFactory,
|
IServiceScopeFactory scopeFactory,
|
||||||
@@ -259,7 +260,7 @@ public partial class ChatService(
|
|||||||
}
|
}
|
||||||
else if (member.Notify == ChatMemberNotify.Mentions) continue;
|
else if (member.Notify == ChatMemberNotify.Mentions) continue;
|
||||||
|
|
||||||
accountsToNotify.Add(member.Account);
|
accountsToNotify.Add(member.Account.ToProtoValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogInformation($"Trying to deliver message to {accountsToNotify.Count} accounts...");
|
logger.LogInformation($"Trying to deliver message to {accountsToNotify.Count} accounts...");
|
||||||
@@ -333,8 +334,6 @@ public partial class ChatService(
|
|||||||
var messages = await db.ChatMessages
|
var messages = await db.ChatMessages
|
||||||
.IgnoreQueryFilters()
|
.IgnoreQueryFilters()
|
||||||
.Include(m => m.Sender)
|
.Include(m => m.Sender)
|
||||||
.Include(m => m.Sender.Account)
|
|
||||||
.Include(m => m.Sender.Account.Profile)
|
|
||||||
.Where(m => userRooms.Contains(m.ChatRoomId))
|
.Where(m => userRooms.Contains(m.ChatRoomId))
|
||||||
.GroupBy(m => m.ChatRoomId)
|
.GroupBy(m => m.ChatRoomId)
|
||||||
.Select(g => g.OrderByDescending(m => m.CreatedAt).FirstOrDefault())
|
.Select(g => g.OrderByDescending(m => m.CreatedAt).FirstOrDefault())
|
||||||
@@ -450,8 +449,6 @@ public partial class ChatService(
|
|||||||
var changes = await db.ChatMessages
|
var changes = await db.ChatMessages
|
||||||
.IgnoreQueryFilters()
|
.IgnoreQueryFilters()
|
||||||
.Include(e => e.Sender)
|
.Include(e => e.Sender)
|
||||||
.Include(e => e.Sender.Account)
|
|
||||||
.Include(e => e.Sender.Account.Profile)
|
|
||||||
.Where(m => m.ChatRoomId == roomId)
|
.Where(m => m.ChatRoomId == roomId)
|
||||||
.Where(m => m.UpdatedAt > timestamp || m.DeletedAt > timestamp)
|
.Where(m => m.UpdatedAt > timestamp || m.DeletedAt > timestamp)
|
||||||
.Select(m => new MessageChange
|
.Select(m => new MessageChange
|
||||||
@@ -463,6 +460,20 @@ public partial class ChatService(
|
|||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
var changesMembers = changes
|
||||||
|
.Select(c => c.Message!.Sender)
|
||||||
|
.DistinctBy(x => x.Id)
|
||||||
|
.ToList();
|
||||||
|
changesMembers = await crs.LoadMemberAccounts(changesMembers);
|
||||||
|
|
||||||
|
foreach (var change in changes)
|
||||||
|
{
|
||||||
|
if (change.Message == null) continue;
|
||||||
|
var sender = changesMembers.FirstOrDefault(x => x.Id == change.Message.SenderId);
|
||||||
|
if (sender is not null)
|
||||||
|
change.Message.Sender = sender;
|
||||||
|
}
|
||||||
|
|
||||||
return new SyncResponse
|
return new SyncResponse
|
||||||
{
|
{
|
||||||
Changes = changes,
|
Changes = changes,
|
||||||
@@ -493,18 +504,16 @@ public partial class ChatService(
|
|||||||
|
|
||||||
if (attachmentsId is not null)
|
if (attachmentsId is not null)
|
||||||
{
|
{
|
||||||
var messageResourceId = $"message:{message.Id}";
|
|
||||||
|
|
||||||
// Delete existing references for this message
|
// Delete existing references for this message
|
||||||
await fileRefs.DeleteResourceReferencesAsync(
|
await fileRefs.DeleteResourceReferencesAsync(
|
||||||
new DeleteResourceReferencesRequest { ResourceId = messageResourceId }
|
new DeleteResourceReferencesRequest { ResourceId = message.ResourceIdentifier }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create new references for each attachment
|
// Create new references for each attachment
|
||||||
var createRequest = new CreateReferenceBatchRequest
|
var createRequest = new CreateReferenceBatchRequest
|
||||||
{
|
{
|
||||||
Usage = ChatFileUsageIdentifier,
|
Usage = ChatFileUsageIdentifier,
|
||||||
ResourceId = messageResourceId,
|
ResourceId = message.ResourceIdentifier,
|
||||||
};
|
};
|
||||||
createRequest.FilesId.AddRange(attachmentsId);
|
createRequest.FilesId.AddRange(attachmentsId);
|
||||||
await fileRefs.CreateReferenceBatchAsync(createRequest);
|
await fileRefs.CreateReferenceBatchAsync(createRequest);
|
||||||
|
@@ -62,8 +62,6 @@ public class RealtimeCallController(
|
|||||||
.Where(c => c.EndedAt == null)
|
.Where(c => c.EndedAt == null)
|
||||||
.Include(c => c.Room)
|
.Include(c => c.Room)
|
||||||
.Include(c => c.Sender)
|
.Include(c => c.Sender)
|
||||||
.ThenInclude(c => c.Account)
|
|
||||||
.ThenInclude(c => c.Profile)
|
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (ongoingCall is null) return NotFound();
|
if (ongoingCall is null) return NotFound();
|
||||||
return Ok(ongoingCall);
|
return Ok(ongoingCall);
|
||||||
|
@@ -17,21 +17,16 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AngleSharp" Version="1.3.0"/>
|
<PackageReference Include="AngleSharp" Version="1.3.0"/>
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
|
||||||
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
|
|
||||||
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1"/>
|
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1"/>
|
||||||
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1"/>
|
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1"/>
|
||||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0"/>
|
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0"/>
|
||||||
<PackageReference Include="FFMpegCore" Version="5.2.0" />
|
|
||||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
|
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.1"/>
|
<PackageReference Include="HtmlAgilityPack" Version="1.12.1"/>
|
||||||
<PackageReference Include="Livekit.Server.Sdk.Dotnet" Version="1.0.8" />
|
<PackageReference Include="Livekit.Server.Sdk.Dotnet" Version="1.0.10" />
|
||||||
<PackageReference Include="MailKit" Version="4.13.0" />
|
|
||||||
<PackageReference Include="Markdig" Version="0.41.3"/>
|
<PackageReference Include="Markdig" Version="0.41.3"/>
|
||||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7"/>
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
@@ -40,14 +35,10 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Minio" Version="6.0.5" />
|
|
||||||
<PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115">
|
<PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="NetVips" Version="3.1.0" />
|
|
||||||
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.1" />
|
|
||||||
<PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.1" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||||
<PackageReference Include="NodaTime" Version="3.2.2"/>
|
<PackageReference Include="NodaTime" Version="3.2.2"/>
|
||||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0"/>
|
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0"/>
|
||||||
@@ -61,7 +52,6 @@
|
|||||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0"/>
|
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0"/>
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/>
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/>
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0"/>
|
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0"/>
|
||||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
|
||||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
|
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
|
||||||
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1"/>
|
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1"/>
|
||||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1"/>
|
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1"/>
|
||||||
@@ -70,16 +60,11 @@
|
|||||||
<PackageReference Include="Quartz" Version="3.14.0"/>
|
<PackageReference Include="Quartz" Version="3.14.0"/>
|
||||||
<PackageReference Include="Quartz.AspNetCore" Version="3.14.0"/>
|
<PackageReference Include="Quartz.AspNetCore" Version="3.14.0"/>
|
||||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0"/>
|
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0"/>
|
||||||
<PackageReference Include="SkiaSharp" Version="3.119.0" />
|
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="3.119.0" />
|
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.0" />
|
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.macOS" Version="3.119.0" />
|
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.8.41"/>
|
<PackageReference Include="StackExchange.Redis" Version="2.8.41"/>
|
||||||
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0"/>
|
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0"/>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3"/>
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3"/>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3"/>
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3"/>
|
||||||
<PackageReference Include="System.ServiceModel.Syndication" Version="9.0.6" />
|
<PackageReference Include="System.ServiceModel.Syndication" Version="9.0.7" />
|
||||||
<PackageReference Include="tusdotnet" Version="2.10.0" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -89,7 +74,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Connection\" />
|
|
||||||
<Folder Include="Discovery\"/>
|
<Folder Include="Discovery\"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -182,6 +166,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj"/>
|
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj"/>
|
||||||
|
<ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
1882
DysonNetwork.Sphere/Migrations/20250717135738_InitialMigration.Designer.cs
generated
Normal file
1882
DysonNetwork.Sphere/Migrations/20250717135738_InitialMigration.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
1018
DysonNetwork.Sphere/Migrations/20250717135738_InitialMigration.cs
Normal file
1018
DysonNetwork.Sphere/Migrations/20250717135738_InitialMigration.cs
Normal file
File diff suppressed because it is too large
Load Diff
1879
DysonNetwork.Sphere/Migrations/AppDatabaseModelSnapshot.cs
Normal file
1879
DysonNetwork.Sphere/Migrations/AppDatabaseModelSnapshot.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@ using DysonNetwork.Shared.Registry;
|
|||||||
using DysonNetwork.Sphere;
|
using DysonNetwork.Sphere;
|
||||||
using DysonNetwork.Sphere.Startup;
|
using DysonNetwork.Sphere.Startup;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using tusdotnet.Stores;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
@@ -502,8 +502,6 @@ public class PublisherController(
|
|||||||
|
|
||||||
var publisher = await db.Publishers
|
var publisher = await db.Publishers
|
||||||
.Where(p => p.Name == name)
|
.Where(p => p.Name == name)
|
||||||
.Include(publisher => publisher.Picture)
|
|
||||||
.Include(publisher => publisher.Background)
|
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (publisher is null) return NotFound();
|
if (publisher is null) return NotFound();
|
||||||
|
|
||||||
|
@@ -1,696 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using DysonNetwork.Shared.Data;
|
|
||||||
using DysonNetwork.Shared.Proto;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using NodaTime;
|
|
||||||
using Google.Protobuf.WellKnownTypes;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Realm;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("/api/realms")]
|
|
||||||
public class RealmController(
|
|
||||||
AppDatabase db,
|
|
||||||
RealmService rs,
|
|
||||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
|
||||||
ActionLogService.ActionLogServiceClient als,
|
|
||||||
AccountService.AccountServiceClient accounts
|
|
||||||
) : Controller
|
|
||||||
{
|
|
||||||
[HttpGet("{slug}")]
|
|
||||||
public async Task<ActionResult<Realm>> GetRealm(string slug)
|
|
||||||
{
|
|
||||||
var realm = await db.Realms
|
|
||||||
.Where(e => e.Slug == slug)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (realm is null) return NotFound();
|
|
||||||
|
|
||||||
return Ok(realm);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<List<Realm>>> ListJoinedRealms()
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
|
|
||||||
var members = await db.RealmMembers
|
|
||||||
.Where(m => m.AccountId == accountId)
|
|
||||||
.Where(m => m.JoinedAt != null)
|
|
||||||
.Where(m => m.LeaveAt == null)
|
|
||||||
.Include(e => e.Realm)
|
|
||||||
.Select(m => m.Realm)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return members.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("invites")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<List<RealmMember>>> ListInvites()
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
|
|
||||||
var members = await db.RealmMembers
|
|
||||||
.Where(m => m.AccountId == accountId)
|
|
||||||
.Where(m => m.JoinedAt == null)
|
|
||||||
.Include(e => e.Realm)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return members.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RealmMemberRequest
|
|
||||||
{
|
|
||||||
[Required] public Guid RelatedUserId { get; set; }
|
|
||||||
[Required] public int Role { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("invites/{slug}")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<RealmMember>> InviteMember(string slug,
|
|
||||||
[FromBody] RealmMemberRequest request)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
|
|
||||||
var relatedUser =
|
|
||||||
await accounts.GetAccountAsync(new GetAccountRequest { Id = request.RelatedUserId.ToString() });
|
|
||||||
if (relatedUser == null) return BadRequest("Related user was not found");
|
|
||||||
|
|
||||||
var hasBlocked = await accounts.HasRelationshipAsync(new GetRelationshipRequest()
|
|
||||||
{
|
|
||||||
AccountId = currentUser.Id,
|
|
||||||
RelatedId = request.RelatedUserId.ToString(),
|
|
||||||
Status = -100
|
|
||||||
});
|
|
||||||
if (hasBlocked?.Value ?? false)
|
|
||||||
return StatusCode(403, "You cannot invite a user that blocked you.");
|
|
||||||
|
|
||||||
var realm = await db.Realms
|
|
||||||
.Where(p => p.Slug == slug)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (realm is null) return NotFound();
|
|
||||||
|
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, accountId, request.Role))
|
|
||||||
return StatusCode(403, "You cannot invite member has higher permission than yours.");
|
|
||||||
|
|
||||||
var hasExistingMember = await db.RealmMembers
|
|
||||||
.Where(m => m.AccountId == Guid.Parse(relatedUser.Id))
|
|
||||||
.Where(m => m.RealmId == realm.Id)
|
|
||||||
.Where(m => m.LeaveAt == null)
|
|
||||||
.AnyAsync();
|
|
||||||
if (hasExistingMember)
|
|
||||||
return BadRequest("This user has been joined the realm or leave cannot be invited again.");
|
|
||||||
|
|
||||||
var member = new RealmMember
|
|
||||||
{
|
|
||||||
AccountId = Guid.Parse(relatedUser.Id),
|
|
||||||
RealmId = realm.Id,
|
|
||||||
Role = request.Role,
|
|
||||||
};
|
|
||||||
|
|
||||||
db.RealmMembers.Add(member);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "realms.members.invite",
|
|
||||||
Meta =
|
|
||||||
{
|
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
|
||||||
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
|
||||||
{ "role", Value.ForNumber(request.Role) }
|
|
||||||
},
|
|
||||||
AccountId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
member.Account = relatedUser;
|
|
||||||
member.Realm = realm;
|
|
||||||
await rs.SendInviteNotify(member);
|
|
||||||
|
|
||||||
return Ok(member);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("invites/{slug}/accept")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<Realm>> AcceptMemberInvite(string slug)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
|
|
||||||
var member = await db.RealmMembers
|
|
||||||
.Where(m => m.AccountId == accountId)
|
|
||||||
.Where(m => m.Realm.Slug == slug)
|
|
||||||
.Where(m => m.JoinedAt == null)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (member is null) return NotFound();
|
|
||||||
|
|
||||||
member.JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow);
|
|
||||||
db.Update(member);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "realms.members.join",
|
|
||||||
Meta =
|
|
||||||
{
|
|
||||||
{ "realm_id", Value.ForString(member.RealmId.ToString()) },
|
|
||||||
{ "account_id", Value.ForString(member.AccountId.ToString()) }
|
|
||||||
},
|
|
||||||
AccountId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(member);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("invites/{slug}/decline")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult> DeclineMemberInvite(string slug)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
|
|
||||||
var member = await db.RealmMembers
|
|
||||||
.Where(m => m.AccountId == accountId)
|
|
||||||
.Where(m => m.Realm.Slug == slug)
|
|
||||||
.Where(m => m.JoinedAt == null)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (member is null) return NotFound();
|
|
||||||
|
|
||||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(
|
|
||||||
ActionLogType.RealmLeave,
|
|
||||||
new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } },
|
|
||||||
Request
|
|
||||||
);
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("{slug}/members")]
|
|
||||||
public async Task<ActionResult<List<RealmMember>>> ListMembers(
|
|
||||||
string slug,
|
|
||||||
[FromQuery] int offset = 0,
|
|
||||||
[FromQuery] int take = 20,
|
|
||||||
[FromQuery] bool withStatus = false,
|
|
||||||
[FromQuery] string? status = null
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var realm = await db.Realms
|
|
||||||
.Where(r => r.Slug == slug)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (realm is null) return NotFound();
|
|
||||||
|
|
||||||
if (!realm.IsPublic)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Normal))
|
|
||||||
return StatusCode(403, "You must be a member to view this realm's members.");
|
|
||||||
}
|
|
||||||
|
|
||||||
IQueryable<RealmMember> query = db.RealmMembers
|
|
||||||
.Where(m => m.RealmId == realm.Id)
|
|
||||||
.Where(m => m.LeaveAt == null)
|
|
||||||
.Include(m => m.Account)
|
|
||||||
.Include(m => m.Account.Profile);
|
|
||||||
|
|
||||||
if (withStatus)
|
|
||||||
{
|
|
||||||
var members = await query
|
|
||||||
.OrderBy(m => m.CreatedAt)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var memberStatuses = await aes.GetStatuses(members.Select(m => m.AccountId).ToList());
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(status))
|
|
||||||
{
|
|
||||||
members = members.Where(m =>
|
|
||||||
memberStatuses.TryGetValue(m.AccountId, out var s) && s.Label != null &&
|
|
||||||
s.Label.Equals(status, StringComparison.OrdinalIgnoreCase)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
members = members.OrderByDescending(m => memberStatuses.TryGetValue(m.AccountId, out var s) && s.IsOnline)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var total = members.Count;
|
|
||||||
Response.Headers["X-Total"] = total.ToString();
|
|
||||||
|
|
||||||
var result = members.Skip(offset).Take(take).ToList();
|
|
||||||
|
|
||||||
return Ok(result);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var total = await query.CountAsync();
|
|
||||||
Response.Headers["X-Total"] = total.ToString();
|
|
||||||
|
|
||||||
var members = await query
|
|
||||||
.OrderBy(m => m.CreatedAt)
|
|
||||||
.Skip(offset)
|
|
||||||
.Take(take)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return Ok(members);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("{slug}/members/me")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<RealmMember>> GetCurrentIdentity(string slug)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
|
|
||||||
var member = await db.RealmMembers
|
|
||||||
.Where(m => m.AccountId == accountId)
|
|
||||||
.Where(m => m.Realm.Slug == slug)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if (member is null) return NotFound();
|
|
||||||
return Ok(member);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{slug}/members/me")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult> LeaveRealm(string slug)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
|
|
||||||
var member = await db.RealmMembers
|
|
||||||
.Where(m => m.AccountId == accountId)
|
|
||||||
.Where(m => m.Realm.Slug == slug)
|
|
||||||
.Where(m => m.JoinedAt != null)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (member is null) return NotFound();
|
|
||||||
|
|
||||||
if (member.Role == RealmMemberRole.Owner)
|
|
||||||
return StatusCode(403, "Owner cannot leave their own realm.");
|
|
||||||
|
|
||||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(
|
|
||||||
ActionLogType.RealmLeave,
|
|
||||||
new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } },
|
|
||||||
Request
|
|
||||||
);
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RealmRequest
|
|
||||||
{
|
|
||||||
[MaxLength(1024)] public string? Slug { get; set; }
|
|
||||||
[MaxLength(1024)] public string? Name { get; set; }
|
|
||||||
[MaxLength(4096)] public string? Description { get; set; }
|
|
||||||
public string? PictureId { get; set; }
|
|
||||||
public string? BackgroundId { get; set; }
|
|
||||||
public bool? IsCommunity { get; set; }
|
|
||||||
public bool? IsPublic { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<Realm>> CreateRealm(RealmRequest request)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
if (string.IsNullOrWhiteSpace(request.Name)) return BadRequest("You cannot create a realm without a name.");
|
|
||||||
if (string.IsNullOrWhiteSpace(request.Slug)) return BadRequest("You cannot create a realm without a slug.");
|
|
||||||
|
|
||||||
var slugExists = await db.Realms.AnyAsync(r => r.Slug == request.Slug);
|
|
||||||
if (slugExists) return BadRequest("Realm with this slug already exists.");
|
|
||||||
|
|
||||||
var realm = new Realm
|
|
||||||
{
|
|
||||||
Name = request.Name!,
|
|
||||||
Slug = request.Slug!,
|
|
||||||
Description = request.Description!,
|
|
||||||
AccountId = currentUser.Id,
|
|
||||||
IsCommunity = request.IsCommunity ?? false,
|
|
||||||
IsPublic = request.IsPublic ?? false,
|
|
||||||
Members = new List<RealmMember>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Role = RealmMemberRole.Owner,
|
|
||||||
AccountId = Guid.Parse(currentUser.Id),
|
|
||||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (request.PictureId is not null)
|
|
||||||
{
|
|
||||||
var pictureResult = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId });
|
|
||||||
if (pictureResult is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
|
||||||
realm.Picture = CloudFileReferenceObject.FromProtoValue(pictureResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.BackgroundId is not null)
|
|
||||||
{
|
|
||||||
var backgroundResult = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId });
|
|
||||||
if (backgroundResult is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
|
||||||
realm.Background = CloudFileReferenceObject.FromProtoValue(backgroundResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
db.Realms.Add(realm);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "realms.create",
|
|
||||||
Meta =
|
|
||||||
{
|
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
|
||||||
{ "name", Value.ForString(realm.Name) },
|
|
||||||
{ "slug", Value.ForString(realm.Slug) },
|
|
||||||
{ "is_community", Value.ForBool(realm.IsCommunity) },
|
|
||||||
{ "is_public", Value.ForBool(realm.IsPublic) }
|
|
||||||
},
|
|
||||||
UserId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
var realmResourceId = $"realm:{realm.Id}";
|
|
||||||
|
|
||||||
if (realm.Picture is not null)
|
|
||||||
{
|
|
||||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
|
||||||
{
|
|
||||||
FileId = realm.Picture.Id,
|
|
||||||
Usage = "realm.picture",
|
|
||||||
ResourceId = realmResourceId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (realm.Background is not null)
|
|
||||||
{
|
|
||||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
|
||||||
{
|
|
||||||
FileId = realm.Background.Id,
|
|
||||||
Usage = "realm.background",
|
|
||||||
ResourceId = realmResourceId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(realm);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPatch("{slug}")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<Realm>> Update(string slug, [FromBody] RealmRequest request)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
|
|
||||||
var realm = await db.Realms
|
|
||||||
.Where(r => r.Slug == slug)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (realm is null) return NotFound();
|
|
||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
var member = await db.RealmMembers
|
|
||||||
.Where(m => m.AccountId == accountId && m.RealmId == realm.Id && m.JoinedAt != null)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (member is null || member.Role < RealmMemberRole.Moderator)
|
|
||||||
return StatusCode(403, "You do not have permission to update this realm.");
|
|
||||||
|
|
||||||
if (request.Slug is not null && request.Slug != realm.Slug)
|
|
||||||
{
|
|
||||||
var slugExists = await db.Realms.AnyAsync(r => r.Slug == request.Slug);
|
|
||||||
if (slugExists) return BadRequest("Realm with this slug already exists.");
|
|
||||||
realm.Slug = request.Slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.Name is not null)
|
|
||||||
realm.Name = request.Name;
|
|
||||||
if (request.Description is not null)
|
|
||||||
realm.Description = request.Description;
|
|
||||||
if (request.IsCommunity is not null)
|
|
||||||
realm.IsCommunity = request.IsCommunity.Value;
|
|
||||||
if (request.IsPublic is not null)
|
|
||||||
realm.IsPublic = request.IsPublic.Value;
|
|
||||||
|
|
||||||
if (request.PictureId is not null)
|
|
||||||
{
|
|
||||||
var pictureResult = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId });
|
|
||||||
if (pictureResult is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
|
||||||
|
|
||||||
// Remove old references for the realm picture
|
|
||||||
if (realm.Picture is not null)
|
|
||||||
{
|
|
||||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
|
||||||
{
|
|
||||||
ResourceId = realm.ResourceIdentifier
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
realm.Picture = CloudFileReferenceObject.FromProtoValue(pictureResult);
|
|
||||||
|
|
||||||
// Create a new reference
|
|
||||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
|
||||||
{
|
|
||||||
FileId = realm.Picture.Id,
|
|
||||||
Usage = "realm.picture",
|
|
||||||
ResourceId = realm.ResourceIdentifier
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.BackgroundId is not null)
|
|
||||||
{
|
|
||||||
var backgroundResult = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId });
|
|
||||||
if (backgroundResult is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
|
||||||
|
|
||||||
// Remove old references for the realm background
|
|
||||||
if (realm.Background is not null)
|
|
||||||
{
|
|
||||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
|
||||||
{
|
|
||||||
ResourceId = realm.ResourceIdentifier
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
realm.Background = CloudFileReferenceObject.FromProtoValue(backgroundResult);
|
|
||||||
|
|
||||||
// Create a new reference
|
|
||||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
|
||||||
{
|
|
||||||
FileId = realm.Background.Id,
|
|
||||||
Usage = "realm.background",
|
|
||||||
ResourceId = realm.ResourceIdentifier
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
db.Realms.Update(realm);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "realms.update",
|
|
||||||
Meta =
|
|
||||||
{
|
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
|
||||||
{ "name_updated", Value.ForBool(request.Name != null) },
|
|
||||||
{ "slug_updated", Value.ForBool(request.Slug != null) },
|
|
||||||
{ "description_updated", Value.ForBool(request.Description != null) },
|
|
||||||
{ "picture_updated", Value.ForBool(request.PictureId != null) },
|
|
||||||
{ "background_updated", Value.ForBool(request.BackgroundId != null) },
|
|
||||||
{ "is_community_updated", Value.ForBool(request.IsCommunity != null) },
|
|
||||||
{ "is_public_updated", Value.ForBool(request.IsPublic != null) }
|
|
||||||
},
|
|
||||||
UserId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(realm);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{slug}/members/me")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<RealmMember>> JoinRealm(string slug)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
|
|
||||||
var realm = await db.Realms
|
|
||||||
.Where(r => r.Slug == slug)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (realm is null) return NotFound();
|
|
||||||
|
|
||||||
if (!realm.IsCommunity)
|
|
||||||
return StatusCode(403, "Only community realms can be joined without invitation.");
|
|
||||||
|
|
||||||
var existingMember = await db.RealmMembers
|
|
||||||
.Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.RealmId == realm.Id)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (existingMember is not null)
|
|
||||||
return BadRequest("You are already a member of this realm.");
|
|
||||||
|
|
||||||
var member = new RealmMember
|
|
||||||
{
|
|
||||||
AccountId = currentUser.Id,
|
|
||||||
RealmId = realm.Id,
|
|
||||||
Role = RealmMemberRole.Normal,
|
|
||||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
|
||||||
};
|
|
||||||
|
|
||||||
db.RealmMembers.Add(member);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "realms.members.join",
|
|
||||||
Meta =
|
|
||||||
{
|
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
|
||||||
{ "account_id", Value.ForString(currentUser.Id) },
|
|
||||||
{ "is_community", Value.ForBool(realm.IsCommunity) }
|
|
||||||
},
|
|
||||||
UserId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(member);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{slug}/members/{memberId:guid}")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult> RemoveMember(string slug, Guid memberId)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
|
|
||||||
var realm = await db.Realms
|
|
||||||
.Where(r => r.Slug == slug)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (realm is null) return NotFound();
|
|
||||||
|
|
||||||
var member = await db.RealmMembers
|
|
||||||
.Where(m => m.AccountId == memberId && m.RealmId == realm.Id)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (member is null) return NotFound();
|
|
||||||
|
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role))
|
|
||||||
return StatusCode(403, "You do not have permission to remove members from this realm.");
|
|
||||||
|
|
||||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "realms.members.kick",
|
|
||||||
Meta =
|
|
||||||
{
|
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
|
||||||
{ "account_id", Value.ForString(memberId.ToString()) },
|
|
||||||
{ "kicker_id", Value.ForString(currentUser.Id) }
|
|
||||||
},
|
|
||||||
UserId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPatch("{slug}/members/{memberId:guid}/role")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<RealmMember>> UpdateMemberRole(string slug, Guid memberId, [FromBody] int newRole)
|
|
||||||
{
|
|
||||||
if (newRole >= RealmMemberRole.Owner) return BadRequest("Unable to set realm member to owner or greater role.");
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
|
|
||||||
var realm = await db.Realms
|
|
||||||
.Where(r => r.Slug == slug)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (realm is null) return NotFound();
|
|
||||||
|
|
||||||
var member = await db.RealmMembers
|
|
||||||
.Where(m => m.AccountId == memberId && m.RealmId == realm.Id)
|
|
||||||
.Include(m => m.Account)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (member is null) return NotFound();
|
|
||||||
|
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role,
|
|
||||||
newRole))
|
|
||||||
return StatusCode(403, "You do not have permission to update member roles in this realm.");
|
|
||||||
|
|
||||||
member.Role = newRole;
|
|
||||||
db.RealmMembers.Update(member);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "realms.members.role_update",
|
|
||||||
Meta =
|
|
||||||
{
|
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
|
||||||
{ "account_id", Value.ForString(memberId.ToString()) },
|
|
||||||
{ "new_role", Value.ForNumber(newRole) },
|
|
||||||
{ "updater_id", Value.ForString(currentUser.Id) }
|
|
||||||
},
|
|
||||||
UserId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(member);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{slug}")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult> Delete(string slug)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
|
|
||||||
var realm = await db.Realms
|
|
||||||
.Where(r => r.Slug == slug)
|
|
||||||
.Include(r => r.Picture)
|
|
||||||
.Include(r => r.Background)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (realm is null) return NotFound();
|
|
||||||
|
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Owner))
|
|
||||||
return StatusCode(403, "Only the owner can delete this realm.");
|
|
||||||
|
|
||||||
db.Realms.Remove(realm);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "realms.delete",
|
|
||||||
Meta =
|
|
||||||
{
|
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
|
||||||
{ "realm_name", Value.ForString(realm.Name) },
|
|
||||||
{ "realm_slug", Value.ForString(realm.Slug) }
|
|
||||||
},
|
|
||||||
UserId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete all file references for this realm
|
|
||||||
var realmResourceId = $"realm:{realm.Id}";
|
|
||||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
|
||||||
{
|
|
||||||
ResourceId = realmResourceId
|
|
||||||
});
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -2,7 +2,6 @@ using System.Net;
|
|||||||
using DysonNetwork.Shared.Auth;
|
using DysonNetwork.Shared.Auth;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Prometheus;
|
using Prometheus;
|
||||||
using tusdotnet;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Startup;
|
namespace DysonNetwork.Sphere.Startup;
|
||||||
|
|
||||||
|
@@ -17,11 +17,9 @@ using System.Threading.RateLimiting;
|
|||||||
using DysonNetwork.Shared.Auth;
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.GeoIp;
|
using DysonNetwork.Shared.GeoIp;
|
||||||
using DysonNetwork.Shared.Proto;
|
|
||||||
using DysonNetwork.Sphere.WebReader;
|
using DysonNetwork.Sphere.WebReader;
|
||||||
using DysonNetwork.Sphere.Developer;
|
using DysonNetwork.Sphere.Developer;
|
||||||
using DysonNetwork.Sphere.Discovery;
|
using DysonNetwork.Sphere.Discovery;
|
||||||
using tusdotnet.Stores;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Startup;
|
namespace DysonNetwork.Sphere.Startup;
|
||||||
|
|
||||||
|
@@ -16,22 +16,10 @@
|
|||||||
"GeoIp": {
|
"GeoIp": {
|
||||||
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
|
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
|
||||||
},
|
},
|
||||||
"Oidc": {
|
"RealtimeChat": {
|
||||||
"Google": {
|
"Endpoint": "https://solar-network-im44o8gq.livekit.cloud",
|
||||||
"ClientId": "961776991058-963m1qin2vtp8fv693b5fdrab5hmpl89.apps.googleusercontent.com",
|
"ApiKey": "APIs6TiL8wj3A4j",
|
||||||
"ClientSecret": ""
|
"ApiSecret": "SffxRneIwTnlHPtEf3zicmmv3LUEl7xXael4PvWZrEhE"
|
||||||
},
|
|
||||||
"Apple": {
|
|
||||||
"ClientId": "dev.solsynth.solian",
|
|
||||||
"TeamId": "W7HPZ53V6B",
|
|
||||||
"KeyId": "B668YP4KBG",
|
|
||||||
"PrivateKeyPath": "./Keys/Solarpass.p8"
|
|
||||||
},
|
|
||||||
"Microsoft": {
|
|
||||||
"ClientId": "YOUR_MICROSOFT_CLIENT_ID",
|
|
||||||
"ClientSecret": "YOUR_MICROSOFT_CLIENT_SECRET",
|
|
||||||
"DiscoveryEndpoint": "YOUR_MICROSOFT_DISCOVERY_ENDPOINT"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"KnownProxies": [
|
"KnownProxies": [
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
|
Reference in New Issue
Block a user