This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended. To summarise this work: - [x] created a new **Xcode** project; - [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project; - [x] created the `Locations` app and also, its `Libraries` package; - [x] created the `Shared` package to share dependencies between the apps; - [x] added a `Makefile` file and implemented some **environment** and **help** commands. Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Reviewed-on: rock-n-code/deep-linking-assignment#1
252 lines
9.6 KiB
Swift
252 lines
9.6 KiB
Swift
import Foundation
|
|
import WMF
|
|
|
|
struct TalkPageAPIResponse: Codable {
|
|
let threads: TalkPageThreadItems?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case threads = "discussiontoolspageinfo"
|
|
}
|
|
}
|
|
|
|
struct TalkPageThreadItems: Codable {
|
|
let threadItems: [TalkPageItem]
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case threadItems = "threaditemshtml"
|
|
}
|
|
}
|
|
|
|
struct TalkPageItem: Codable {
|
|
let type: TalkPageItemType
|
|
let level: Int?
|
|
let id: String
|
|
let html: String?
|
|
let name: String?
|
|
let headingLevel: Int?
|
|
let replies: [TalkPageItem]
|
|
let otherContent: String?
|
|
let author: String?
|
|
let timestamp: Date?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case type, level, id, html, name ,headingLevel, replies, author, timestamp
|
|
case otherContent = "othercontent"
|
|
}
|
|
|
|
enum TalkPageItemType: String, Codable {
|
|
case comment = "comment"
|
|
case heading = "heading"
|
|
}
|
|
|
|
init(type: TalkPageItemType, level: Int?, id: String, html: String?, name: String?, headingLevel: Int?, replies: [TalkPageItem], otherContent: String?, author: String?, timestamp: Date?) {
|
|
self.type = type
|
|
self.level = level
|
|
self.id = id
|
|
self.html = html
|
|
self.name = name
|
|
self.headingLevel = headingLevel
|
|
self.replies = replies
|
|
self.otherContent = otherContent
|
|
self.author = author
|
|
self.timestamp = timestamp
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
type = try values.decode(TalkPageItemType.self, forKey: .type)
|
|
level = try? values.decode(Int.self, forKey: .level)
|
|
id = try values.decode(String.self, forKey: .id)
|
|
html = try? values.decode(String.self, forKey: .html)
|
|
name = try? values.decode(String.self, forKey: .name)
|
|
headingLevel = try? values.decode(Int.self, forKey: .headingLevel)
|
|
replies = (try? values.decode([TalkPageItem].self, forKey: .replies)) ?? []
|
|
otherContent = try? values.decode(String.self, forKey: .otherContent)
|
|
author = try? values.decode(String.self, forKey: .author)
|
|
|
|
if let timestampString = try? values.decode(String.self, forKey: .timestamp) {
|
|
let timestampDate = (timestampString as NSString).wmf_iso8601Date()
|
|
timestamp = timestampDate
|
|
} else {
|
|
timestamp = nil
|
|
}
|
|
}
|
|
|
|
func updatingReplies(replies: [TalkPageItem]) -> TalkPageItem {
|
|
return TalkPageItem(type: type, level: level, id: id, html: html, name: name, headingLevel: headingLevel, replies: replies, otherContent: otherContent, author: author, timestamp: timestamp)
|
|
}
|
|
}
|
|
|
|
class TalkPageFetcher: Fetcher {
|
|
|
|
func fetchTalkPageContent(talkPageTitle: String, siteURL: URL, completion: @escaping (Result<[TalkPageItem], Error>) -> Void) {
|
|
guard let title = talkPageTitle.denormalizedPageTitle else {
|
|
completion(.failure(RequestError.invalidParameters))
|
|
return
|
|
}
|
|
|
|
let params = ["action" : "discussiontoolspageinfo",
|
|
"page" : title,
|
|
"format": "json",
|
|
"prop" : "threaditemshtml",
|
|
"formatversion" : "2"
|
|
]
|
|
|
|
performDecodableMediaWikiAPIGET(for: siteURL, with: params) { (result: Result<TalkPageAPIResponse, Error>) in
|
|
switch result {
|
|
case let .success(talk):
|
|
completion(.success(talk.threads?.threadItems ?? []))
|
|
case let .failure(error):
|
|
completion(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This function takes a **topic** argument of type String.
|
|
/// This argument expects a `name` value from `TalkPageItem` heading (or topic) items.
|
|
func subscribeToTopic(talkPageTitle: String, siteURL: URL, topic: String, shouldSubscribe: Bool, completion: @escaping (Result<Bool, Error>) -> Void) {
|
|
|
|
guard let title = talkPageTitle.denormalizedPageTitle else {
|
|
completion(.failure(RequestError.invalidParameters))
|
|
return
|
|
}
|
|
|
|
var params = ["action": "discussiontoolssubscribe",
|
|
"page": title,
|
|
"format": "json",
|
|
"commentname": topic,
|
|
"formatversion": "2"
|
|
]
|
|
|
|
if shouldSubscribe {
|
|
params["subscribe"] = "1"
|
|
}
|
|
|
|
performTokenizedMediaWikiAPIPOST(to: siteURL, with: params, reattemptLoginOn401Response: true) { result, httpResponse, error in
|
|
if let error = error {
|
|
completion(.failure(error))
|
|
return
|
|
}
|
|
|
|
if let resultError = result?["error"] as? [String: Any],
|
|
let info = resultError["info"] as? String {
|
|
completion(.failure(RequestError.api(info)))
|
|
return
|
|
}
|
|
|
|
if let resultSuccess = result?["discussiontoolssubscribe"] as? [String: Any],
|
|
let didSubscribe = resultSuccess["subscribe"] as? Bool {
|
|
completion(.success(didSubscribe))
|
|
return
|
|
}
|
|
completion(.failure(RequestError.unexpectedResponse))
|
|
}
|
|
}
|
|
|
|
/// Returns a list of active talk page topics subscription
|
|
/// - Parameters:
|
|
/// - siteURL: URL for the talk page, takes a URL object
|
|
/// - topics: Expects a array of Strings containing the `name` value from `TalkPageItem`
|
|
/// - completion: Returns either and array with the the `name` property of subscribed topics or an Error
|
|
func getSubscribedTopics(siteURL: URL, topics: [String], completion: @escaping (Result<[String], Error>) -> Void) {
|
|
|
|
let joinedString = topics.joined(separator: "|")
|
|
|
|
let params = ["action": "discussiontoolsgetsubscriptions",
|
|
"format": "json",
|
|
"commentname": joinedString,
|
|
"formatversion": "2"
|
|
]
|
|
|
|
performMediaWikiAPIGET(for: siteURL, with: params, cancellationKey: nil) { result, httpResponse, error in
|
|
if let error = error {
|
|
completion(.failure(error))
|
|
return
|
|
}
|
|
|
|
if let resultError = result?["error"] as? [String: Any],
|
|
let info = resultError["info"] as? String {
|
|
completion(.failure(RequestError.api(info)))
|
|
return
|
|
}
|
|
|
|
if let resultSuccess = result?["subscriptions"] as? [String: Any] {
|
|
var subscribedTopics = [String]()
|
|
for (topicId, subStatus) in resultSuccess {
|
|
if subStatus as? Int == 1 {
|
|
subscribedTopics.append(topicId)
|
|
}
|
|
}
|
|
completion(.success(subscribedTopics))
|
|
return
|
|
}
|
|
completion(.failure(RequestError.unexpectedResponse))
|
|
}
|
|
}
|
|
|
|
func postReply(talkPageTitle: String, siteURL: URL, commentId: String, comment: String, completion: @escaping(Result<Void, Error>) -> Void) {
|
|
guard let title = talkPageTitle.denormalizedPageTitle else {
|
|
completion(.failure(RequestError.invalidParameters))
|
|
return
|
|
}
|
|
|
|
let params = ["action": "discussiontoolsedit",
|
|
"paction": "addcomment",
|
|
"page": title,
|
|
"format": "json",
|
|
"formatversion" : "2",
|
|
"commentid": commentId,
|
|
"wikitext": comment
|
|
|
|
]
|
|
|
|
performTokenizedMediaWikiAPIPOST(to: siteURL, with: params, reattemptLoginOn401Response: false) { result, httpResponse, error in
|
|
self.evaluateResponse(error, result, completion: completion)
|
|
}
|
|
}
|
|
|
|
func postTopic(talkPageTitle: String, siteURL: URL, topicTitle: String, topicBody: String, completion: @escaping(Result<Void, Error>) -> Void) {
|
|
|
|
guard let title = talkPageTitle.denormalizedPageTitle else {
|
|
completion(.failure(RequestError.invalidParameters))
|
|
return
|
|
}
|
|
|
|
let params = ["action": "discussiontoolsedit",
|
|
"paction": "addtopic",
|
|
"page": title,
|
|
"format": "json",
|
|
"formatversion" : "2",
|
|
"sectiontitle": topicTitle,
|
|
"wikitext": topicBody ]
|
|
|
|
performTokenizedMediaWikiAPIPOST(to: siteURL, with: params, reattemptLoginOn401Response: false) { result, httpResponse, error in
|
|
self.evaluateResponse(error, result, completion: completion)
|
|
}
|
|
}
|
|
|
|
fileprivate func evaluateResponse(_ error: Error?, _ result: [String : Any]?, completion: @escaping(Result<Void, Error>) -> Void) {
|
|
if let error = error {
|
|
completion(.failure(error))
|
|
return
|
|
}
|
|
|
|
if let resultError = result?["error"] as? [String: Any],
|
|
let info = resultError["info"] as? String {
|
|
completion(.failure(RequestError.api(info)))
|
|
return
|
|
}
|
|
|
|
guard let discussionToolsEdit = result?["discussiontoolsedit"] as? [String: Any],
|
|
let discussionToolsEditResult = discussionToolsEdit["result"] as? String,
|
|
discussionToolsEditResult == "success" else {
|
|
completion(.failure(RequestError.unexpectedResponse))
|
|
return
|
|
}
|
|
|
|
completion(.success(()))
|
|
}
|
|
|
|
}
|