Files
WebDrop/server/src/WebDrop.Core/Settings.cs
T
justme d946fc5e8a feat(api)!: fold devices into users and report member presence
Contract 7.0.0 (breaking). Devices become an attribute of the account;
members are managed per user, and only unauthenticated guests keep a
per-terminal registration.

- rename seats -> max_members, seats_used -> members_used,
  seats_full -> members_full, SeatJoined -> JoinResult, the `seats`
  stream control message -> `members`, and POST /channels/{id}/seats ->
  POST /channels/{id}/join (stored channel settings are migrated on start)
- drop per-terminal encrypted device labels and the seat list/release
  endpoints; add GET/DELETE /channels/{id}/guests for guest terminals
- authenticate account members by account session only: the terminal key
  is optional for logged-in accounts, no seat row is written for them,
  and gated-download grants are counted per user (per terminal for guests)
- GET /channels/{id}/members now returns online, devices and
  online_devices per account; presence is tracked in memory from active
  WebSocket/SSE subscriptions and is not pushed over the stream
- owner invitation list no longer returns accounts that already joined
2026-10-14 22:18:59 +08:00

132 lines
6.5 KiB
C#

using System.Text.Json.Serialization;
namespace WebDrop.Core;
/// <summary>服务端限制,由配置绑定。</summary>
public sealed class ServerLimits
{
public int MaxPayloadBytes { get; set; } = (1 << 20) + 16;
public int MaxFramesPerItem { get; set; } = 65_536;
public long MaxChannelBytes { get; set; } = 1L << 30;
public long MaxWorkspaceBytes { get; set; } = 1L << 40;
public long MaxWorkspaceChannelBytes { get; set; } = 10L << 30;
/// <summary>最大账号成员数。属性名和 info 字段保持旧线格式兼容。</summary>
public int MaxMembers { get; set; } = 64;
public int MaxVisitors { get; set; } = 10_000;
public int MinTtlSeconds { get; set; } = 60;
public int MaxTtlSeconds { get; set; } = 604_800;
public int MaxItemsLimit { get; set; } = 10_000;
public int InlineThresholdBytes { get; set; } = 64 * 1024;
public int DownloadGraceSeconds { get; set; } = 300;
public int TombstoneRetentionSeconds { get; set; } = 604_800;
}
public static class ServerSettingChoices
{
public static readonly int[] FileMaxDownloads = [0, 1, 3, 5, 10, 20];
public static bool IsFileMaxDownloadsAllowed(long value, int currentDefault) => value == currentDefault
|| value is >= int.MinValue and <= int.MaxValue && Array.IndexOf(FileMaxDownloads, (int)value) >= 0;
}
public sealed class ChannelSettings
{
[JsonPropertyName("join_policy")] public JoinPolicy JoinPolicy { get; set; } = JoinPolicy.Open;
[JsonPropertyName("allow_guests")] public bool AllowGuests { get; set; } = true;
[JsonPropertyName("name")] public string? Name { get; set; }
/// <summary>成员人数上限;线字段名保留为 seats 以兼容已有频道设置。</summary>
[JsonPropertyName("max_members")] public int MaxMembers { get; set; } = 2;
[JsonPropertyName("members_can_upload")] public bool MembersCanUpload { get; set; } = true;
[JsonPropertyName("delete_policy")] public DeletePolicy DeletePolicy { get; set; } = DeletePolicy.Own;
[JsonPropertyName("ttl_seconds")] public int TtlSeconds { get; set; } = 86_400;
[JsonPropertyName("max_items")] public int MaxItems { get; set; } = 1000;
[JsonPropertyName("file_max_downloads")] public int FileMaxDownloads { get; set; } = 10;
[JsonPropertyName("ui_restrict_copy_download")] public bool UiRestrictCopyDownload { get; set; }
public void Validate(ServerLimits limits)
{
if (!Enum.IsDefined(JoinPolicy)) throw new WebDropException(ErrorCode.BadRequest);
if (Name is { } name && (name.Length > 48 || name.Any(char.IsControl))) throw new WebDropException(ErrorCode.BadRequest);
if (MaxMembers < 1 || MaxMembers > limits.MaxMembers) throw new WebDropException(ErrorCode.BadRequest);
if (TtlSeconds < limits.MinTtlSeconds || TtlSeconds > limits.MaxTtlSeconds) throw new WebDropException(ErrorCode.BadRequest);
if (MaxItems < 1 || MaxItems > limits.MaxItemsLimit) throw new WebDropException(ErrorCode.BadRequest);
if (FileMaxDownloads < 0) throw new WebDropException(ErrorCode.BadRequest);
}
}
public sealed class ChannelSettingsPatch
{
[JsonPropertyName("join_policy")] public JoinPolicy? JoinPolicy { get; init; }
[JsonPropertyName("allow_guests")] public bool? AllowGuests { get; init; }
[JsonPropertyName("join_password")] public string? JoinPassword { get; init; }
[JsonPropertyName("name")] public string? Name { get; init; }
[JsonPropertyName("max_members")] public int? MaxMembers { get; init; }
[JsonPropertyName("members_can_upload")] public bool? MembersCanUpload { get; init; }
[JsonPropertyName("delete_policy")] public DeletePolicy? DeletePolicy { get; init; }
[JsonPropertyName("ttl_seconds")] public int? TtlSeconds { get; init; }
[JsonPropertyName("max_items")] public int? MaxItems { get; init; }
[JsonPropertyName("file_max_downloads")] public int? FileMaxDownloads { get; init; }
[JsonPropertyName("ui_restrict_copy_download")] public bool? UiRestrictCopyDownload { get; init; }
public ChannelSettings ApplyTo(ChannelSettings s) => new()
{
JoinPolicy = JoinPolicy ?? s.JoinPolicy,
AllowGuests = AllowGuests ?? s.AllowGuests,
Name = Name is null ? s.Name : string.IsNullOrWhiteSpace(Name) ? null : Name.Trim(),
MaxMembers = MaxMembers ?? s.MaxMembers,
MembersCanUpload = MembersCanUpload ?? s.MembersCanUpload,
DeletePolicy = DeletePolicy ?? s.DeletePolicy,
TtlSeconds = TtlSeconds ?? s.TtlSeconds,
MaxItems = MaxItems ?? s.MaxItems,
FileMaxDownloads = FileMaxDownloads ?? s.FileMaxDownloads,
UiRestrictCopyDownload = UiRestrictCopyDownload ?? s.UiRestrictCopyDownload,
};
}
public sealed class ShareSettings
{
[JsonPropertyName("ttl_seconds")] public int TtlSeconds { get; set; } = 86_400;
[JsonPropertyName("max_visitors")] public int MaxVisitors { get; set; }
[JsonPropertyName("file_max_downloads")] public int FileMaxDownloads { get; set; } = 10;
public void Validate(ServerLimits limits)
{
if (TtlSeconds < limits.MinTtlSeconds || TtlSeconds > limits.MaxTtlSeconds
|| MaxVisitors < 0 || MaxVisitors > limits.MaxVisitors
|| FileMaxDownloads < 0)
throw new WebDropException(ErrorCode.BadRequest);
}
}
public sealed class ShareSettingsPatch
{
[JsonPropertyName("ttl_seconds")] public int? TtlSeconds { get; init; }
[JsonPropertyName("max_visitors")] public int? MaxVisitors { get; init; }
[JsonPropertyName("file_max_downloads")] public int? FileMaxDownloads { get; init; }
public ShareSettings ApplyTo(ShareSettings s) => new()
{
TtlSeconds = TtlSeconds ?? s.TtlSeconds,
MaxVisitors = MaxVisitors ?? s.MaxVisitors,
FileMaxDownloads = FileMaxDownloads ?? s.FileMaxDownloads,
};
}
/// <summary>创建接口与 /info 使用同一个配置来源;旧存储对象的字段默认值不作迁移。</summary>
public static class CreationDefaults
{
public const ChannelMode ChannelMode = WebDrop.Core.ChannelMode.Enc;
public static ChannelSettings Channel(ServerLimits limits) => new()
{
JoinPolicy = JoinPolicy.Invite, AllowGuests = false,
MaxMembers = Math.Min(10, limits.MaxMembers),
TtlSeconds = Math.Clamp(86_400, limits.MinTtlSeconds, limits.MaxTtlSeconds),
MaxItems = Math.Min(1000, limits.MaxItemsLimit),
};
public static ShareSettings Share(ServerLimits limits) => new()
{
TtlSeconds = Math.Clamp(86_400, limits.MinTtlSeconds, limits.MaxTtlSeconds),
MaxVisitors = Math.Min(10, limits.MaxVisitors),
};
}