🐛 Fix stuff I think

This commit is contained in:
2025-07-18 12:20:47 +08:00
parent 651820e384
commit 086a12f971
23 changed files with 5114 additions and 850 deletions

View File

@@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore;
using NodaTime;
using NodaTime.Serialization.Protobuf;
using OtpNet;
using VerificationMark = DysonNetwork.Shared.Data.VerificationMark;
namespace DysonNetwork.Pass.Account;
@@ -41,7 +42,9 @@ public class Account : ModelBase
Language = Language,
ActivatedAt = ActivatedAt?.ToTimestamp(),
IsSuperuser = IsSuperuser,
Profile = Profile.ToProtoValue()
Profile = Profile.ToProtoValue(),
CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp()
};
// Add contacts
@@ -54,6 +57,32 @@ public class Account : ModelBase
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
@@ -132,12 +161,42 @@ public class AccountProfile : ModelBase, IIdentifiedResource
Background = Background?.ToProtoValue(),
AccountId = AccountId.ToString(),
Verification = Verification?.ToProtoValue(),
ActiveBadge = ActiveBadge?.ToProtoValue()
ActiveBadge = ActiveBadge?.ToProtoValue(),
CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp()
};
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}";
}
@@ -167,11 +226,36 @@ public class AccountContact : ModelBase
Content = Content,
IsPrimary = IsPrimary,
VerifiedAt = VerifiedAt?.ToTimestamp(),
AccountId = AccountId.ToString()
AccountId = AccountId.ToString(),
CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp()
};
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

View File

@@ -49,6 +49,7 @@ public class AccountServiceGrpc(
var accounts = await _db.Accounts
.AsNoTracking()
.Where(a => accountIds.Contains(a.Id))
.Include(a => a.Profile)
.ToListAsync();
var response = new GetAccountBatchResponse();

View File

@@ -15,7 +15,7 @@ public class AccountBadge : ModelBase
[MaxLength(1024)] public string Type { get; set; } = null!;
[MaxLength(1024)] public string? Label { 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? ExpiredAt { get; set; }
@@ -33,7 +33,7 @@ public class AccountBadge : ModelBase
Meta = Meta,
ActivatedAt = ActivatedAt,
ExpiredAt = ExpiredAt,
AccountId = AccountId
AccountId = AccountId,
};
}
@@ -48,11 +48,31 @@ public class AccountBadge : ModelBase
ActivatedAt = ActivatedAt?.ToTimestamp(),
ExpiredAt = ExpiredAt?.ToTimestamp(),
AccountId = AccountId.ToString(),
CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp()
};
proto.Meta.Add(GrpcTypeHelper.ConvertToValueMap(Meta));
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
@@ -61,7 +81,7 @@ public class BadgeReferenceObject : ModelBase
public string Type { get; set; } = null!;
public string? Label { 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? ExpiredAt { get; set; }
public Guid AccountId { get; set; }
@@ -78,9 +98,26 @@ public class BadgeReferenceObject : ModelBase
ExpiredAt = ExpiredAt?.ToTimestamp(),
AccountId = AccountId.ToString()
};
if (Meta is not null)
proto.Meta.Add(GrpcTypeHelper.ConvertToValueMap(Meta!));
proto.Meta.Add(GrpcTypeHelper.ConvertToValueMap(Meta!));
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;
}
}

View File

@@ -36,6 +36,28 @@ public class VerificationMark
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

View File

