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
89 lines
2.8 KiB
C#
89 lines
2.8 KiB
C#
namespace WebDrop.Core;
|
|
|
|
public enum ErrorCode
|
|
{
|
|
BadRequest,
|
|
Unauthorized,
|
|
Forbidden,
|
|
NotJoined,
|
|
MembersFull,
|
|
ModeMismatch,
|
|
NotFound,
|
|
Conflict,
|
|
Gone,
|
|
TooLarge,
|
|
CaptchaRequired,
|
|
CaptchaUnavailable,
|
|
RateLimited,
|
|
VisitorsFull,
|
|
Sealed,
|
|
AccountRequired,
|
|
InvitationRequired,
|
|
ChannelPasswordRequired,
|
|
ChannelPasswordInvalid,
|
|
SpaceNameRequired,
|
|
}
|
|
|
|
/// <summary>预期内的业务失败。消息只含错误码,绝不回显用户提交的内容。</summary>
|
|
public sealed class WebDropException : Exception
|
|
{
|
|
public ErrorCode Code { get; }
|
|
|
|
/// <summary>仅在 410 日志续传失败时有值,见 api.md。</summary>
|
|
public long? OldestSeq { get; }
|
|
|
|
public WebDropException(ErrorCode code, long? oldestSeq = null) : base(code.ToWire())
|
|
{
|
|
Code = code;
|
|
OldestSeq = oldestSeq;
|
|
}
|
|
}
|
|
|
|
public static class ErrorCodes
|
|
{
|
|
public static string ToWire(this ErrorCode code) => code switch
|
|
{
|
|
ErrorCode.BadRequest => "bad_request",
|
|
ErrorCode.Unauthorized => "unauthorized",
|
|
ErrorCode.Forbidden => "forbidden",
|
|
ErrorCode.NotJoined => "not_joined",
|
|
ErrorCode.MembersFull => "members_full",
|
|
ErrorCode.ModeMismatch => "mode_mismatch",
|
|
ErrorCode.NotFound => "not_found",
|
|
ErrorCode.Conflict => "conflict",
|
|
ErrorCode.Gone => "gone",
|
|
ErrorCode.TooLarge => "too_large",
|
|
ErrorCode.CaptchaRequired => "captcha_required",
|
|
ErrorCode.CaptchaUnavailable => "captcha_unavailable",
|
|
ErrorCode.RateLimited => "rate_limited",
|
|
ErrorCode.VisitorsFull => "visitors_full",
|
|
ErrorCode.Sealed => "sealed",
|
|
ErrorCode.AccountRequired => "account_required",
|
|
ErrorCode.InvitationRequired => "invitation_required",
|
|
ErrorCode.ChannelPasswordRequired => "channel_password_required",
|
|
ErrorCode.ChannelPasswordInvalid => "channel_password_invalid",
|
|
ErrorCode.SpaceNameRequired => "space_name_required",
|
|
_ => "bad_request",
|
|
};
|
|
|
|
public static int ToStatus(this ErrorCode code) => code switch
|
|
{
|
|
ErrorCode.BadRequest => 400,
|
|
ErrorCode.Unauthorized => 401,
|
|
ErrorCode.Forbidden or ErrorCode.NotJoined or ErrorCode.MembersFull or ErrorCode.ModeMismatch => 403,
|
|
ErrorCode.NotFound => 404,
|
|
ErrorCode.Conflict => 409,
|
|
ErrorCode.Sealed => 409,
|
|
ErrorCode.SpaceNameRequired => 400,
|
|
ErrorCode.Gone => 410,
|
|
ErrorCode.TooLarge => 413,
|
|
ErrorCode.CaptchaRequired => 428,
|
|
ErrorCode.CaptchaUnavailable => 503,
|
|
ErrorCode.RateLimited => 429,
|
|
ErrorCode.VisitorsFull => 403,
|
|
ErrorCode.AccountRequired => 401,
|
|
ErrorCode.InvitationRequired or ErrorCode.ChannelPasswordRequired or ErrorCode.ChannelPasswordInvalid => 403,
|
|
_ => 400,
|
|
};
|
|
}
|