From c2232a5e140436199022e6766a844025003b58e0 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 8 Jun 2024 13:29:56 -0700 Subject: [PATCH] Don't fail decoding when one status fails to decode Also remove old workaround for bad dates from #477 Closes #478 --- .../Pachyderm/Sources/Pachyderm/Client.swift | 16 ++++------ .../Sources/Pachyderm/Model/Account.swift | 4 +-- .../Pachyderm/Model/SearchResults.swift | 2 +- .../Sources/Pachyderm/Model/Timeline.swift | 4 +-- .../Pachyderm/Utilities/TryDecode.swift | 32 +++++++++++++++++++ .../ConversationViewController.swift | 2 +- .../TrendingStatusesViewController.swift | 2 +- .../Explore/TrendsViewController.swift | 4 +-- ...LocalPredicateStatusesViewController.swift | 13 +++++--- .../ProfileStatusesViewController.swift | 10 +++--- .../Screens/Report/ReportAddStatusView.swift | 2 +- .../Search/SearchResultsViewController.swift | 4 +-- .../Timeline/TimelineViewController.swift | 11 ++++--- 13 files changed, 70 insertions(+), 36 deletions(-) create mode 100644 Packages/Pachyderm/Sources/Pachyderm/Utilities/TryDecode.swift diff --git a/Packages/Pachyderm/Sources/Pachyderm/Client.swift b/Packages/Pachyderm/Sources/Pachyderm/Client.swift index 5ec8c134b1..906cf2eedd 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Client.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Client.swift @@ -42,8 +42,7 @@ public struct Client: Sendable { } else if let date = iso8601.date(from: str) { return date } else { -// throw DecodingError.typeMismatch(Date.self, .init(codingPath: container.codingPath, debugDescription: "unexpected date format: \(str)")) - return Date(timeIntervalSinceReferenceDate: 0) + throw DecodingError.typeMismatch(Date.self, .init(codingPath: container.codingPath, debugDescription: "unexpected date format: \(str)")) } }) @@ -205,8 +204,8 @@ public struct Client: Sendable { return Request(method: .get, path: "/api/v1/accounts/verify_credentials") } - public static func getFavourites(range: RequestRange = .default) -> Request<[Status]> { - var request = Request<[Status]>(method: .get, path: "/api/v1/favourites") + public static func getFavourites(range: RequestRange = .default) -> Request<[TryDecode]> { + var request = Request<[TryDecode]>(method: .get, path: "/api/v1/favourites") request.range = range return request } @@ -457,14 +456,13 @@ public struct Client: Sendable { } // MARK: - Timelines - public static func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[Status]> { + public static func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[TryDecode]> { return timeline.request(range: range) } - // MARK: - Bookmarks - public static func getBookmarks(range: RequestRange = .default) -> Request<[Status]> { - var request = Request<[Status]>(method: .get, path: "/api/v1/bookmarks") + public static func getBookmarks(range: RequestRange = .default) -> Request<[TryDecode]> { + var request = Request<[TryDecode]>(method: .get, path: "/api/v1/bookmarks") request.range = range return request } @@ -492,7 +490,7 @@ public struct Client: Sendable { return Request<[Hashtag]>(method: .get, path: "/api/v1/trends/tags", queryParameters: parameters) } - public static func getTrendingStatuses(limit: Int? = nil, offset: Int? = nil) -> Request<[Status]> { + public static func getTrendingStatuses(limit: Int? = nil, offset: Int? = nil) -> Request<[TryDecode]> { var parameters: [Parameter] = [] if let limit { parameters.append("limit" => limit) diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Account.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Account.swift index baf2d1e798..3359b8d7f8 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Account.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Account.swift @@ -95,8 +95,8 @@ public final class Account: AccountProtocol, Decodable, Sendable { return request } - public static func getStatuses(_ accountID: String, range: RequestRange = .default, onlyMedia: Bool? = nil, pinned: Bool? = nil, excludeReplies: Bool? = nil, excludeReblogs: Bool? = nil) -> Request<[Status]> { - var request = Request<[Status]>(method: .get, path: "/api/v1/accounts/\(accountID)/statuses", queryParameters: [ + public static func getStatuses(_ accountID: String, range: RequestRange = .default, onlyMedia: Bool? = nil, pinned: Bool? = nil, excludeReplies: Bool? = nil, excludeReblogs: Bool? = nil) -> Request<[TryDecode]> { + var request = Request<[TryDecode]>(method: .get, path: "/api/v1/accounts/\(accountID)/statuses", queryParameters: [ "only_media" => onlyMedia, "pinned" => pinned, "exclude_replies" => excludeReplies, diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/SearchResults.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/SearchResults.swift index 56013a366b..008e8a6ef6 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/SearchResults.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/SearchResults.swift @@ -10,7 +10,7 @@ import Foundation public struct SearchResults: Decodable, Sendable { public let accounts: [Account] - public let statuses: [Status] + public let statuses: [TryDecode] public let hashtags: [Hashtag] private enum CodingKeys: String, CodingKey { diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Timeline.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Timeline.swift index 137a0aac89..e1eb105e4e 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Timeline.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Timeline.swift @@ -32,8 +32,8 @@ extension Timeline { } } - func request(range: RequestRange) -> Request<[Status]> { - var request: Request<[Status]> = Request<[Status]>(method: .get, path: endpoint) + func request(range: RequestRange) -> Request<[TryDecode]> { + var request = Request<[TryDecode]>(method: .get, path: endpoint) if case .public(true) = self { request.queryParameters.append("local" => true) } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Utilities/TryDecode.swift b/Packages/Pachyderm/Sources/Pachyderm/Utilities/TryDecode.swift new file mode 100644 index 0000000000..b7188bc88a --- /dev/null +++ b/Packages/Pachyderm/Sources/Pachyderm/Utilities/TryDecode.swift @@ -0,0 +1,32 @@ +// +// TryDecode.swift +// Pachyderm +// +// Created by Shadowfacts on 6/8/24. +// + +import Foundation + +public enum TryDecode: Decodable { + case error(String) + case value(T) + + public init(from decoder: any Decoder) throws { + do { + self = .value(try T(from: decoder)) + } catch { + self = .error(error.localizedDescription) + } + } + + public var value: T? { + if case .value(let value) = self { + value + } else { + nil + } + } +} + +extension TryDecode: Sendable where T: Sendable { +} diff --git a/Tusker/Screens/Conversation/ConversationViewController.swift b/Tusker/Screens/Conversation/ConversationViewController.swift index f6bddceefb..dde6534d45 100644 --- a/Tusker/Screens/Conversation/ConversationViewController.swift +++ b/Tusker/Screens/Conversation/ConversationViewController.swift @@ -232,7 +232,7 @@ class ConversationViewController: UIViewController { let request = Client.search(query: effectiveURL, types: [.statuses], resolve: true) do { let (results, _) = try await mastodonController.run(request) - guard let status = results.statuses.first(where: { $0.url?.serialized() == effectiveURL }) else { + guard let status = results.statuses.compactMap(\.value).first(where: { $0.url?.serialized() == effectiveURL }) else { throw UnableToResolveError() } _ = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status) diff --git a/Tusker/Screens/Explore/TrendingStatusesViewController.swift b/Tusker/Screens/Explore/TrendingStatusesViewController.swift index 6d8900b4e6..63d23a4805 100644 --- a/Tusker/Screens/Explore/TrendingStatusesViewController.swift +++ b/Tusker/Screens/Explore/TrendingStatusesViewController.swift @@ -123,7 +123,7 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController private func loadTrendingStatuses() async { let statuses: [Status] do { - statuses = try await mastodonController.run(Client.getTrendingStatuses()).0 + statuses = try await mastodonController.run(Client.getTrendingStatuses()).0.compactMap(\.value) } catch { let snapshot = NSDiffableDataSourceSnapshot() await MainActor.run { diff --git a/Tusker/Screens/Explore/TrendsViewController.swift b/Tusker/Screens/Explore/TrendsViewController.swift index e280d04606..52c7786d29 100644 --- a/Tusker/Screens/Explore/TrendsViewController.swift +++ b/Tusker/Screens/Explore/TrendsViewController.swift @@ -277,7 +277,7 @@ class TrendsViewController: UIViewController, CollectionViewController { let linksReq = Client.getTrendingLinks(limit: 10) async let links = try? mastodonController.run(linksReq).0 let statusesReq = Client.getTrendingStatuses(limit: 10) - async let statuses = try? mastodonController.run(statusesReq).0 + async let statuses = try? mastodonController.run(statusesReq).0.compactMap(\.value) if let links = await links { if snapshot.sectionIdentifiers.contains(.profileSuggestions) { @@ -332,7 +332,7 @@ class TrendsViewController: UIViewController, CollectionViewController { do { let request = Client.getTrendingStatuses(offset: origSnapshot.itemIdentifiers(inSection: .trendingStatuses).count) - let (statuses, _) = try await mastodonController.run(request) + let statuses = try await mastodonController.run(request).0.compactMap(\.value) await mastodonController.persistentContainer.addAll(statuses: statuses) diff --git a/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift b/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift index 92939bf9b0..a743da7ee0 100644 --- a/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift +++ b/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift @@ -17,7 +17,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont let mastodonController: MastodonController private let predicate: (StatusMO) -> Bool private let predicateTitle: String - private let request: (RequestRange) -> Request<[Status]> + private let request: (RequestRange) -> Request<[TryDecode]> var collectionView: UICollectionView! { view as? UICollectionView @@ -28,7 +28,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont private var newer: RequestRange? private var older: RequestRange? - init(predicate: @escaping (StatusMO) -> Bool, predicateTitle: String, request: @escaping (RequestRange) -> Request<[Status]>, mastodonController: MastodonController) { + init(predicate: @escaping (StatusMO) -> Bool, predicateTitle: String, request: @escaping (RequestRange) -> Request<[TryDecode]>, mastodonController: MastodonController) { self.mastodonController = mastodonController self.predicate = predicate self.predicateTitle = predicateTitle @@ -140,7 +140,8 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont do { let req = request(.count(Self.pageSize)) - let (statuses, pagination) = try await mastodonController.run(req) + let (tryStatuses, pagination) = try await mastodonController.run(req) + let statuses = tryStatuses.compactMap(\.value) newer = pagination?.newer older = pagination?.older @@ -180,7 +181,8 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont do { let req = request(older.withCount(Self.pageSize)) - let (statuses, pagination) = try await mastodonController.run(req) + let (tryStatuses, pagination) = try await mastodonController.run(req) + let statuses = tryStatuses.compactMap(\.value) self.older = pagination?.older await mastodonController.persistentContainer.addAll(statuses: statuses) @@ -278,7 +280,8 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont Task { do { let req = request(newer.withCount(Self.pageSize)) - let (statuses, pagination) = try await mastodonController.run(req) + let (tryStatuses, pagination) = try await mastodonController.run(req) + let statuses = tryStatuses.compactMap(\.value) self.newer = pagination?.newer await mastodonController.persistentContainer.addAll(statuses: statuses) diff --git a/Tusker/Screens/Profile/ProfileStatusesViewController.swift b/Tusker/Screens/Profile/ProfileStatusesViewController.swift index 7a47c82ca2..a4880a6feb 100644 --- a/Tusker/Screens/Profile/ProfileStatusesViewController.swift +++ b/Tusker/Screens/Profile/ProfileStatusesViewController.swift @@ -310,7 +310,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie } let request = Account.getStatuses(accountID, range: .default, onlyMedia: false, pinned: true, excludeReplies: false) - let (statuses, _) = try await mastodonController.run(request) + let statuses = try await mastodonController.run(request).0.compactMap(\.value) await withCheckedContinuation { continuation in mastodonController.persistentContainer.addAll(statuses: statuses) { @@ -526,7 +526,7 @@ extension ProfileStatusesViewController { extension ProfileStatusesViewController: TimelineLikeControllerDataSource { typealias TimelineItem = String // status ID - private func request(for range: RequestRange = .default) -> Request<[Status]> { + private func request(for range: RequestRange = .default) -> Request<[TryDecode]> { switch kind { case .statuses: return Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: false, excludeReplies: true) @@ -539,7 +539,7 @@ extension ProfileStatusesViewController: TimelineLikeControllerDataSource { func loadInitial() async throws -> [String] { let request = request() - let (statuses, _) = try await mastodonController.run(request) + let statuses = try await mastodonController.run(request).0.compactMap(\.value) if !statuses.isEmpty { newer = .after(id: statuses.first!.id, count: nil) @@ -559,7 +559,7 @@ extension ProfileStatusesViewController: TimelineLikeControllerDataSource { } let request = request(for: newer) - let (statuses, _) = try await mastodonController.run(request) + let statuses = try await mastodonController.run(request).0.compactMap(\.value) guard !statuses.isEmpty else { throw Error.allCaughtUp @@ -580,7 +580,7 @@ extension ProfileStatusesViewController: TimelineLikeControllerDataSource { } let request = request(for: older) - let (statuses, _) = try await mastodonController.run(request) + let statuses = try await mastodonController.run(request).0.compactMap(\.value) guard !statuses.isEmpty else { return [] diff --git a/Tusker/Screens/Report/ReportAddStatusView.swift b/Tusker/Screens/Report/ReportAddStatusView.swift index 7ea75315c9..e56cc76ba0 100644 --- a/Tusker/Screens/Report/ReportAddStatusView.swift +++ b/Tusker/Screens/Report/ReportAddStatusView.swift @@ -53,7 +53,7 @@ struct ReportAddStatusView: View { .task { @MainActor in do { let req = Account.getStatuses(report.accountID, range: .count(40), excludeReplies: false, excludeReblogs: true) - let (statuses, _) = try await mastodonController.run(req) + let statuses = try await mastodonController.run(req).0.compactMap(\.value) await mastodonController.persistentContainer.addAll(statuses: statuses) self.statuses = statuses.compactMap { mastodonController.persistentContainer.status(for: $0.id) } } catch { diff --git a/Tusker/Screens/Search/SearchResultsViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift index 89d9fa0570..9e7707caf1 100644 --- a/Tusker/Screens/Search/SearchResultsViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -266,7 +266,7 @@ class SearchResultsViewController: UIViewController, CollectionViewController { guard self.currentQuery == query else { return } self.mastodonController.persistentContainer.performBatchUpdates { (context, addAccounts, addStatuses) in addAccounts(results.accounts) - addStatuses(results.statuses) + addStatuses(results.statuses.compactMap(\.value)) } completion: { DispatchQueue.main.async { self.showSearchResults(results) @@ -299,7 +299,7 @@ class SearchResultsViewController: UIViewController, CollectionViewController { } if !results.statuses.isEmpty && resultTypes.contains(.statuses) { snapshot.appendSections([.statuses]) - snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses) + snapshot.appendItems(results.statuses.compactMap(\.value).map { .status($0.id, .unknown) }, toSection: .statuses) } dataSource.apply(snapshot) diff --git a/Tusker/Screens/Timeline/TimelineViewController.swift b/Tusker/Screens/Timeline/TimelineViewController.swift index a4b83c144b..1978195698 100644 --- a/Tusker/Screens/Timeline/TimelineViewController.swift +++ b/Tusker/Screens/Timeline/TimelineViewController.swift @@ -565,7 +565,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro do { let (marker, _) = try await mastodonController.run(TimelineMarkers.request(timeline: .home)) async let status = try await mastodonController.run(Client.getStatus(id: marker.lastReadID)).0 - async let olderStatuses = try await mastodonController.run(Client.getStatuses(timeline: .home, range: .before(id: marker.lastReadID, count: Self.pageSize))).0 + // TODO: consider replacing undecodable statuses here with items to indicate that to the user + async let olderStatuses = try await mastodonController.run(Client.getStatuses(timeline: .home, range: .before(id: marker.lastReadID, count: Self.pageSize))).0.compactMap(\.value) let allStatuses = try await [status] + olderStatuses await mastodonController.persistentContainer.addAll(statuses: allStatuses) @@ -1100,7 +1101,7 @@ extension TimelineViewController: TimelineLikeControllerDataSource { func loadInitial() async throws -> [TimelineItem] { let request = Client.getStatuses(timeline: timeline, range: .count(TimelineViewController.pageSize)) - let (statuses, _) = try await mastodonController.run(request) + let statuses = try await mastodonController.run(request).0.compactMap(\.value) await withCheckedContinuation { continuation in mastodonController.persistentContainer.addAll(statuses: statuses) { @@ -1119,7 +1120,7 @@ extension TimelineViewController: TimelineLikeControllerDataSource { let newer = RequestRange.after(id: id, count: TimelineViewController.pageSize) let request = Client.getStatuses(timeline: timeline, range: newer) - let (statuses, _) = try await mastodonController.run(request) + let statuses = try await mastodonController.run(request).0.compactMap(\.value) guard !statuses.isEmpty else { throw TimelineViewController.Error.allCaughtUp @@ -1143,7 +1144,7 @@ extension TimelineViewController: TimelineLikeControllerDataSource { let older = RequestRange.before(id: id, count: TimelineViewController.pageSize) let request = Client.getStatuses(timeline: timeline, range: older) - let (statuses, _) = try await mastodonController.run(request) + let statuses = try await mastodonController.run(request).0.compactMap(\.value) guard !statuses.isEmpty else { return [] @@ -1181,7 +1182,7 @@ extension TimelineViewController: TimelineLikeControllerDataSource { } let request = Client.getStatuses(timeline: timeline, range: range) - let (statuses, _) = try await mastodonController.run(request) + let statuses = try await mastodonController.run(request).0.compactMap(\.value) guard !statuses.isEmpty else { return []