@@ -6,7 +6,6 @@ option csharp_namespace = "DysonNetwork.Shared.Proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/struct.proto";
@@ -28,6 +27,9 @@ message Account {
repeated AccountConnection connections = 11;
repeated Relationship outgoing_relationships = 12;
repeated Relationship incoming_relationships = 13;
google.protobuf.Timestamp created_at = 14;
google.protobuf.Timestamp updated_at = 15;
}
// Profile contains detailed information about a user
@@ -55,6 +57,9 @@ message AccountProfile {
CloudFile background = 20;
string account_id = 21;
google.protobuf.Timestamp created_at = 22;
google.protobuf.Timestamp updated_at = 23;
}
// AccountContact represents a contact method for an account
@@ -65,6 +70,9 @@ message AccountContact {
bool is_primary = 4;
string content = 5;
string account_id = 6;
google.protobuf.Timestamp created_at = 7;
google.protobuf.Timestamp updated_at = 8;
}
// Enum for contact types
@@ -85,7 +93,10 @@ message AccountAuthFactor {
google.protobuf.Timestamp enabled_at = 6;
google.protobuf.Timestamp expired_at = 7;
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
@@ -108,6 +119,9 @@ message AccountBadge {
google.protobuf.Timestamp activated_at = 6; // When the badge was activated
google.protobuf.Timestamp expired_at = 7; // Optional expiration time
string account_id = 8; // ID of the account this badge belongs to
google.protobuf.Timestamp created_at = 9;
google.protobuf.Timestamp updated_at = 10;
}
// 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.Timestamp last_used_at = 7;
string account_id = 8;
google.protobuf.Timestamp created_at = 9;
google.protobuf.Timestamp updated_at = 10;
}
// VerificationMark represents verification status
@@ -128,6 +145,9 @@ message VerificationMark {
string title = 2;
string description = 3;
string verified_by = 4;
google.protobuf.Timestamp created_at = 5;
google.protobuf.Timestamp updated_at = 6;
}
enum VerificationMarkType {
@@ -160,6 +180,7 @@ message Relationship {
optional Account account = 3;
optional Account related = 4;
int32 status = 5;
google.protobuf.Timestamp created_at = 6;
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
string account_id = 7; // The account that performed the action
google.protobuf.StringValue session_id = 8; // The session in which the action was performed
google.protobuf.Timestamp created_at = 9; // When the action log was created
}

View 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();
}
}

View File

