Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.
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
24 changes: 24 additions & 0 deletions WordPressKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
240315B0A1D6C2B981572B5B /* Pods_WordPressKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED05C8FF3E61D93CE5BA527E /* Pods_WordPressKitTests.framework */; };
40247DFA2120D8E100AE1C3C /* AutomatedTransferService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40247DF92120D8E100AE1C3C /* AutomatedTransferService.swift */; };
40247DFC2120E69600AE1C3C /* AutomatedTransferStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40247DFB2120E69600AE1C3C /* AutomatedTransferStatus.swift */; };
404057C5221B30400060250C /* SearchTermStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404057C4221B30400060250C /* SearchTermStatsType.swift */; };
404057C7221B36070060250C /* stats-search-term-result.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057C6221B36070060250C /* stats-search-term-result.json */; };
404057C9221B789B0060250C /* AuthorsStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404057C8221B789B0060250C /* AuthorsStatsType.swift */; };
404057CB221B80BC0060250C /* stats-top-authors.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057CA221B80BC0060250C /* stats-top-authors.json */; };
4041405E220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */; };
40414060220F9F1F00CF7C5B /* StatsAllTimesInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */; };
40A71C6E220E1D8E002E3D25 /* StatsServiceRemoteV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */; };
Expand Down Expand Up @@ -490,6 +494,10 @@
264F5C834541BBF2018F4964 /* Pods-WordPressKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressKitTests/Pods-WordPressKitTests.debug.xcconfig"; sourceTree = "<group>"; };
40247DF92120D8E100AE1C3C /* AutomatedTransferService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomatedTransferService.swift; sourceTree = "<group>"; };
40247DFB2120E69600AE1C3C /* AutomatedTransferStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomatedTransferStatus.swift; sourceTree = "<group>"; };
404057C4221B30400060250C /* SearchTermStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTermStatsType.swift; sourceTree = "<group>"; };
404057C6221B36070060250C /* stats-search-term-result.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-search-term-result.json"; sourceTree = "<group>"; };
404057C8221B789B0060250C /* AuthorsStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorsStatsType.swift; sourceTree = "<group>"; };
404057CA221B80BC0060250C /* stats-top-authors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-top-authors.json"; sourceTree = "<group>"; };
4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsDotComFollowersInsight.swift; sourceTree = "<group>"; };
4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsAllTimesInsight.swift; sourceTree = "<group>"; };
40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsServiceRemoteV2.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -987,6 +995,15 @@
name = Frameworks;
sourceTree = "<group>";
};
404057C3221B30140060250C /* Time-based data */ = {
isa = PBXGroup;
children = (
404057C4221B30400060250C /* SearchTermStatsType.swift */,
404057C8221B789B0060250C /* AuthorsStatsType.swift */,
);
path = "Time-based data";
sourceTree = "<group>";
};
40414061220F9F2800CF7C5B /* Insights */ = {
isa = PBXGroup;
children = (
Expand All @@ -1006,6 +1023,7 @@
40B01BF3220E534900036D10 /* V2 */ = {
isa = PBXGroup;
children = (
404057C3221B30140060250C /* Time-based data */,
40414061220F9F2800CF7C5B /* Insights */,
);
name = V2;
Expand Down Expand Up @@ -1499,7 +1517,9 @@
93BD27421EE73384002BB00B /* Mock Data */ = {
isa = PBXGroup;
children = (
404057CA221B80BC0060250C /* stats-top-authors.json */,
40F9880D221ACFB400B7B369 /* stats-streak-result.json */,
404057C6221B36070060250C /* stats-search-term-result.json */,
826016F61F9FAF6300533B6C /* activity-log-auth-failure.json */,
826016F81F9FAF6300533B6C /* activity-log-bad-json-failure.json */,
826016F51F9FAF6300533B6C /* activity-log-success-1.json */,
Expand Down Expand Up @@ -1965,6 +1985,7 @@
93F50A471F227F3600B5BEBA /* xmlrpc-response-getprofile.xml in Resources */,
7403A2F51EF06FEB00DED7DC /* me-settings-bad-json-failure.json in Resources */,
9AB6D64E218731AB0008F274 /* post-revisions-mapping-success.json in Resources */,
404057C7221B36070060250C /* stats-search-term-result.json in Resources */,
7403A2F41EF06FEB00DED7DC /* me-settings-auth-failure.json in Resources */,
930999581F16598A00F006A1 /* get-single-theme-v1.1.json in Resources */,
74B335E61F06F6E90053A184 /* WordPressComRestApiMedia.json in Resources */,
Expand Down Expand Up @@ -2042,6 +2063,7 @@
74D67F151F15C2D70010C5ED /* site-roles-success.json in Resources */,
D8DB404221EF22B500B8238E /* site-segments-multiple.json in Resources */,
740B23E11F17FB4200067A2A /* xmlrpc-metaweblog-editpost-bad-xml-failure.xml in Resources */,
404057CB221B80BC0060250C /* stats-top-authors.json in Resources */,
93AC8ED51ED32FD000900F5A /* stats-v1.1-tags-categories-views-day.json in Resources */,
17BF9A7520C7E18200BF57D2 /* reader-site-search-success-no-icon.json in Resources */,
828A2402201B6B8A004F6859 /* activity-rewind-status-restore-in-progress.json in Resources */,
Expand Down Expand Up @@ -2281,6 +2303,7 @@
742362E31F1025B400BD0A7F /* RemoteMenuLocation.m in Sources */,
82FFBF561F460DD400F4573F /* BlogJetpackSettingsServiceRemote.swift in Sources */,
9368C7B81EC630270092CE8E /* StatsStreakItem.m in Sources */,
404057C9221B789B0060250C /* AuthorsStatsType.swift in Sources */,
74BA04F61F06DC0A00ED5CD8 /* CommentServiceRemoteXMLRPC.m in Sources */,
7430C9A81F1927180051B8E6 /* ReaderTopicServiceRemote.m in Sources */,
17CE77F120C6EB41001DEA5A /* ReaderFeed.swift in Sources */,
Expand Down Expand Up @@ -2329,6 +2352,7 @@
40247DFC2120E69600AE1C3C /* AutomatedTransferStatus.swift in Sources */,
730E869F21E44EFD00753E1A /* WordPressComServiceRemote+SiteVerticals.swift in Sources */,
740B23C51F17EE8000067A2A /* RemotePost.m in Sources */,
404057C5221B30400060250C /* SearchTermStatsType.swift in Sources */,
40247DFA2120D8E100AE1C3C /* AutomatedTransferService.swift in Sources */,
74E2295B1F1E77290085F7F2 /* KeyringConnection.swift in Sources */,
74B5F0DA1EF8299B00B411E7 /* BlogServiceRemoteXMLRPC.m in Sources */,
Expand Down
92 changes: 91 additions & 1 deletion WordPressKit/StatsServiceRemoteV2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,22 @@ public class StatsServiceRemoteV2: ServiceRemoteWordPressComREST {
private let siteID: Int
private let siteTimezone: TimeZone

private lazy var periodDataQueryDateFormatter: DateFormatter = {
let df = DateFormatter()
df.locale = Locale(identifier: "en_US_POSIX")
df.dateFormat = "yyyy-MM-dd"
return df
}()

public init(wordPressComRestApi api: WordPressComRestApi, siteID: Int, siteTimezone: TimeZone) {
self.siteID = siteID
self.siteTimezone = siteTimezone
super.init(wordPressComRestApi: api)
}


/// Responsible for fetching Stats data for Insights — latest data about a site,
/// in general — not considering a specific slice of time.
/// For a possible set of returned types, see objects that conform to `InsightProtocol`.
public func getInsight<InsightType: InsightProtocol>(completion: @escaping ((InsightType?, Error?) -> Void)) {
let properties = InsightType.queryProperties as [String: AnyObject]
let pathComponent = InsightType.pathComponent
Expand All @@ -41,6 +50,44 @@ public class StatsServiceRemoteV2: ServiceRemoteWordPressComREST {
})
}


/// Used to fetch data about site over a specific timeframe.
/// - parameters:
/// - period: An enum representing whether either a day, a week, a month or a year worth's of data.
/// - endingOn: Date on which the `period` for which data you're interested in **is ending**.
/// e.g. if you want data spanning 11-17 Feb 2019, you should pass in a period of `.week` and an
/// ending date of `Feb 17 2019`.
/// - limit: Limit of how many objects you want returned for your query. Default is `10`. `0` means no limit.
public func getData<TimeStatsType: TimeStatsProtocol>(for period: StatsPeriodUnit,
endingOn: Date,
limit: Int = 10,
completion: @escaping ((TimeStatsType?, Error?) -> Void)) {
let pathComponent = TimeStatsType.pathComponent
let path = self.path(forEndpoint: "sites/\(siteID)/\(pathComponent)/", withVersion: ._1_1)

let properties = ["period": period.stringValue,
"date": periodDataQueryDateFormatter.string(from: endingOn),
"max": limit as AnyObject] as [String: AnyObject]

wordPressComRestApi.GET(path, parameters: properties, success: { (response, _) in
guard
let jsonResponse = response as? [String: AnyObject],
let dateString = response["date"] as? String,
let date = self.periodDataQueryDateFormatter.date(from: dateString),
let periodString = response["period"] as? String,
let parsedPeriod = StatsPeriodUnit(string: periodString),
let timestats = TimeStatsType(date: date, period: parsedPeriod, jsonDictionary: jsonResponse)
else {
completion(nil, ResponseError.decodingFailure)
return
}

completion(timestats, nil)
}, failure: { (error, _) in
completion(nil, error)
})
}

// "Last Post" Insights are "fun" in the way that they require multiple requests to actually create them,
// so we do this "fun" dance in a separate method.
public func getInsight(completion: @escaping ((StatsLastPostInsight?, Error?) -> Void)) {
Expand Down Expand Up @@ -111,6 +158,48 @@ public protocol InsightProtocol {
init?(jsonDictionary: [String: AnyObject])
}

// naming is hard.
public protocol TimeStatsProtocol {
static var pathComponent: String { get }

var period: StatsPeriodUnit { get }
var periodEndDate: Date { get }

init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String: AnyObject])
}

// We'll bring `StatsPeriodUnit` into this file when the "old" `WPStatsServiceRemote` gets removed.
// For now we can piggy-back off the old type and add this as an extension.
fileprivate extension StatsPeriodUnit {
var stringValue: String {
switch self {
case .day:
return "day"
case .week:
return "week"
case .month:
return "month"
case .year:
return "year"
}
}

init?(string: String) {
switch string {
case "day":
self = .day
case "week":
self = .week
case "month":
self = .month
case "year":
self = .year
default:
return nil
}
}
}

extension InsightProtocol {

// A big chunk of those use the same endpoint and queryProperties.. Let's simplify the protocol conformance in those cases.
Expand All @@ -124,6 +213,7 @@ extension InsightProtocol {
}
}


// Swift compiler doesn't like if this is not declared _in this file_, and refuses to compile the project.
// I'm guessing this has somethign to do with generic specialisation, but I'm not enough
// of a `swiftc` guru to really know. Leaving this in here to appease Swift gods.
Expand Down
84 changes: 84 additions & 0 deletions WordPressKit/Time-based data/AuthorsStatsType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
struct AuthorsStatsType {
let period: StatsPeriodUnit
let periodEndDate: Date

let topAuthors: [StatsTopAuthor]
}

struct StatsTopAuthor {
let name: String
let iconURL: URL?
let viewsCount: Int
let posts: [StatsTopPost]

init?(jsonDictionary: [String: AnyObject]) {
guard
let name = jsonDictionary["name"] as? String,
let views = jsonDictionary["views"] as? Int,
let avatar = jsonDictionary["avatar"] as? String,
let posts = jsonDictionary["posts"] as? [[String: AnyObject]]
else {
return nil
}

let url: URL?
if var components = URLComponents(string: avatar) {
components.query = "d=mm&s=60"
url = try? components.asURL()
} else {
url = nil
}

let mappedPosts = posts.compactMap { StatsTopPost(jsonDictionary: $0) }

self.name = name
self.viewsCount = views
self.iconURL = url
self.posts = mappedPosts

}
}

struct StatsTopPost {
let title: String
let postID: Int
let postURL: URL?
let viewsCount: Int

init?(jsonDictionary: [String: AnyObject]) {
guard
let title = jsonDictionary["title"] as? String,
let postID = jsonDictionary["id"] as? Int,
let viewsCount = jsonDictionary["views"] as? Int,
let postURL = jsonDictionary["url"] as? String
else {
return nil
}

self.title = title
self.postID = postID
self.viewsCount = viewsCount
self.postURL = URL(string: postURL)
}
}

extension AuthorsStatsType: TimeStatsProtocol {
static var pathComponent: String {
return "stats/top-authors/"
}

init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) {
guard
let days = jsonDictionary["days"] as? [String: AnyObject],
let firstKey = days.keys.first,
let firstDay = days[firstKey] as? [String: AnyObject],
let authors = firstDay["authors"] as? [[String: AnyObject]]
else {
return nil
}

self.period = period
self.periodEndDate = date
self.topAuthors = authors.compactMap { StatsTopAuthor(jsonDictionary: $0) }
}
}
52 changes: 52 additions & 0 deletions WordPressKit/Time-based data/SearchTermStatsType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
struct SearchTermStatsType {
let period: StatsPeriodUnit
let periodEndDate: Date

let totalSearchTermsCount: Int
let hiddenSearchTermsCount: Int
let otherSearchTermsCount: Int
let searchTerms: [SearchTerm]
}


extension SearchTermStatsType: TimeStatsProtocol {
static var pathComponent: String {
return "stats/search-terms"
}

init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) {
guard
let days = jsonDictionary["days"] as? [String: AnyObject],
let firstKey = days.keys.first,
let firstDay = days[firstKey] as? [String: AnyObject],
let totalSearchTerms = firstDay["total_search_terms"] as? Int,
let hiddenSearchTerms = firstDay["encrypted_search_terms"] as? Int,
let otherSearchTerms = firstDay["other_search_terms"] as? Int,
let searchTermsDict = firstDay["search_terms"] as? [[String: AnyObject]]
else {
return nil
}

let searchTerms: [SearchTerm] = searchTermsDict.compactMap {
guard let term = $0["term"] as? String, let views = $0["views"] as? Int else {
return nil
}

return SearchTerm(term: term, viewsCount: views)
}


self.periodEndDate = date
self.period = period
self.totalSearchTermsCount = totalSearchTerms
self.hiddenSearchTermsCount = hiddenSearchTerms
self.otherSearchTermsCount = otherSearchTerms
self.searchTerms = searchTerms
}

}

public struct SearchTerm {
let term: String
let viewsCount: Int
}
Loading