Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved Date Decoding #9

Merged
merged 1 commit into from
Jun 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions Sources/DiscordKit/APIUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@

import Foundation

let iso8601 = { () -> ISO8601DateFormatter in
let fmt = ISO8601DateFormatter()
fmt.formatOptions = [.withInternetDateTime]
return fmt
}()

let iso8601WithFractionalSeconds = { () -> ISO8601DateFormatter in
let fmt = ISO8601DateFormatter()
fmt.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return fmt
}()
Comment on lines +10 to +20
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

annoyingly if you specify with fractional seconds, you must use fractional seconds, and the discord api is inconsistent with this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah thats the annoying part. Another problematic thing is the nonce value in Message. It can either be an int or a string according to the dev docs, so I have no idea how to add that property.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this PR seems quite well done, would be great if you could resolve the issues with #6


public extension DiscordAPI {
/// Populate a ``GatewayConnProperties`` struct with some constant
/// values + some dynamic versions
Expand Down Expand Up @@ -48,4 +60,32 @@ public extension DiscordAPI {
static var userAgent: String {
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) discord/\(GatewayConfig.default.parity.version) Chrome/91.0.4472.164 Electron/\(GatewayConfig.default.parity.electronVersion) Safari/537.36"
}

static func encoder() -> JSONEncoder {
let enc = JSONEncoder()
enc.dateEncodingStrategy = .custom({ date, encoder in
var container = encoder.singleValueContainer()
let dateString = iso8601WithFractionalSeconds.string(from: date)
try container.encode(dateString)
})
return enc
}

static func decoder() -> JSONDecoder {
let dec = JSONDecoder()
dec.dateDecodingStrategy = .custom({ decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)

if let date = iso8601.date(from: dateString) {
return date
}
if let date = iso8601WithFractionalSeconds.date(from: dateString) {
return date
}

throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateString)")
})
return dec
}
}
6 changes: 3 additions & 3 deletions Sources/DiscordKit/Gateway/RobustWebSocket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,10 @@ public class RobustWebSocket: NSObject, ObservableObject {

let decoded: GatewayIncoming
do {
decoded = try JSONDecoder().decode(GatewayIncoming.self, from: message.data(using: .utf8) ?? Data())
decoded = try DiscordAPI.decoder().decode(GatewayIncoming.self, from: message.data(using: .utf8) ?? Data())
} catch {
print(error)
print(message)
//print(message)
return
}

Expand Down Expand Up @@ -485,7 +485,7 @@ public extension RobustWebSocket {
guard connected else { return }

let sendPayload = GatewayOutgoing(op: op, d: data, s: seq)
guard let encoded = try? JSONEncoder().encode(sendPayload)
guard let encoded = try? DiscordAPI.encoder().encode(sendPayload)
else { return }

log.debug("Outgoing Payload: <\(String(describing: op), privacy: .public)> \(String(describing: data), privacy: .sensitive(mask: .hash)) [seq: \(String(describing: self.seq), privacy: .public)]")
Expand Down
8 changes: 4 additions & 4 deletions Sources/DiscordKit/Objects/Channel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public struct Channel: Identifiable, Codable, GatewayData, Equatable {
public let owner_id: Snowflake?
public let application_id: Snowflake?
public let parent_id: Snowflake? // ID of parent category (for channels) or parent channel (for threads)
public let last_pin_timestamp: ISOTimestamp?
public let last_pin_timestamp: Date?
public let rtc_region: String?
public let video_quality_mode: VideoQualityMode?
public let message_count: Int? // Approx. msg count in threads, stops counting at 50
Expand All @@ -70,16 +70,16 @@ public struct Channel: Identifiable, Codable, GatewayData, Equatable {
public struct ThreadMeta: Codable {
public let archived: Bool
public let auto_archive_duration: Int // Duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080
public let archive_timestamp: ISOTimestamp
public let archive_timestamp: Date
public let locked: Bool
public let invitable: Bool? // Only available in private threads
public let create_timestamp: ISOTimestamp? // Timestamp when the thread was created; only populated for threads created after 2022-01-09
public let create_timestamp: Date? // Timestamp when the thread was created; only populated for threads created after 2022-01-09
}

public struct ThreadMember: Codable, GatewayData {
public let id: Snowflake? // ID of thread
public let user_id: Snowflake? // ID of user
public let join_timestamp: ISOTimestamp // When user last joined thread
public let join_timestamp: Date // When user last joined thread
public let flags: Int // Any user-thread settings, currently only used for notifications
public let guild_id: Snowflake?
}
4 changes: 2 additions & 2 deletions Sources/DiscordKit/Objects/Embed.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public struct Embed: Codable, Identifiable {
public let type: EmbedType?
public let description: String?
public let url: String?
public let timestamp: ISOTimestamp?
public let timestamp: Date?
public let color: Int?
public let footer: EmbedFooter?
public let image: EmbedMedia?
Expand All @@ -31,7 +31,7 @@ public struct Embed: Codable, Identifiable {
public let author: EmbedAuthor?
public let fields: [EmbedField]?
public var id: String {
"\(title ?? "")\(description ?? "")\(url ?? "")\(String(color ?? 0))\(timestamp ?? "")"
"\(title ?? "")\(description ?? "")\(url ?? "")\(String(color ?? 0))\(String(timestamp?.timeIntervalSince1970 ?? 0))"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ import Foundation
public struct ChannelPinsUpdate: Codable, GatewayData {
public let guild_id: Snowflake?
public let channel_id: Snowflake
public let last_pin_timestamp: ISOTimestamp?
public let last_pin_timestamp: Date?
}
6 changes: 3 additions & 3 deletions Sources/DiscordKit/Objects/Gateway/Event/GuildMemberEvt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ public struct GuildMemberUpdate: Codable, GatewayData {
public let user: User
public let nick: String?
public let avatar: String? // User's guild avatar hash
public let joined_at: ISOTimestamp?
public let premium_since: ISOTimestamp? // When user started boosting guild
public let joined_at: Date?
public let premium_since: Date? // When user started boosting guild
public let deaf: Bool?
public let mute: Bool?
public let pending: Bool?
public let communication_disabled_until: ISOTimestamp?
public let communication_disabled_until: Date?
}
8 changes: 4 additions & 4 deletions Sources/DiscordKit/Objects/Guild.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public enum GuildFeature: String, Codable {
}

public struct Guild: GatewayData, Equatable, Identifiable {
public init(id: Snowflake, name: String, icon: String? = nil, icon_hash: String? = nil, splash: String? = nil, discovery_splash: String? = nil, owner: Bool? = nil, owner_id: Snowflake, permissions: String? = nil, region: String? = nil, afk_channel_id: Snowflake? = nil, afk_timeout: Int, widget_enabled: Bool? = nil, widget_channel_id: Snowflake? = nil, verification_level: VerificationLevel, default_message_notifications: MessageNotifLevel, explicit_content_filter: ExplicitContentFilterLevel, roles: [DecodableThrowable<Role>], emojis: [DecodableThrowable<Emoji>], features: [DecodableThrowable<GuildFeature>], mfa_level: MFALevel, application_id: Snowflake? = nil, system_channel_id: Snowflake? = nil, system_channel_flags: Int, rules_channel_id: Snowflake? = nil, joined_at: ISOTimestamp? = nil, large: Bool? = nil, unavailable: Bool? = nil, member_count: Int? = nil, voice_states: [VoiceState]? = nil, members: [Member]? = nil, channels: [Channel]? = nil, threads: [Channel]? = nil, presences: [PresenceUpdate]? = nil, max_presences: Int? = nil, max_members: Int? = nil, vanity_url_code: String? = nil, description: String? = nil, banner: String? = nil, premium_tier: PremiumLevel, premium_subscription_count: Int? = nil, preferred_locale: Locale, public_updates_channel_id: Snowflake? = nil, max_video_channel_users: Int? = nil, approximate_member_count: Int? = nil, approximate_presence_count: Int? = nil, welcome_screen: GuildWelcomeScreen? = nil, nsfw_level: NSFWLevel, stage_instances: [StageInstance]? = nil, stickers: [Sticker]? = nil, guild_scheduled_events: [GuildScheduledEvent]? = nil, premium_progress_bar_enabled: Bool) {
public init(id: Snowflake, name: String, icon: String? = nil, icon_hash: String? = nil, splash: String? = nil, discovery_splash: String? = nil, owner: Bool? = nil, owner_id: Snowflake, permissions: String? = nil, region: String? = nil, afk_channel_id: Snowflake? = nil, afk_timeout: Int, widget_enabled: Bool? = nil, widget_channel_id: Snowflake? = nil, verification_level: VerificationLevel, default_message_notifications: MessageNotifLevel, explicit_content_filter: ExplicitContentFilterLevel, roles: [DecodableThrowable<Role>], emojis: [DecodableThrowable<Emoji>], features: [DecodableThrowable<GuildFeature>], mfa_level: MFALevel, application_id: Snowflake? = nil, system_channel_id: Snowflake? = nil, system_channel_flags: Int, rules_channel_id: Snowflake? = nil, joined_at: Date? = nil, large: Bool? = nil, unavailable: Bool? = nil, member_count: Int? = nil, voice_states: [VoiceState]? = nil, members: [Member]? = nil, channels: [Channel]? = nil, threads: [Channel]? = nil, presences: [PresenceUpdate]? = nil, max_presences: Int? = nil, max_members: Int? = nil, vanity_url_code: String? = nil, description: String? = nil, banner: String? = nil, premium_tier: PremiumLevel, premium_subscription_count: Int? = nil, preferred_locale: Locale, public_updates_channel_id: Snowflake? = nil, max_video_channel_users: Int? = nil, approximate_member_count: Int? = nil, approximate_presence_count: Int? = nil, welcome_screen: GuildWelcomeScreen? = nil, nsfw_level: NSFWLevel, stage_instances: [StageInstance]? = nil, stickers: [Sticker]? = nil, guild_scheduled_events: [GuildScheduledEvent]? = nil, premium_progress_bar_enabled: Bool) {
self.id = id
self.name = name
self.icon = icon
Expand Down Expand Up @@ -122,7 +122,7 @@ public struct Guild: GatewayData, Equatable, Identifiable {
public let system_channel_id: Snowflake? // ID of channel for system-created messages
public let system_channel_flags: Int
public let rules_channel_id: Snowflake?
public var joined_at: ISOTimestamp?
public var joined_at: Date?
public var large: Bool?
public var unavailable: Bool? // If guild is unavailable due to an outage
public var member_count: Int?
Expand Down Expand Up @@ -205,8 +205,8 @@ public struct GuildScheduledEvent: Codable, GatewayData {
public let creator_id: Snowflake?
public let name: String
public let description: String?
public let scheduled_start_time: ISOTimestamp
public let scheduled_end_time: ISOTimestamp?
public let scheduled_start_time: Date
public let scheduled_end_time: Date?
public let privacy_level: GuildScheduledEvtPrivacyLvl
public let status: GuildScheduledEvtStatus
public let entity_type: GuildScheduledEvtEntityType
Expand Down
15 changes: 0 additions & 15 deletions Sources/DiscordKit/Objects/ISOTimestamp.swift

This file was deleted.

2 changes: 1 addition & 1 deletion Sources/DiscordKit/Objects/Integration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public struct Integration: Codable, GatewayData {
public let expire_grace_period: Int? // The grace period (in days) before expiring subscribers
public let user: User?
public let account: IntegrationAccount
public let synced_at: ISOTimestamp?
public let synced_at: Date?
public let subscriber_count: Int?
public let revoked: Bool?
public let application: IntegrationApplication?
Expand Down
6 changes: 3 additions & 3 deletions Sources/DiscordKit/Objects/Member.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ public struct Member: Codable, GatewayData {
public let nick: String?
public let avatar: String?
public let roles: [Snowflake]
public let joined_at: ISOTimestamp
public let premium_since: ISOTimestamp? // When the user started boosting the guild
public let joined_at: Date
public let premium_since: Date? // When the user started boosting the guild
public let deaf: Bool
public let mute: Bool
public let pending: Bool?
public let permissions: String? // Total permissions of the member in the channel, including overwrites, returned when in the interaction object
public let communication_disabled_until: ISOTimestamp? // When the user's timeout will expire and the user will be able to communicate in the guild again, null or a time in the past if the user is not timed out
public let communication_disabled_until: Date? // When the user's timeout will expire and the user will be able to communicate in the guild again, null or a time in the past if the user is not timed out
public let guild_id: Snowflake?
}
12 changes: 6 additions & 6 deletions Sources/DiscordKit/Objects/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@ public struct Message: Codable, GatewayData, Equatable {
public var content: String

/// When this message was sent
public let timestamp: ISOTimestamp
public let timestamp: Date

/// When this message was edited (or null if never)
public var edited_timestamp: ISOTimestamp?
public var edited_timestamp: Date?

/// If this was a TTS message
public var tts: Bool

Expand Down Expand Up @@ -191,8 +191,8 @@ public struct PartialMessage: Codable, GatewayData {
public let author: User?
public let member: Member?
public let content: String? // The message contents (up to 2000 characters)
public let timestamp: ISOTimestamp?
public let edited_timestamp: ISOTimestamp?
public let timestamp: Date?
public let edited_timestamp: Date?
public let tts: Bool?
public let mention_everyone: Bool?
public let mentions: [User]?
Expand Down
6 changes: 3 additions & 3 deletions Sources/DiscordKit/Objects/User.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ public struct CurrentUser: Codable, GatewayData, Equatable {
/// > Warning: The user profile endpoint is undocumented, and this struct
/// > was created purely from reverse engineering and observations.
public struct UserProfile: Codable, GatewayData {
public init(connected_accounts: [Connection], guild_member: Member?, premium_guild_since: ISOTimestamp?, premium_since: ISOTimestamp?, mutual_guilds: [MutualGuild]?, user: User) {
public init(connected_accounts: [Connection], guild_member: Member?, premium_guild_since: Date?, premium_since: Date?, mutual_guilds: [MutualGuild]?, user: User) {
self.connected_accounts = connected_accounts
self.guild_member = guild_member
self.premium_guild_since = premium_guild_since
Expand All @@ -163,8 +163,8 @@ public struct UserProfile: Codable, GatewayData {

public let connected_accounts: [Connection]
public let guild_member: Member?
public let premium_guild_since: ISOTimestamp?
public let premium_since: ISOTimestamp?
public let premium_guild_since: Date?
public let premium_since: Date?
public let mutual_guilds: [MutualGuild]?

/// A more complete ``User`` struct, containing the user's bio, among others.
Expand Down
10 changes: 5 additions & 5 deletions Sources/DiscordKit/REST/APIRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public extension DiscordAPI {

req.setValue(Locale.englishUS.rawValue, forHTTPHeaderField: "x-discord-locale")
req.setValue("bugReporterEnabled", forHTTPHeaderField: "x-debug-options")
guard let superEncoded = try? JSONEncoder().encode(getSuperProperties()) else {
guard let superEncoded = try? DiscordAPI.encoder().encode(getSuperProperties()) else {
DiscordAPI.log.error("Couldn't encode super properties, something is seriously wrong")
return nil
}
Expand Down Expand Up @@ -118,9 +118,9 @@ public extension DiscordAPI {
guard let d = try? await makeRequest(path: path, query: query)
else { return nil }

return try JSONDecoder().decode(T.self, from: d)
return try DiscordAPI.decoder().decode(T.self, from: d)
} catch let DecodingError.dataCorrupted(context) {
print(context)
print(context.debugDescription)
} catch let DecodingError.keyNotFound(key, context) {
print("Key '\(key)' not found:", context.debugDescription)
print("codingPath:", context.codingPath)
Expand All @@ -142,7 +142,7 @@ public extension DiscordAPI {
body: B? = nil,
attachments: [URL] = []
) async -> D? {
let p = body != nil ? try? JSONEncoder().encode(body) : nil
let p = body != nil ? try? DiscordAPI.encoder().encode(body) : nil
guard let d = try? await makeRequest(
path: path,
attachments: attachments,
Expand All @@ -151,7 +151,7 @@ public extension DiscordAPI {
)
else { return nil }

return try? JSONDecoder().decode(D.self, from: d)
return try? DiscordAPI.decoder().decode(D.self, from: d)
}

/// Make a `POST` request to the Discord REST API, for endpoints
Expand Down