145 lines
9.8 KiB
C#
145 lines
9.8 KiB
C#
namespace WebDrop.Core;
|
|
|
|
public sealed partial class ChannelService
|
|
{
|
|
public void AccountProfileChanged(string username)
|
|
{
|
|
foreach (var id in store.Read(tx => tx.ListAccountSpaces(username, clock.NowMs))) events.SettingsChanged(id);
|
|
}
|
|
public IReadOnlyList<ChannelInfo> AccountSpaces(string username) => store.Read(tx => tx.ListAccountSpaces(username, clock.NowMs).Select(id =>
|
|
{
|
|
var channel = tx.GetChannel(id)!;
|
|
var role = tx.GetChannelMember(id, username)?.Role == "owner" ? Role.Owner : channel.WriteHash is not null ? Role.Write : Role.Read;
|
|
return ToInfo(channel, tx.CountMembers(id), new Principal(role, [], username));
|
|
}).ToList());
|
|
private static WorkspaceState NewWorkspace(string username, long now)
|
|
{
|
|
var id = Guid.NewGuid().ToString();
|
|
return new() { Revision = 1, DefaultChannelId = id, Channels = [new WorkspaceChannel { Id = id, Name = "常规", CreatedAt = now, CreatedBy = username }] };
|
|
}
|
|
public WorkspaceState GetWorkspace(Id16 id, Credentials creds) => store.Read(tx =>
|
|
{
|
|
var (space, principal) = Authenticate(tx, id, creds, SeatRule.Required);
|
|
var state = tx.GetWorkspace(id) ?? new WorkspaceState();
|
|
if (principal.AccountName != space.CreatorAccount)
|
|
{
|
|
var crypto = tx.GetWorkspaceCrypto(id);
|
|
var grants = crypto.Access.Where(a => a.Username == principal.AccountName).ToList();
|
|
var ids = grants.Where(a => a.ChatRead || a.FilesRead).Select(a => a.ChannelId).ToHashSet();
|
|
state.Channels.RemoveAll(c => !ids.Contains(c.Id));
|
|
foreach (var workspaceChannel in state.Channels)
|
|
{
|
|
var grant = grants.Find(a => a.ChannelId == workspaceChannel.Id);
|
|
if (grant is null || !grant.FilesRead && !grant.FilesUpload) workspaceChannel.FileBytes = 0;
|
|
}
|
|
state.Folders.RemoveAll(f => !ids.Contains(f.ThreadId));
|
|
state.Locations.RemoveAll(l => !Id16.TryParse(l.ItemId, out var itemId) || !CanReadObject(tx, space, principal, itemId));
|
|
}
|
|
state.Locations = state.Locations.Where(l => Id16.TryParse(l.ItemId, out var itemId) && tx.GetItem(id, itemId) is { } item && item.ExpiresAt > clock.NowMs).ToList();
|
|
return state;
|
|
});
|
|
private WorkspaceState ChangeWorkspace(Id16 id, Credentials creds, Action<IResourceTx, WorkspaceState, Principal> change, bool requireAccount = true)
|
|
{
|
|
var state = store.Write(tx =>
|
|
{
|
|
var (channel, principal) = Authenticate(tx, id, creds, SeatRule.Required);
|
|
if (requireAccount && principal.AccountName is null) throw new WebDropException(ErrorCode.AccountRequired);
|
|
if (requireAccount && principal.Role != Role.Owner) throw new WebDropException(ErrorCode.Forbidden);
|
|
if (!ChannelPolicy.CanWrite(channel, principal)) throw new WebDropException(ErrorCode.Forbidden);
|
|
var current = tx.GetWorkspace(id) ?? new WorkspaceState();
|
|
current.Locations = current.Locations.Where(l => Id16.TryParse(l.ItemId, out var itemId) && tx.GetItem(id, itemId) is not null).ToList();
|
|
change(tx, current, principal);
|
|
ValidateWorkspace(current);
|
|
current.Revision++;
|
|
tx.PutWorkspace(id, current);
|
|
return current;
|
|
});
|
|
events.SettingsChanged(id);
|
|
return state;
|
|
}
|
|
public (WorkspaceState State, WorkspaceChannel Channel) CreateWorkspaceChannelResult(Id16 id, Credentials creds, string name, string? groupId)
|
|
{
|
|
WorkspaceChannel? created = null;
|
|
var state = ChangeWorkspace(id, creds, (_, workspace, principal) =>
|
|
{
|
|
created = new WorkspaceChannel { Id = Guid.NewGuid().ToString(), Name = WorkspaceName(name), GroupId = groupId, CreatedAt = clock.NowMs, CreatedBy = principal.AccountName! };
|
|
workspace.Channels.Add(created);
|
|
});
|
|
return (state, created!);
|
|
}
|
|
public WorkspaceState CreateWorkspaceChannel(Id16 id, Credentials creds, string name, string? groupId) => CreateWorkspaceChannelResult(id, creds, name, groupId).State;
|
|
public WorkspaceState RenameWorkspaceChannel(Id16 id, Credentials creds, string channelId, string name) => ChangeWorkspace(id, creds, (_, state, _) =>
|
|
FindChannel(state, channelId).Name = WorkspaceName(name));
|
|
public WorkspaceState GroupWorkspaceChannel(Id16 id, Credentials creds, string channelId, string? groupId) => ChangeWorkspace(id, creds, (_, state, _) =>
|
|
FindChannel(state, channelId).GroupId = groupId);
|
|
public (WorkspaceState State, WorkspaceGroup Group) CreateWorkspaceGroupResult(Id16 id, Credentials creds, string name)
|
|
{
|
|
WorkspaceGroup? created = null;
|
|
var state = ChangeWorkspace(id, creds, (_, workspace, _) =>
|
|
{
|
|
created = new WorkspaceGroup { Id = Guid.NewGuid().ToString(), Name = WorkspaceName(name), CreatedAt = clock.NowMs };
|
|
workspace.Groups.Add(created);
|
|
});
|
|
return (state, created!);
|
|
}
|
|
public WorkspaceState CreateWorkspaceGroup(Id16 id, Credentials creds, string name) => CreateWorkspaceGroupResult(id, creds, name).State;
|
|
public WorkspaceState RenameWorkspaceGroup(Id16 id, Credentials creds, string groupId, string name) => ChangeWorkspace(id, creds, (_, state, _) =>
|
|
(state.Groups.Find(g => g.Id == groupId) ?? throw new WebDropException(ErrorCode.NotFound)).Name = WorkspaceName(name));
|
|
public WorkspaceState CreateWorkspaceFolder(Id16 id, Credentials creds, string threadId, string? parentId, string name) => ChangeWorkspace(id, creds, (_, state, _) =>
|
|
state.Folders.Add(new WorkspaceFolder { Id = Guid.NewGuid().ToString(), ThreadId = threadId, ParentId = parentId, Name = WorkspaceName(name) }));
|
|
public WorkspaceState RenameWorkspaceFolder(Id16 id, Credentials creds, string folderId, string name) => ChangeWorkspace(id, creds, (_, state, _) =>
|
|
(state.Folders.Find(f => f.Id == folderId) ?? throw new WebDropException(ErrorCode.NotFound)).Name = WorkspaceName(name));
|
|
public WorkspaceState MoveWorkspaceItems(Id16 id, Credentials creds, string[] itemIds, string threadId, string? folderId) => ChangeWorkspace(id, creds, (tx, state, principal) =>
|
|
{
|
|
if (itemIds.Length is < 1 or > 100 || itemIds.Distinct().Count() != itemIds.Length) throw new WebDropException(ErrorCode.BadRequest);
|
|
foreach (var itemId in itemIds)
|
|
{
|
|
if (!Id16.TryParse(itemId, out var parsed)) throw new WebDropException(ErrorCode.BadRequest);
|
|
var item = tx.GetItem(id, parsed) ?? throw new WebDropException(ErrorCode.NotFound);
|
|
if ((item.Flags & (FrameFlags.Control | FrameFlags.Snapshot)) != 0) throw new WebDropException(ErrorCode.BadRequest);
|
|
if (principal.Role != Role.Owner && (item.AuthorAccount is not null ? item.AuthorAccount != principal.AccountName : !item.AuthorHash.AsSpan().SequenceEqual(principal.EndpointHash))) throw new WebDropException(ErrorCode.Forbidden);
|
|
state.Locations.RemoveAll(l => l.ItemId == itemId);
|
|
state.Locations.Add(new WorkspaceLocation { ItemId = itemId, ThreadId = threadId, FolderId = folderId, AuthorAccount = item.AuthorAccount });
|
|
}
|
|
}, requireAccount: false);
|
|
private static WorkspaceChannel FindChannel(WorkspaceState state, string id) => state.Channels.Find(c => c.Id == id) ?? throw new WebDropException(ErrorCode.NotFound);
|
|
private static string WorkspaceName(string name)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name) || name.Trim().Length > 80 || name.Any(char.IsControl)) throw new WebDropException(ErrorCode.BadRequest);
|
|
return name.Trim();
|
|
}
|
|
private static void ValidateWorkspace(WorkspaceState state)
|
|
{
|
|
if (state.Channels is null || state.Groups is null || state.Folders is null || state.Locations is null
|
|
|| state.Channels.Count > 256 || state.Groups.Count > 128 || state.Folders.Count > 2048 || state.Locations.Count > 10_000)
|
|
throw new WebDropException(ErrorCode.BadRequest);
|
|
if (state.Channels.Any(c => c is null) || state.Groups.Any(g => g is null) || state.Folders.Any(f => f is null) || state.Locations.Any(l => l is null)) throw new WebDropException(ErrorCode.BadRequest);
|
|
var allIds = state.Channels.Select(c => c.Id).Concat(state.Groups.Select(g => g.Id)).Concat(state.Folders.Select(f => f.Id)).ToArray();
|
|
if (allIds.Any(id => !Guid.TryParseExact(id, "D", out _)) || allIds.Distinct().Count() != allIds.Length) throw new WebDropException(ErrorCode.BadRequest);
|
|
foreach (var group in state.Groups) group.Name = WorkspaceName(group.Name);
|
|
foreach (var channel in state.Channels)
|
|
{
|
|
channel.Name = WorkspaceName(channel.Name);
|
|
if (channel.GroupId is not null && !state.Groups.Any(g => g.Id == channel.GroupId)) throw new WebDropException(ErrorCode.BadRequest);
|
|
}
|
|
foreach (var folder in state.Folders)
|
|
{
|
|
folder.Name = WorkspaceName(folder.Name);
|
|
_ = FindChannel(state, folder.ThreadId);
|
|
var visited = new HashSet<string> { folder.Id };
|
|
for (var parentId = folder.ParentId; parentId is not null;)
|
|
{
|
|
var parent = state.Folders.Find(f => f.Id == parentId && f.ThreadId == folder.ThreadId);
|
|
if (parent is null || !visited.Add(parent.Id)) throw new WebDropException(ErrorCode.BadRequest);
|
|
parentId = parent.ParentId;
|
|
}
|
|
}
|
|
if (state.Locations.Select(l => l.ItemId).Distinct().Count() != state.Locations.Count) throw new WebDropException(ErrorCode.BadRequest);
|
|
foreach (var location in state.Locations)
|
|
{
|
|
_ = FindChannel(state, location.ThreadId);
|
|
if (location.FolderId is not null && !state.Folders.Any(f => f.Id == location.FolderId && f.ThreadId == location.ThreadId)) throw new WebDropException(ErrorCode.BadRequest);
|
|
}
|
|
}
|
|
}
|