diff --git a/extra/admin-api/Models/Spacebar.Models.Api/LoginRequest.cs b/extra/admin-api/Models/Spacebar.Models.Api/LoginRequest.cs
new file mode 100644
index 000000000..6971d55e2
--- /dev/null
+++ b/extra/admin-api/Models/Spacebar.Models.Api/LoginRequest.cs
@@ -0,0 +1,79 @@
+using System.Text.Json.Serialization;
+
+namespace Spacebar.Models.Api;
+
+public class LoginRequest {
+ [JsonPropertyName("login")]
+ public required string Login { get; set; }
+
+ [JsonPropertyName("password")]
+ public required string Password { get; set; }
+
+ ///
+ /// Whether to un-delete a self-disabled or self-deleted account
+ ///
+ [JsonPropertyName("undelete")]
+ public bool? Undelete { get; set; }
+
+ ///
+ /// One of gift, guild_template, guild_invite, dm_invite, friend_invite, role_subscription or role_subscription_setting
+ ///
+ [JsonPropertyName("login_source")]
+ public string? LoginSource { get; set; }
+
+ [JsonPropertyName("gift_code_sku_id")]
+ public string? GiftCodeSkuId { get; set; }
+}
+
+///
+/// Note: nullable fields missing "if the login was not completed"
+///
+public class LoginResponse {
+ [JsonPropertyName("user_id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
+ public ulong UserId { get; set; }
+
+ [JsonPropertyName("token")]
+ public string? Token { get; set; }
+
+ [JsonPropertyName("user_settings")]
+ public LoginUserSettings? UserSettings { get; set; }
+
+ ///
+ /// Values: update_password
+ ///
+ [JsonPropertyName("required_actions")]
+ public List? RequiredActions { get; set; }
+
+ ///
+ /// MFA ticket
+ ///
+ [JsonPropertyName("ticket")]
+ public string? Ticket { get; set; }
+
+ [JsonPropertyName("login_instance_id")]
+ public string? LoginInstanceId { get; set; }
+
+ [JsonPropertyName("mfa")]
+ public bool? Mfa { get; set; }
+
+ [JsonPropertyName("totp")]
+ public bool? Totp { get; set; }
+
+ [JsonPropertyName("sms")]
+ public bool? Sms { get; set; }
+
+ [JsonPropertyName("backup")]
+ public bool? Backup { get; set; }
+
+ [JsonPropertyName("webauthn")]
+ public string? Webauthn { get; set; }
+
+
+ public class LoginUserSettings {
+ [JsonPropertyName("locale")]
+ public string Locale { get; set; }
+
+ [JsonPropertyName("theme")]
+ public string Theme { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/extra/admin-api/Models/Spacebar.Models.Api/Spacebar.Models.Api.csproj b/extra/admin-api/Models/Spacebar.Models.Api/Spacebar.Models.Api.csproj
new file mode 100644
index 000000000..237d66167
--- /dev/null
+++ b/extra/admin-api/Models/Spacebar.Models.Api/Spacebar.Models.Api.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/extra/admin-api/Models/Spacebar.Models.Api/SpacebarApiError.cs b/extra/admin-api/Models/Spacebar.Models.Api/SpacebarApiError.cs
new file mode 100644
index 000000000..03bf9264c
--- /dev/null
+++ b/extra/admin-api/Models/Spacebar.Models.Api/SpacebarApiError.cs
@@ -0,0 +1,29 @@
+using System.Text.Json.Nodes;
+
+namespace Spacebar.Models.Api;
+
+public class SpacebarApiException : Exception {
+ public int Code { get; set; }
+ public string? Request { get; set; } // Spacebar extension
+ // public Dictionary Errors { get; set; }
+ public JsonObject? Errors { get; set; }
+
+ public class FieldErrorList {
+ // public
+ }
+
+ public SpacebarApiException(string? message) : base(message) {
+
+ }
+
+ // TODO: abstract out to HTTP layer
+ public static SpacebarApiException FromJson(JsonObject resp) {
+ var ex = new SpacebarApiException(resp["message"]!.GetValue()) {
+ Code = resp["code"]!.GetValue(),
+ Request = resp["request"]?.GetValue(),
+ Errors = resp["errors"]?.AsObject()
+ };
+
+ return ex;
+ }
+}
\ No newline at end of file
diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/GatewayPayload.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/GatewayPayload.cs
new file mode 100644
index 000000000..1a5d2c864
--- /dev/null
+++ b/extra/admin-api/Models/Spacebar.Models.Gateway/GatewayPayload.cs
@@ -0,0 +1,83 @@
+using System.Diagnostics;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+
+namespace Spacebar.Models.Gateway;
+
+public class GatewayPayload {
+ [JsonPropertyName("op")]
+ public GatewayOpcode Opcode { get; set; }
+
+ [JsonPropertyName("d")]
+ public JsonObject? EventData { get; set; }
+
+ [JsonPropertyName("s")]
+ public int? Sequence { get; set; }
+
+ [JsonPropertyName("t")]
+ public string? DispatchEventType { get; set; }
+
+ public T? GetData() where T : class {
+ if (EventData is null) return null;
+ var sw = Stopwatch.StartNew();
+ var jso = new JsonSerializerOptions();
+ if (typeof(T) == typeof(ReadyResponse)) {
+ Console.WriteLine("Adding ReadyResponse sourcegen");
+ jso.TypeInfoResolver = new ReadyResponseSerializerContext();
+ }
+ else
+ Console.WriteLine($"No TypeInfoResolver for {typeof(T).FullName}");
+
+ var deserialized = EventData.Deserialize(jso);
+ Console.WriteLine($"Deserialized {typeof(T).FullName} in {sw.Elapsed}");
+ return deserialized;
+ }
+}
+
+public enum GatewayOpcode {
+ S2CDispatch,
+ Heartbeat,
+ C2SIdentify,
+ C2SPresenceUpdate,
+ C2SVoiceStateUpdate,
+ C2SVoiceServerPing,
+ C2SResume,
+ S2CReconnect,
+ C2SRequestGuildMembers,
+ S2CInvalidSession,
+ S2CHello,
+ S2CHeartbeatAck,
+ C2SGuildSync,
+ C2SCallConnect,
+ C2SGuildSubscriptions,
+ C2SLobbyConnect,
+ C2SLobbyDisconnect,
+ C2SLobbyVoiceStates,
+ C2SStreamCreate,
+ C2SStreamDelete,
+ C2SStreamWatch,
+ C2SStreamPing,
+ C2SStreamSetPaused,
+ C2SLfgSubscription,
+ C2SRequestGuildApplicationCommands,
+ C2SEmbeddedActivityCreate,
+ C2SEmbeddedActivityDelete,
+ C2SEmbeddedActivityUpdate,
+ C2SRequestForumUnreads,
+ C2SRemoteCommand,
+ C2SRequestDeletedEntityIds,
+ C2SRequestSoundboardSounds,
+ C2SSpeedTestCreate,
+ C2SSpeedTestDelete,
+ C2SRequestLastMessages,
+ C2SSearchRecentMembers,
+ C2SRequestChannelStatuses,
+ C2SGuildSubscriptionsBulk,
+ C2SGuildChannelsResync,
+ C2SRequestChannelMemberCount,
+ C2SQoSHeartbeat,
+ C2SUpdateTimeSpentSessionId,
+ C2SLobbyVoiceServerPing,
+ C2SRequestChannelInfo
+}
\ No newline at end of file
diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/IdentifyRequest.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/IdentifyRequest.cs
index ee39e292c..f94ebecd7 100644
--- a/extra/admin-api/Models/Spacebar.Models.Gateway/IdentifyRequest.cs
+++ b/extra/admin-api/Models/Spacebar.Models.Gateway/IdentifyRequest.cs
@@ -3,6 +3,11 @@ using System.Text.Json.Serialization;
namespace Spacebar.Models.Gateway;
+public class HelloResponse {
+ [JsonPropertyName("heartbeat_interval")]
+ public int HeartbeatInterval { get; set; }
+}
+
public class IdentifyRequest {
[JsonPropertyName("token")]
public string Token { get; set; }
@@ -32,6 +37,171 @@ public class IdentifyRequest {
public JsonObject? ClientState { get; set; }
}
+///
+/// This class defines all client properties used by discord
+/// This should *not* be used as a schema, as the types are merely conventions, and the structure isn't well-defined.
+///
+public class IdentifyClientProperties {
+ [JsonPropertyName("os")]
+ public string? OperatingSystem { get; set; }
+
+ [JsonPropertyName("os_version")]
+ public string? OperatingSystemVersion { get; set; }
+
+ [JsonPropertyName("os_sdk_version")]
+ public string? OperatingSystemSdkVersion { get; set; }
+
+ [JsonPropertyName("os_arch")]
+ public string? OperatingSystemArchitecture { get; set; }
+
+ [JsonPropertyName("app_arch")]
+ public string? ApplicationArchitecture { get; set; }
+
+ [JsonPropertyName("browser")]
+ public string? Browser { get; set; }
+
+ [JsonPropertyName("browser_user_agent")]
+ public string? BrowserUserAgent { get; set; }
+
+ [JsonPropertyName("browser_version")]
+ public string? BrowserVersion { get; set; }
+
+ [JsonPropertyName("client_build_number")]
+ public int? ClientBuildNumber { get; set; }
+
+ [JsonPropertyName("native_build_number")]
+ public int? NativeBuildNumber { get; set; }
+
+ [JsonPropertyName("client_version")]
+ public string? ClientVersion { get; set; }
+
+ [JsonPropertyName("client_event_source")]
+ public string? ClientEventSource { get; set; }
+
+ [JsonPropertyName("client_app_state")]
+ public string? ClientAppState { get; set; }
+
+ [JsonPropertyName("client_launch_id")]
+ public string? ClientLaunchId { get; set; }
+
+ [JsonPropertyName("client_heartbeat_session_id")]
+ public string? ClientHeartbeatSessionId { get; set; }
+
+ [JsonPropertyName("client_performance_cpu")]
+ public int? ClientPerformanceCpu { get; set; }
+
+ [JsonPropertyName("client_performance_memory")]
+ public int? ClientPerformanceMemory { get; set; }
+
+ [JsonPropertyName("cpu_core_count")]
+ public int? CpuCoreCount { get; set; }
+
+ [JsonPropertyName("release_channel")]
+ public string? ReleaseChannel { get; set; }
+
+ [JsonPropertyName("system_locale")]
+ public string? SystemLocale { get; set; }
+
+ [JsonPropertyName("device")]
+ public string? DeviceModel { get; set; }
+
+ [JsonPropertyName("device_vendor_id")]
+ public string? DeviceVendorId { get; set; }
+
+ [JsonPropertyName("device_advertiser_id")]
+ public string? DeviceAdvertiserId { get; set; }
+
+ [JsonPropertyName("design_id")]
+ public int? DesignId { get; set; }
+
+ [JsonPropertyName("accessibility_support_enabled")]
+ public string? AccessibilitySupportEnabled { get; set; }
+
+ [JsonPropertyName("accessibility_features")]
+ public int? AccessibilityFeatures { get; set; }
+
+ [JsonPropertyName("window_manager")]
+ public string? WindowManager { get; set; }
+
+ [JsonPropertyName("distro")]
+ public string? Distro { get; set; }
+
+ [JsonPropertyName("runtime_environment")]
+ public string? RuntimeEnvironment { get; set; }
+
+ [JsonPropertyName("display_server")]
+ public string? DisplayServer { get; set; }
+
+ [JsonPropertyName("referrer")]
+ public string? Referrer { get; set; }
+
+ [JsonPropertyName("referrer_current")]
+ public string? ReferrerCurrent { get; set; }
+
+ [JsonPropertyName("referring_domain")]
+ public string? ReferringDomain { get; set; }
+
+ [JsonPropertyName("referring_domain_current")]
+ public string? ReferringDomainCurrent { get; set; }
+
+ [JsonPropertyName("search_engine")]
+ public string? SearchEngine { get; set; }
+
+ [JsonPropertyName("search_engine_current")]
+ public string? SearchEngineCurrent { get; set; }
+
+ [JsonPropertyName("mp_keyword")]
+ public string? MpKeyword { get; set; }
+
+ [JsonPropertyName("mp_keyword_current")]
+ public string? MpKeywordCurrent { get; set; }
+
+ [JsonPropertyName("utm_campaign")]
+ public string? UtmCampaign { get; set; }
+
+ [JsonPropertyName("utm_campaign_current")]
+ public string? UtmCampaignCurrent { get; set; }
+
+ [JsonPropertyName("utm_content")]
+ public string? UtmContent { get; set; }
+
+ [JsonPropertyName("utm_content_current")]
+ public string? UtmContentCurrent { get; set; }
+
+ [JsonPropertyName("utm_medium")]
+ public string? UtmMedium { get; set; }
+
+ [JsonPropertyName("utm_medium_current")]
+ public string? UtmMediumCurrent { get; set; }
+
+ [JsonPropertyName("utm_source")]
+ public string? UtmSource { get; set; }
+
+ [JsonPropertyName("utm_source_current")]
+ public string? UtmSourceCurrent { get; set; }
+
+ [JsonPropertyName("utm_term")]
+ public string? UtmTerm { get; set; }
+
+ [JsonPropertyName("utm_term_current")]
+ public string? UtmTermCurrent { get; set; }
+
+ [JsonPropertyName("has_client_mods")]
+ public bool? HasClientMods { get; set; }
+
+ [JsonPropertyName("launch_signature")]
+ public string? LaunchSignature { get; set; }
+
+ [JsonPropertyName("installation_id")]
+ public string? InstallationId { get; set; }
+
+ [JsonPropertyName("is_fast_connect")]
+ public bool? IsFastConnect { get; set; }
+
+ [JsonPropertyName("version")]
+ public string? Version { get; set; }
+}
+
[Flags]
public enum GatewayIntentFlags : ulong {
Guilds = 1,
diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/QoSPayload.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/QoSPayload.cs
index efb8dff9f..e470318bd 100644
--- a/extra/admin-api/Models/Spacebar.Models.Gateway/QoSPayload.cs
+++ b/extra/admin-api/Models/Spacebar.Models.Gateway/QoSPayload.cs
@@ -2,6 +2,16 @@ using System.Text.Json.Serialization;
namespace Spacebar.Models.Gateway;
+public class HeartbeatRequest {
+ [JsonPropertyName("seq")]
+ public required int? Sequence { get; set; }
+}
+
+public class QoSHeartbeatRequest : HeartbeatRequest {
+ [JsonPropertyName("qos")]
+ public required QoSPayload QoSPayload { get; set; }
+}
+
public class QoSPayload {
[JsonPropertyName("ver")]
public int Version { get; set; }
diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/ReadyResponse.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/ReadyResponse.cs
index 5573cdb59..d26fecf6d 100644
--- a/extra/admin-api/Models/Spacebar.Models.Gateway/ReadyResponse.cs
+++ b/extra/admin-api/Models/Spacebar.Models.Gateway/ReadyResponse.cs
@@ -1,3 +1,37 @@
+using System.Text.Json.Serialization;
+using Spacebar.Models.Generic;
+
namespace Spacebar.Models.Gateway;
-public class ReadyResponse { }
\ No newline at end of file
+public class ReadyResponse {
+ /* TODO: _trace, analytics_token, api_code_version, auth_session_id_hash, connected_accounts, consents, country_code, experiments, friend_suggestion_count, game_relationships,
+ geo_ordered_rtc_regions, guild_experiments, guild_join_requests*/
+ [JsonPropertyName("guilds")]
+ public required List Guilds { get; set; }
+
+ [JsonPropertyName("merged_members")]
+ public List> MergedMembers { get; set; }
+
+ // TODO: notification_settings
+
+ [JsonPropertyName("presences")]
+ public List Presences { get; set; } // TODO: right type?
+
+ [JsonPropertyName("private_channels")]
+ public List PrivateChannels { get; set; }
+
+ [JsonPropertyName("read_state")]
+ public object ReadStates { get; set; } // TODO: schemas
+
+ [JsonPropertyName("relationships")]
+ public List