@@ -41,6 +41,7 @@ public static class ServiceHelper
.GetAwaiter()
.GetResult();
});
services.AddSingleton<AccountClientHelper>();
services.AddSingleton<ActionLogService.ActionLogServiceClient>(sp =>
{

View File

@@ -97,8 +97,6 @@ public partial class ChatController(
.Where(m => m.ChatRoomId == roomId)
.OrderByDescending(m => m.CreatedAt)
.Include(m => m.Sender)
.Include(m => m.Sender.Account)
.Include(m => m.Sender.Account.Profile)
.Skip(offset)
.Take(take)
.ToListAsync();
@@ -232,8 +230,6 @@ public partial class ChatController(
var message = await db.ChatMessages
.Include(m => m.Sender)
.Include(m => m.Sender.Account)
.Include(m => m.Sender.Account.Profile)
.Include(message => message.ChatRoom)
.FirstOrDefaultAsync(m => m.Id == messageId && m.ChatRoomId == roomId);

View File

@@ -2,8 +2,8 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Proto;
using NodaTime;
using Account = DysonNetwork.Pass.Account.Account;
namespace DysonNetwork.Sphere.Chat;

View File

@@ -194,7 +194,7 @@ public class ChatRoomController(
{
Role = ChatMemberRole.Owner,
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
.Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.ChatRoomId == roomId)
.Include(m => m.Account)
.Include(m => m.Account.Profile)
.FirstOrDefaultAsync();
if (member == null)
return NotFound();
return Ok(member);
return Ok(await crs.LoadMemberAccount(member));
}
[HttpGet("{roomId:guid}/members")]
public async Task<ActionResult<List<ChatMember>>> ListMembers(Guid roomId, [FromQuery] int take = 20,
[FromQuery] int skip = 0, [FromQuery] bool withStatus = false, [FromQuery] string? status = null)
public async Task<ActionResult<List<ChatMember>>> ListMembers(Guid roomId,
[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
.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.");
}
IQueryable<ChatMember> query = db.ChatMembers
var query = db.ChatMembers
.Where(m => m.ChatRoomId == roomId)
.Where(m => m.LeaveAt == null) // Add this condition to exclude left members
.Include(m => m.Account)
.Include(m => m.Account.Profile);
.Where(m => m.LeaveAt == null);
// if (withStatus)
// {
@@ -509,7 +509,7 @@ public class ChatRoomController(
//
// var result = members.Skip(skip).Take(take).ToList();
//
// return Ok(result);
// return Ok(await crs.LoadMemberAccounts(result));
// }
// else
// {
@@ -522,7 +522,7 @@ public class ChatRoomController(
.Take(take)
.ToListAsync();
return Ok(members);
return Ok(await crs.LoadMemberAccounts(members));
// }
}
@@ -952,7 +952,7 @@ public class ChatRoomController(
? localizer["ChatInviteDirectBody", sender.Nick]
: localizer["ChatInviteBody", member.ChatRoom.Name ?? "Unnamed"];
CultureService.SetCultureInfo(member.Account);
CultureService.SetCultureInfo(member.Account.Language);
await pusher.SendPushNotificationToUserAsync(
new SendPushNotificationToUserRequest
{

View File

@@ -1,12 +1,18 @@
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Registry;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using Account = DysonNetwork.Pass.Account.Account;
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 ChatMemberCacheKey = "chatroom:{0}:member:{1}";
@@ -18,12 +24,11 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
return cachedMembers;
var members = await db.ChatMembers
.Include(m => m.Account)
.ThenInclude(m => m.Profile)
.Where(m => m.ChatRoomId == roomId)
.Where(m => m.JoinedAt != null)
.Where(m => m.LeaveAt == null)
.ToListAsync();
members = await LoadMemberAccounts(members);
var chatRoomGroup = ChatRoomGroupPrefix + roomId;
await cache.SetWithGroupsAsync(cacheKey, members,
@@ -40,14 +45,13 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
if (member is not null) return member;
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)
.Include(m => m.ChatRoom)
.FirstOrDefaultAsync();
if (member == null) return member;
member = await LoadMemberAccount(member);
var chatRoomGroup = ChatRoomGroupPrefix + chatRoomId;
await cache.SetWithGroupsAsync(cacheKey, member,
[chatRoomGroup],
@@ -92,8 +96,6 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
.Where(m => directRoomsId.Contains(m.ChatRoomId))
.Where(m => m.AccountId != userId)
.Where(m => m.LeaveAt == null)
.Include(m => m.Account)
.Include(m => m.Account.Profile)
.GroupBy(m => m.ChatRoomId)
.ToDictionaryAsync(g => g.Key, g => g.ToList())
: new Dictionary<Guid, List<ChatMember>>();
@@ -112,12 +114,13 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
var members = await db.ChatMembers
.Where(m => m.ChatRoomId == room.Id && m.AccountId != userId)
.Where(m => m.LeaveAt == null)
.Include(m => m.Account)
.Include(m => m.Account.Profile)
.ToListAsync();
if (members.Count > 0)
room.DirectMembers = members.Select(ChatMemberTransmissionObject.FromEntity).ToList();
if (members.Count <= 0) return room;
members = await LoadMemberAccounts(members);
room.DirectMembers = members.Select(ChatMemberTransmissionObject.FromEntity).ToList();
return room;
}
@@ -131,4 +134,24 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
.FirstOrDefaultAsync(m => m.ChatRoomId == roomId && m.AccountId == accountId);
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();
}
}

View File

@@ -9,6 +9,7 @@ namespace DysonNetwork.Sphere.Chat;
public partial class ChatService(
AppDatabase db,
ChatRoomService crs,
FileService.FileServiceClient filesClient,
FileReferenceService.FileReferenceServiceClient fileRefs,
IServiceScopeFactory scopeFactory,
@@ -259,7 +260,7 @@ public partial class ChatService(
}
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...");
@@ -333,8 +334,6 @@ public partial class ChatService(
var messages = await db.ChatMessages
.IgnoreQueryFilters()
.Include(m => m.Sender)
.Include(m => m.Sender.Account)
.Include(m => m.Sender.Account.Profile)
.Where(m => userRooms.Contains(m.ChatRoomId))
.GroupBy(m => m.ChatRoomId)
.Select(g => g.OrderByDescending(m => m.CreatedAt).FirstOrDefault())
@@ -450,8 +449,6 @@ public partial class ChatService(
var changes = await db.ChatMessages
.IgnoreQueryFilters()
.Include(e => e.Sender)
.Include(e => e.Sender.Account)
.Include(e => e.Sender.Account.Profile)
.Where(m => m.ChatRoomId == roomId)
.Where(m => m.UpdatedAt > timestamp || m.DeletedAt > timestamp)
.Select(m => new MessageChange
@@ -463,6 +460,20 @@ public partial class ChatService(
})
.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
{
Changes = changes,
@@ -493,18 +504,16 @@ public partial class ChatService(
if (attachmentsId is not null)
{
var messageResourceId = $"message:{message.Id}";
// Delete existing references for this message
await fileRefs.DeleteResourceReferencesAsync(
new DeleteResourceReferencesRequest { ResourceId = messageResourceId }
new DeleteResourceReferencesRequest { ResourceId = message.ResourceIdentifier }
);
// Create new references for each attachment
var createRequest = new CreateReferenceBatchRequest
{
Usage = ChatFileUsageIdentifier,
ResourceId = messageResourceId,
ResourceId = message.ResourceIdentifier,
};
createRequest.FilesId.AddRange(attachmentsId);
await fileRefs.CreateReferenceBatchAsync(createRequest);

View File

@@ -62,8 +62,6 @@ public class RealtimeCallController(
.Where(c => c.EndedAt == null)
.Include(c => c.Room)
.Include(c => c.Sender)
.ThenInclude(c => c.Account)
.ThenInclude(c => c.Profile)
.FirstOrDefaultAsync();
if (ongoingCall is null) return NotFound();
return Ok(ongoingCall);

View File

@@ -16,70 +16,55 @@
</PropertyGroup>
<ItemGroup>
<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.PostgreSql" Version="9.0.1" />
<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="HtmlAgilityPack" Version="1.12.1" />
<PackageReference Include="Livekit.Server.Sdk.Dotnet" Version="1.0.8" />
<PackageReference Include="MailKit" Version="4.13.0" />
<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.OpenApi" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
<PackageReference Include="AngleSharp" Version="1.3.0"/>
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1"/>
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1"/>
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0"/>
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
<PackageReference Include="HtmlAgilityPack" Version="1.12.1"/>
<PackageReference Include="Livekit.Server.Sdk.Dotnet" Version="1.0.10" />
<PackageReference Include="Markdig" Version="0.41.3"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7"/>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7"/>
<PackageReference Include="MimeTypes" Version="2.5.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Minio" Version="6.0.5" />
<PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</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="NodaTime" Version="3.2.2" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" 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.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.HealthChecks" Version="8.2.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
<PackageReference Include="prometheus-net.EntityFramework" Version="0.9.5" />
<PackageReference Include="prometheus-net.SystemMetrics" Version="3.1.0" />
<PackageReference Include="Quartz" Version="3.14.0" />
<PackageReference Include="Quartz.AspNetCore" 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.Extensions.AspNetCore" Version="11.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" />
<PackageReference Include="System.ServiceModel.Syndication" Version="9.0.6" />
<PackageReference Include="tusdotnet" Version="2.10.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="NodaTime" Version="3.2.2"/>
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0"/>
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4"/>
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" 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.Runtime" Version="1.12.0"/>
<PackageReference Include="prometheus-net.AspNetCore" 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.EntityFramework" Version="0.9.5"/>
<PackageReference Include="prometheus-net.SystemMetrics" Version="3.1.0"/>
<PackageReference Include="Quartz" Version="3.14.0"/>
<PackageReference Include="Quartz.AspNetCore" Version="3.14.0"/>
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0"/>
<PackageReference Include="StackExchange.Redis" Version="2.8.41"/>
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3"/>
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3"/>
<PackageReference Include="System.ServiceModel.Syndication" Version="9.0.7" />
</ItemGroup>
<ItemGroup>
@@ -89,8 +74,7 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Connection\" />
<Folder Include="Discovery\" />
<Folder Include="Discovery\"/>
</ItemGroup>
<ItemGroup>
@@ -157,31 +141,32 @@
<AutoGen>True</AutoGen>
<DependentUpon>NotificationResource.resx</DependentUpon>
</Compile>
<Compile Remove="Auth\AppleAuthController.cs" />
<Compile Remove="Auth\AppleAuthController.cs"/>
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="app\publish\appsettings.json" />
<_ContentIncludedByDefault Remove="app\publish\DysonNetwork.Sphere.deps.json" />
<_ContentIncludedByDefault Remove="app\publish\DysonNetwork.Sphere.runtimeconfig.json" />
<_ContentIncludedByDefault Remove="app\publish\DysonNetwork.Sphere.staticwebassets.endpoints.json" />
<_ContentIncludedByDefault Remove="app\publish\Keys\Solian.json" />
<_ContentIncludedByDefault Remove="app\publish\package-lock.json" />
<_ContentIncludedByDefault Remove="app\publish\package.json" />
<_ContentIncludedByDefault Remove="Pages\Account\Profile.cshtml" />
<_ContentIncludedByDefault Remove="Pages\Auth\Authorize.cshtml" />
<_ContentIncludedByDefault Remove="Pages\Auth\Callback.cshtml" />
<_ContentIncludedByDefault Remove="Pages\Auth\Challenge.cshtml" />
<_ContentIncludedByDefault Remove="Pages\Auth\Login.cshtml" />
<_ContentIncludedByDefault Remove="Pages\Auth\SelectFactor.cshtml" />
<_ContentIncludedByDefault Remove="Pages\Auth\VerifyFactor.cshtml" />
<_ContentIncludedByDefault Remove="Pages\Checkpoint\CheckpointPage.cshtml" />
<_ContentIncludedByDefault Remove="Pages\Spell\MagicSpellPage.cshtml" />
<_ContentIncludedByDefault Remove="Keys\Solian.json" />
<_ContentIncludedByDefault Remove="app\publish\appsettings.json"/>
<_ContentIncludedByDefault Remove="app\publish\DysonNetwork.Sphere.deps.json"/>
<_ContentIncludedByDefault Remove="app\publish\DysonNetwork.Sphere.runtimeconfig.json"/>
<_ContentIncludedByDefault Remove="app\publish\DysonNetwork.Sphere.staticwebassets.endpoints.json"/>
<_ContentIncludedByDefault Remove="app\publish\Keys\Solian.json"/>
<_ContentIncludedByDefault Remove="app\publish\package-lock.json"/>
<_ContentIncludedByDefault Remove="app\publish\package.json"/>
<_ContentIncludedByDefault Remove="Pages\Account\Profile.cshtml"/>
<_ContentIncludedByDefault Remove="Pages\Auth\Authorize.cshtml"/>
<_ContentIncludedByDefault Remove="Pages\Auth\Callback.cshtml"/>
<_ContentIncludedByDefault Remove="Pages\Auth\Challenge.cshtml"/>
<_ContentIncludedByDefault Remove="Pages\Auth\Login.cshtml"/>
<_ContentIncludedByDefault Remove="Pages\Auth\SelectFactor.cshtml"/>
<_ContentIncludedByDefault Remove="Pages\Auth\VerifyFactor.cshtml"/>
<_ContentIncludedByDefault Remove="Pages\Checkpoint\CheckpointPage.cshtml"/>
<_ContentIncludedByDefault Remove="Pages\Spell\MagicSpellPage.cshtml"/>
<_ContentIncludedByDefault Remove="Keys\Solian.json"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj"/>
<ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj"/>
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,6 @@ using DysonNetwork.Shared.Registry;
using DysonNetwork.Sphere;
using DysonNetwork.Sphere.Startup;
using Microsoft.EntityFrameworkCore;
using tusdotnet.Stores;
var builder = WebApplication.CreateBuilder(args);

View File

@@ -502,8 +502,6 @@ public class PublisherController(
var publisher = await db.Publishers
.Where(p => p.Name == name)
.Include(publisher => publisher.Picture)
.Include(publisher => publisher.Background)
.FirstOrDefaultAsync();
if (publisher is null) return NotFound();

View File

@@ -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();
}
}

View File

@@ -2,7 +2,6 @@ using System.Net;
using DysonNetwork.Shared.Auth;
using Microsoft.AspNetCore.HttpOverrides;
using Prometheus;
using tusdotnet;
namespace DysonNetwork.Sphere.Startup;

View File

@@ -17,11 +17,9 @@ using System.Threading.RateLimiting;
using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.GeoIp;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Sphere.WebReader;
using DysonNetwork.Sphere.Developer;
using DysonNetwork.Sphere.Discovery;
using tusdotnet.Stores;
namespace DysonNetwork.Sphere.Startup;

View File

@@ -16,22 +16,10 @@
"GeoIp": {
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
},
"Oidc": {
"Google": {
"ClientId": "961776991058-963m1qin2vtp8fv693b5fdrab5hmpl89.apps.googleusercontent.com",
"ClientSecret": ""
},
"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"
}
"RealtimeChat": {
"Endpoint": "https://solar-network-im44o8gq.livekit.cloud",
"ApiKey": "APIs6TiL8wj3A4j",
"ApiSecret": "SffxRneIwTnlHPtEf3zicmmv3LUEl7xXael4PvWZrEhE"
},
"KnownProxies": [
"127.0.0.1",