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
132 lines
6.5 KiB
C#
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),
|
|
};
|
|
}
|