Source code for the WriteFreely SwiftUI app for iOS, iPadOS, and macOS
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

289 lines
13 KiB

  1. import Foundation
  2. import WriteFreely
  3. extension WriteFreelyModel {
  4. func loginHandler(result: Result<WFUser, Error>) {
  5. DispatchQueue.main.async {
  6. self.isLoggingIn = false
  7. }
  8. do {
  9. let user = try result.get()
  10. fetchUserCollections()
  11. fetchUserPosts()
  12. do {
  13. try saveTokenToKeychain(user.token, username: user.username, server: account.server)
  14. DispatchQueue.main.async {
  15. self.account.login(user)
  16. }
  17. } catch {
  18. self.currentError = KeychainError.couldNotStoreAccessToken
  19. }
  20. } catch WFError.notFound {
  21. self.currentError = AccountError.usernameNotFound
  22. } catch WFError.unauthorized {
  23. self.currentError = AccountError.invalidPassword
  24. } catch {
  25. if (error as NSError).domain == NSURLErrorDomain,
  26. (error as NSError).code == -1003 {
  27. self.currentError = AccountError.serverNotFound
  28. } else {
  29. self.currentError = error
  30. }
  31. }
  32. }
  33. func logoutHandler(result: Result<Bool, Error>) {
  34. do {
  35. _ = try result.get()
  36. do {
  37. try purgeTokenFromKeychain(username: account.user?.username, server: account.server)
  38. client = nil
  39. DispatchQueue.main.async {
  40. self.account.logout()
  41. do {
  42. try LocalStorageManager.standard.purgeUserCollections()
  43. try self.posts.purgePublishedPosts()
  44. } catch {
  45. self.currentError = error
  46. }
  47. }
  48. } catch {
  49. self.currentError = KeychainError.couldNotPurgeAccessToken
  50. }
  51. } catch WFError.notFound {
  52. // The user token is invalid or doesn't exist, so it's been invalidated by the server. Proceed with
  53. // purging the token from the Keychain, destroying the client object, and setting the AccountModel to its
  54. // logged-out state.
  55. do {
  56. try purgeTokenFromKeychain(username: account.user?.username, server: account.server)
  57. client = nil
  58. DispatchQueue.main.async {
  59. self.account.logout()
  60. do {
  61. try LocalStorageManager.standard.purgeUserCollections()
  62. try self.posts.purgePublishedPosts()
  63. } catch {
  64. self.currentError = error
  65. }
  66. }
  67. } catch {
  68. self.currentError = KeychainError.couldNotPurgeAccessToken
  69. }
  70. } catch {
  71. // We get a 'cannot parse response' (similar to what we were seeing in the Swift package) NSURLError here,
  72. // so we're using a hacky workaround — if we get the NSURLError, but the AccountModel still thinks we're
  73. // logged in, try calling the logout function again and see what we get.
  74. // Conditional cast from 'Error' to 'NSError' always succeeds but is the only way to check error properties.
  75. if (error as NSError).domain == NSURLErrorDomain,
  76. (error as NSError).code == NSURLErrorCannotParseResponse {
  77. if account.isLoggedIn {
  78. self.logout()
  79. }
  80. }
  81. }
  82. }
  83. func fetchUserCollectionsHandler(result: Result<[WFCollection], Error>) {
  84. // We're done with the network request.
  85. DispatchQueue.main.async {
  86. self.isProcessingRequest = false
  87. }
  88. do {
  89. let fetchedCollections = try result.get()
  90. for fetchedCollection in fetchedCollections {
  91. DispatchQueue.main.async {
  92. let localCollection = WFACollection(context: LocalStorageManager.standard.container.viewContext)
  93. localCollection.alias = fetchedCollection.alias
  94. localCollection.blogDescription = fetchedCollection.description
  95. localCollection.email = fetchedCollection.email
  96. localCollection.isPublic = fetchedCollection.isPublic ?? false
  97. localCollection.styleSheet = fetchedCollection.styleSheet
  98. localCollection.title = fetchedCollection.title
  99. localCollection.url = fetchedCollection.url
  100. }
  101. }
  102. DispatchQueue.main.async {
  103. LocalStorageManager.standard.saveContext()
  104. }
  105. } catch WFError.unauthorized {
  106. self.currentError = AccountError.genericAuthError
  107. self.logout()
  108. } catch {
  109. self.currentError = AppError.genericError(error.localizedDescription)
  110. }
  111. }
  112. // swiftlint:disable function_body_length
  113. func fetchUserPostsHandler(result: Result<[WFPost], Error>) {
  114. // We're done with the network request.
  115. DispatchQueue.main.async {
  116. self.isProcessingRequest = false
  117. }
  118. let request = WFAPost.createFetchRequest()
  119. do {
  120. let locallyCachedPosts = try LocalStorageManager.standard.container.viewContext.fetch(request)
  121. do {
  122. var postsToDelete = locallyCachedPosts.filter { $0.status != PostStatus.local.rawValue }
  123. let fetchedPosts = try result.get()
  124. for fetchedPost in fetchedPosts {
  125. if let managedPost = locallyCachedPosts.first(where: { $0.postId == fetchedPost.postId }) {
  126. DispatchQueue.main.async {
  127. managedPost.wasDeletedFromServer = false
  128. if let fetchedPostUpdatedDate = fetchedPost.updatedDate,
  129. let localPostUpdatedDate = managedPost.updatedDate {
  130. managedPost.hasNewerRemoteCopy = fetchedPostUpdatedDate > localPostUpdatedDate
  131. } else {
  132. self.currentError = AppError.genericError(
  133. "Error updating post: could not determine which copy of post is newer."
  134. )
  135. }
  136. if managedPost.collectionAlias != fetchedPost.collectionAlias {
  137. // The post has been moved so we update the managed post's collectionAlias property.
  138. DispatchQueue.main.async {
  139. if self.selectedPost == managedPost {
  140. self.selectedPost = nil
  141. }
  142. managedPost.collectionAlias = fetchedPost.collectionAlias
  143. }
  144. }
  145. postsToDelete.removeAll(where: { $0.postId == fetchedPost.postId })
  146. }
  147. } else {
  148. DispatchQueue.main.async {
  149. let managedPost = WFAPost(context: LocalStorageManager.standard.container.viewContext)
  150. self.importData(from: fetchedPost, into: managedPost)
  151. managedPost.collectionAlias = fetchedPost.collectionAlias
  152. managedPost.wasDeletedFromServer = false
  153. }
  154. }
  155. }
  156. DispatchQueue.main.async {
  157. for post in postsToDelete { post.wasDeletedFromServer = true }
  158. LocalStorageManager.standard.saveContext()
  159. }
  160. } catch {
  161. self.currentError = AppError.genericError(error.localizedDescription)
  162. }
  163. } catch WFError.unauthorized {
  164. self.currentError = AccountError.genericAuthError
  165. self.logout()
  166. } catch {
  167. self.currentError = LocalStoreError.couldNotFetchPosts("cached")
  168. }
  169. }
  170. // swiftlint:enable function_body_length
  171. func publishHandler(result: Result<WFPost, Error>) {
  172. // We're done with the network request.
  173. DispatchQueue.main.async {
  174. self.isProcessingRequest = false
  175. }
  176. // ⚠️ NOTE:
  177. // The API does not return a collection alias, so we take care not to overwrite the
  178. // cached post's collection alias with the 'nil' value from the fetched post.
  179. // See: https://github.com/writeas/writefreely-swift/issues/20
  180. do {
  181. let fetchedPost = try result.get()
  182. // If this is an updated post, check it against postToUpdate.
  183. if let updatingPost = self.postToUpdate {
  184. importData(from: fetchedPost, into: updatingPost)
  185. DispatchQueue.main.async {
  186. LocalStorageManager.standard.saveContext()
  187. }
  188. } else {
  189. // Otherwise if it's a newly-published post, find it in the local store.
  190. let request = WFAPost.createFetchRequest()
  191. let matchBodyPredicate = NSPredicate(format: "body == %@", fetchedPost.body)
  192. if let fetchedPostTitle = fetchedPost.title {
  193. let matchTitlePredicate = NSPredicate(format: "title == %@", fetchedPostTitle)
  194. request.predicate = NSCompoundPredicate(
  195. andPredicateWithSubpredicates: [
  196. matchTitlePredicate,
  197. matchBodyPredicate
  198. ]
  199. )
  200. } else {
  201. request.predicate = matchBodyPredicate
  202. }
  203. do {
  204. let cachedPostsResults = try LocalStorageManager.standard.container.viewContext.fetch(request)
  205. guard let cachedPost = cachedPostsResults.first else { return }
  206. importData(from: fetchedPost, into: cachedPost)
  207. DispatchQueue.main.async {
  208. LocalStorageManager.standard.saveContext()
  209. }
  210. } catch {
  211. self.currentError = LocalStoreError.couldNotFetchPosts("cached")
  212. }
  213. }
  214. } catch {
  215. self.currentError = AppError.genericError(error.localizedDescription)
  216. }
  217. }
  218. func updateFromServerHandler(result: Result<WFPost, Error>) {
  219. // We're done with the network request.
  220. DispatchQueue.main.async {
  221. self.isProcessingRequest = false
  222. }
  223. // ⚠️ NOTE:
  224. // The API does not return a collection alias, so we take care not to overwrite the
  225. // cached post's collection alias with the 'nil' value from the fetched post.
  226. // See: https://github.com/writeas/writefreely-swift/issues/20
  227. do {
  228. let fetchedPost = try result.get()
  229. #if os(iOS)
  230. guard let cachedPost = self.selectedPost else { return }
  231. #else
  232. guard let cachedPost = self.editor.postToUpdate else { return }
  233. #endif
  234. importData(from: fetchedPost, into: cachedPost)
  235. cachedPost.hasNewerRemoteCopy = false
  236. DispatchQueue.main.async {
  237. LocalStorageManager.standard.saveContext()
  238. #if os(macOS)
  239. self.selectedPost = cachedPost
  240. #endif
  241. cachedPost.status = PostStatus.published.rawValue
  242. }
  243. } catch {
  244. self.currentError = AppError.genericError(error.localizedDescription)
  245. }
  246. }
  247. func movePostHandler(result: Result<Bool, Error>) {
  248. // We're done with the network request.
  249. DispatchQueue.main.async {
  250. self.isProcessingRequest = false
  251. }
  252. do {
  253. let succeeded = try result.get()
  254. if succeeded {
  255. if let post = selectedPost {
  256. updateFromServer(post: post)
  257. } else {
  258. return
  259. }
  260. }
  261. } catch {
  262. DispatchQueue.main.async {
  263. LocalStorageManager.standard.container.viewContext.rollback()
  264. }
  265. self.currentError = AppError.genericError(error.localizedDescription)
  266. }
  267. }
  268. private func importData(from fetchedPost: WFPost, into cachedPost: WFAPost) {
  269. cachedPost.appearance = fetchedPost.appearance
  270. cachedPost.body = fetchedPost.body
  271. cachedPost.createdDate = fetchedPost.createdDate
  272. cachedPost.language = fetchedPost.language
  273. cachedPost.postId = fetchedPost.postId
  274. cachedPost.rtl = fetchedPost.rtl ?? false
  275. cachedPost.slug = fetchedPost.slug
  276. cachedPost.status = PostStatus.published.rawValue
  277. cachedPost.title = fetchedPost.title ?? ""
  278. cachedPost.updatedDate = fetchedPost.updatedDate
  279. }
  280. }