diff --git a/Package.swift b/Package.swift index a8fe8a94..3172714e 100644 --- a/Package.swift +++ b/Package.swift @@ -11,8 +11,8 @@ let package = Package( targets: [ .binaryTarget( name: "WordPressKit", - url: "https://github.com/user-attachments/files/19732825/WordPressKit.zip", - checksum: "0ecd8b19cd00a4ef363ab6e826f92166c1efa4693db8f3218533510a3f951dbb" + url: "https://github.com/user-attachments/files/19949939/WordPressKit.zip", + checksum: "ba06ff0716595023dd6c98b6a5bc74d4abb35bfb668e24026ffd041460f59137" ), ] ) diff --git a/Sources/CoreAPI/WordPressComRestApi.swift b/Sources/CoreAPI/WordPressComRestApi.swift index 17b476ef..92d09222 100644 --- a/Sources/CoreAPI/WordPressComRestApi.swift +++ b/Sources/CoreAPI/WordPressComRestApi.swift @@ -376,7 +376,7 @@ open class WordPressComRestApi: NSObject { open func perform( _ method: HTTPRequestBuilder.Method, URLString: String, - parameters: [String: AnyObject]? = nil, + parameters: [String: Any]? = nil, fulfilling progress: Progress? = nil ) async -> APIResult { await perform(method, URLString: URLString, parameters: parameters, fulfilling: progress) { @@ -387,7 +387,7 @@ open class WordPressComRestApi: NSObject { open func perform( _ method: HTTPRequestBuilder.Method, URLString: String, - parameters: [String: AnyObject]? = nil, + parameters: [String: Any]? = nil, fulfilling progress: Progress? = nil, jsonDecoder: JSONDecoder? = nil, type: T.Type = T.self @@ -401,7 +401,7 @@ open class WordPressComRestApi: NSObject { private func perform( _ method: HTTPRequestBuilder.Method, URLString: String, - parameters: [String: AnyObject]?, + parameters: [String: Any]?, fulfilling progress: Progress?, decoder: @escaping (Data) throws -> T ) async -> APIResult { diff --git a/Sources/WordPressKit/Models/RemoteSubscriber.swift b/Sources/WordPressKit/Models/RemoteSubscriber.swift new file mode 100644 index 00000000..f6414b7d --- /dev/null +++ b/Sources/WordPressKit/Models/RemoteSubscriber.swift @@ -0,0 +1,23 @@ +import Foundation + +public struct RemoteSubscriber: Decodable { + public let userID: Int + public let subscriptionID: Int + public let emailAddress: String? + public let dateSubscribed: Date + public let isEmailSubscriber: Bool + public let subscriptionStatus: String? + public let displayName: String? + public let avatar: String? + + private enum CodingKeys: String, CodingKey { + case userID = "user_id" + case subscriptionID = "subscription_id" + case emailAddress = "email_address" + case dateSubscribed = "date_subscribed" + case isEmailSubscriber = "is_email_subscriber" + case subscriptionStatus = "subscription_status" + case displayName = "display_name" + case avatar + } +} diff --git a/Sources/WordPressKit/Services/PeopleServiceRemote.swift b/Sources/WordPressKit/Services/PeopleServiceRemote.swift index e23b26ae..5c40228b 100644 --- a/Sources/WordPressKit/Services/PeopleServiceRemote.swift +++ b/Sources/WordPressKit/Services/PeopleServiceRemote.swift @@ -173,6 +173,91 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { }) } + public struct SubscribersParameters { + public var sortField: SortField? + public var sortOrder: SortOrder? + public var filters: [Filter] + + public enum SortField: String { + case dateSubscribed = "date_subscribed" + case email = "email" + case name = "name" + case plan = "plan" + case subscriptionStatus = "subscription_status" + } + + public enum SortOrder: String { + case ascending = "asc" + case descending = "dsc" + } + + public protocol Filter: CustomStringConvertible {} + + public enum FilterSubscriptionType: String, Filter { + case email = "email_subscriber" + case reader = "reader_subscriber" + case unconfirmed = "unconfirmed_subscriber" + case blocked = "blocked_subscriber" + + public var description: String { rawValue } + } + + public enum FilterPaymentType: String, Filter { + case free + case paid + + public var description: String { rawValue } + } + + public init(sortField: SortField? = nil, sortOrder: SortOrder? = nil, filters: [Filter] = []) { + self.sortField = sortField + self.sortOrder = sortOrder + self.filters = filters + } + } + + public struct SubscribersResponse: Decodable { + public var total: Int + public var pages: Int + public var page: Int + public var subscribers: [RemoteSubscriber] + } + + public func getSubscribers( + siteID: Int, + page: Int? = nil, + perPage: Int? = 25, + parameters: SubscribersParameters = .init() + ) async throws -> SubscribersResponse { + let url = self.path(forEndpoint: "sites/\(siteID)/subscribers", withVersion: ._2_0) + var query: [String: Any] = [:] + if let page { + query["page"] = page + } + if let perPage { + query["per_page"] = perPage + } + if let sortField = parameters.sortField { + query["sort"] = sortField.rawValue + } + if let sortOrder = parameters.sortOrder { + query["sort_order"] = sortOrder.rawValue + } + if !parameters.filters.isEmpty { + query["filters"] = parameters.filters.map { $0.description } + } + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.supportMultipleDateFormats + + return try await wordPressComRestApi.perform( + .get, + URLString: url, + jsonDecoder: decoder, + type: SubscribersResponse.self + ).get().body + } + /// Updates a specified User's Role /// /// - Parameters: diff --git a/Tests/WordPressKitTests/Mock Data/site-subscribers-response.json b/Tests/WordPressKitTests/Mock Data/site-subscribers-response.json new file mode 100644 index 00000000..e6004902 --- /dev/null +++ b/Tests/WordPressKitTests/Mock Data/site-subscribers-response.json @@ -0,0 +1,20 @@ +{ + "total": 1, + "pages": 1, + "page": 1, + "per_page": 100, + "subscribers": [ + { + "user_id": 1, + "subscription_id": 2, + "email_address": "user@example.com", + "date_subscribed": "2025-02-28T19:36:36+00:00", + "is_email_subscriber": false, + "subscription_status": "Not subscribed", + "avatar": "https://0.gravatar.com/avatar/example.jpg", + "display_name": "Test", + "url": "http://example.wordpress.com" + } + ], + "is_owner_subscribed": true +} diff --git a/Tests/WordPressKitTests/Tests/JSONLoader.swift b/Tests/WordPressKitTests/Tests/JSONLoader.swift index e6bda2b9..8fd3b73b 100644 --- a/Tests/WordPressKitTests/Tests/JSONLoader.swift +++ b/Tests/WordPressKitTests/Tests/JSONLoader.swift @@ -12,7 +12,7 @@ import Foundation */ @objc open func loadFile(_ name: String, type: String) -> JSONDictionary? { - let path = Bundle(for: Swift.type(of: self)).path(forResource: name, ofType: type) + let path = JSONLoader.bundle.path(forResource: name, ofType: type) if let unwrappedPath = path { return loadFile(unwrappedPath) @@ -47,4 +47,15 @@ import Foundation return nil } } + + public static func data(named name: String, ext: String = "json") throws -> Data { + guard let url = Bundle(for: JSONLoader.self).url(forResource: name, withExtension: ext) else { + throw URLError(.badURL) + } + return try Data(contentsOf: url) + } + + private static var bundle: Bundle { + Bundle(for: JSONLoader.self) + } } diff --git a/Tests/WordPressKitTests/Tests/MockWordPressComRestApi.swift b/Tests/WordPressKitTests/Tests/MockWordPressComRestApi.swift index 9b552f7f..8edd0079 100644 --- a/Tests/WordPressKitTests/Tests/MockWordPressComRestApi.swift +++ b/Tests/WordPressKitTests/Tests/MockWordPressComRestApi.swift @@ -49,7 +49,7 @@ class MockWordPressComRestApi: WordPressComRestApi { override func perform( _ method: HTTPRequestBuilder.Method, URLString: String, - parameters: [String: AnyObject]? = nil, + parameters: [String: Any]? = nil, fulfilling progress: Progress? = nil, jsonDecoder: JSONDecoder? = nil, type: T.Type = T.self diff --git a/Tests/WordPressKitTests/Tests/PeopleServiceRemoteTests.swift b/Tests/WordPressKitTests/Tests/PeopleServiceRemoteTests.swift index 9ac254eb..1df76fb6 100644 --- a/Tests/WordPressKitTests/Tests/PeopleServiceRemoteTests.swift +++ b/Tests/WordPressKitTests/Tests/PeopleServiceRemoteTests.swift @@ -795,4 +795,17 @@ class PeopleServiceRemoteTests: RemoteTestCase, RESTTestable { waitForExpectations(timeout: timeout, handler: nil) } + func testDecodeSubscribersResponse() throws { + let data = try JSONLoader.data(named: "site-subscribers-response") + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.supportMultipleDateFormats + + let response = try decoder.decode(PeopleServiceRemote.SubscribersResponse.self, from: data) + + XCTAssertEqual(response.total, 1) + + let subscriber = try XCTUnwrap(response.subscribers.first) + XCTAssertEqual(subscriber.userID, 1) + } } diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index 23851528..f5ef8417 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 0C363D452C41B468004E241D /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = 0C363D442C41B468004E241D /* OHHTTPStubs */; }; 0C363D472C41B468004E241D /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C363D462C41B468004E241D /* OHHTTPStubsSwift */; }; 0C674E302BF3A91300F3B3D4 /* JetpackAIServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C674E2F2BF3A91300F3B3D4 /* JetpackAIServiceRemote.swift */; }; + 0C8069A72DC03E85008DFC2F /* site-subscribers-response.json in Resources */ = {isa = PBXBuildFile; fileRef = 0C8069A62DC03E85008DFC2F /* site-subscribers-response.json */; }; 0C938A062C416789009BA7B2 /* Secret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C938A052C416789009BA7B2 /* Secret.swift */; }; 0C938A092C4167BC009BA7B2 /* NSString+XMLExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 0C938A072C4167BB009BA7B2 /* NSString+XMLExtensions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 0C938A0A2C4167BC009BA7B2 /* NSString+XMLExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 0C938A082C4167BB009BA7B2 /* NSString+XMLExtensions.m */; }; @@ -50,6 +51,7 @@ 0CCD4C5C2C41700B00B53F9A /* UIDevice+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCD4C5B2C41700B00B53F9A /* UIDevice+Extensions.swift */; }; 0CCD4C5F2C41711800B53F9A /* NSObject-SafeExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = 0CCD4C5E2C41711800B53F9A /* NSObject-SafeExpectations */; }; 0CCD4C622C41712800B53F9A /* wpxmlrpc in Frameworks */ = {isa = PBXBuildFile; productRef = 0CCD4C612C41712800B53F9A /* wpxmlrpc */; }; + 0CE4E8252DC027AC00056DD9 /* RemoteSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE4E8242DC027AC00056DD9 /* RemoteSubscriber.swift */; }; 0CED1FE82B617CF300E6DD52 /* AtomicSiteServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CED1FE72B617CF300E6DD52 /* AtomicSiteServiceRemote.swift */; }; 0CED1FEB2B617D7D00E6DD52 /* AtomicLogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CED1FEA2B617D7D00E6DD52 /* AtomicLogs.swift */; }; 1769DEAA24729AFF00F42EFC /* HomepageSettingsServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769DEA924729AFF00F42EFC /* HomepageSettingsServiceRemote.swift */; }; @@ -800,6 +802,7 @@ 0C3A2A412A2E7BA500FD91D6 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 0C6183C62C420A3700289E73 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 0C674E2F2BF3A91300F3B3D4 /* JetpackAIServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackAIServiceRemote.swift; sourceTree = ""; }; + 0C8069A62DC03E85008DFC2F /* site-subscribers-response.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "site-subscribers-response.json"; sourceTree = ""; }; 0C938A052C416789009BA7B2 /* Secret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secret.swift; sourceTree = ""; }; 0C938A072C4167BB009BA7B2 /* NSString+XMLExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+XMLExtensions.h"; sourceTree = ""; }; 0C938A082C4167BB009BA7B2 /* NSString+XMLExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+XMLExtensions.m"; sourceTree = ""; }; @@ -824,6 +827,7 @@ 0CB1905F2A2A6943004D3E80 /* blaze-campaigns-search.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blaze-campaigns-search.json"; sourceTree = ""; }; 0CB190642A2A7569004D3E80 /* BlazeCampaignsSearchResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsSearchResponse.swift; sourceTree = ""; }; 0CCD4C5B2C41700B00B53F9A /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extensions.swift"; sourceTree = ""; }; + 0CE4E8242DC027AC00056DD9 /* RemoteSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSubscriber.swift; sourceTree = ""; }; 0CED1FE72B617CF300E6DD52 /* AtomicSiteServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicSiteServiceRemote.swift; sourceTree = ""; }; 0CED1FEA2B617D7D00E6DD52 /* AtomicLogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicLogs.swift; sourceTree = ""; }; 1769DEA924729AFF00F42EFC /* HomepageSettingsServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepageSettingsServiceRemote.swift; sourceTree = ""; }; @@ -1866,6 +1870,7 @@ 4A68E3DC294070A7004AC3DC /* RemoteReaderSite.swift */, 4A68E3E0294076C1004AC3DC /* RemoteReaderSiteInfo.swift */, 9F3E0B9A208732B2009CB5BA /* RemoteReaderSiteInfoSubscription.swift */, + 0CE4E8242DC027AC00056DD9 /* RemoteSubscriber.swift */, 4A68E3DE29407100004AC3DC /* RemoteReaderTopic.swift */, 74E2295D1F1E777B0085F7F2 /* RemoteSharingButton.swift */, 7430C9C81F192F260051B8E6 /* RemoteSourcePostAttribution.h */, @@ -2541,6 +2546,7 @@ 9AEAA772215E71C000876E62 /* site-quick-start-success.json */, 74D67F0C1F15C2D70010C5ED /* site-roles-auth-failure.json */, 74D67F0D1F15C2D70010C5ED /* site-roles-bad-json-failure.json */, + 0C8069A62DC03E85008DFC2F /* site-subscribers-response.json */, 74D67F0E1F15C2D70010C5ED /* site-roles-success.json */, D8DB404121EF22B500B8238E /* site-segments-multiple.json */, D813437721F6D7DC0060D99A /* site-segments-single.json */, @@ -3030,6 +3036,7 @@ C738CAF528622953001BE107 /* qrlogin-validate-expired-401.json in Resources */, 937250EC267A15060086075F /* stats-referrer-mark-as-spam.json in Resources */, F3FF8A23279C954100E5C90F /* site-email-followers-get-success.json in Resources */, + 0C8069A72DC03E85008DFC2F /* site-subscribers-response.json in Resources */, 465F889A263B09BF00F4C950 /* wp-block-editor-v1-settings-success-ThemeJSON.json in Resources */, BA9A7F7F24C6895600925E81 /* plugin-directory-jetpack-beta.json in Resources */, E6B0461425E5B6F500DF6F4F /* sites-invites-links-generate.json in Resources */, @@ -3491,6 +3498,7 @@ 3FD634F32BC3AD6200CEDF5E /* Result+Callback.swift in Sources */, B5A4822E20AC6C1A009D95F6 /* WPKitLogging.m in Sources */, 3FE2E97C2BC3A332002CA2E1 /* WordPressComRestApi.swift in Sources */, + 0CE4E8252DC027AC00056DD9 /* RemoteSubscriber.swift in Sources */, FE6C673C2BB739950083ECAB /* NSAttributedString+extensions.swift in Sources */, 7430C9A61F1927180051B8E6 /* ReaderSiteServiceRemote.m in Sources */, FEE4EF57272FDD4B003CDA3C /* RemoteCommentV2.swift in Sources */,