Source code for the WriteFreely SwiftUI app for iOS, iPadOS, and macOS
Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 

270 linhas
12 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. func fetchUserPostsHandler(result: Result<[WFPost], Error>) {
  113. // We're done with the network request.
  114. DispatchQueue.main.async {
  115. self.isProcessingRequest = false
  116. }
  117. let request = WFAPost.createFetchRequest()
  118. do {
  119. let locallyCachedPosts = try LocalStorageManager.standard.container.viewContext.fetch(request)
  120. do {
  121. var postsToDelete = locallyCachedPosts.filter { $0.status != PostStatus.local.rawValue }
  122. let fetchedPosts = try result.get()
  123. for fetchedPost in fetchedPosts {
  124. if let managedPost = locallyCachedPosts.first(where: { $0.postId == fetchedPost.postId }) {
  125. DispatchQueue.main.async {
  126. managedPost.wasDeletedFromServer = false
  127. if let fetchedPostUpdatedDate = fetchedPost.updatedDate,
  128. let localPostUpdatedDate = managedPost.updatedDate {
  129. managedPost.hasNewerRemoteCopy = fetchedPostUpdatedDate > localPostUpdatedDate
  130. } else {
  131. self.currentError = AppError.genericError(
  132. "Error updating post: could not determine which copy of post is newer."
  133. )
  134. }
  135. postsToDelete.removeAll(where: { $0.postId == fetchedPost.postId })
  136. }
  137. } else {
  138. DispatchQueue.main.async {
  139. let managedPost = WFAPost(context: LocalStorageManager.standard.container.viewContext)
  140. self.importData(from: fetchedPost, into: managedPost)
  141. managedPost.collectionAlias = fetchedPost.collectionAlias
  142. managedPost.wasDeletedFromServer = false
  143. }
  144. }
  145. }
  146. DispatchQueue.main.async {
  147. for post in postsToDelete { post.wasDeletedFromServer = true }
  148. LocalStorageManager.standard.saveContext()
  149. }
  150. } catch {
  151. self.currentError = AppError.genericError(error.localizedDescription)
  152. }
  153. } catch WFError.unauthorized {
  154. self.currentError = AccountError.genericAuthError
  155. self.logout()
  156. } catch {
  157. self.currentError = LocalStoreError.couldNotFetchPosts("cached")
  158. }
  159. }
  160. func publishHandler(result: Result<WFPost, Error>) {
  161. // We're done with the network request.
  162. DispatchQueue.main.async {
  163. self.isProcessingRequest = false
  164. }
  165. // ⚠️ NOTE:
  166. // The API does not return a collection alias, so we take care not to overwrite the
  167. // cached post's collection alias with the 'nil' value from the fetched post.
  168. // See: https://github.com/writeas/writefreely-swift/issues/20
  169. do {
  170. let fetchedPost = try result.get()
  171. // If this is an updated post, check it against postToUpdate.
  172. if let updatingPost = self.postToUpdate {
  173. importData(from: fetchedPost, into: updatingPost)
  174. DispatchQueue.main.async {
  175. LocalStorageManager.standard.saveContext()
  176. }
  177. } else {
  178. // Otherwise if it's a newly-published post, find it in the local store.
  179. let request = WFAPost.createFetchRequest()
  180. let matchBodyPredicate = NSPredicate(format: "body == %@", fetchedPost.body)
  181. if let fetchedPostTitle = fetchedPost.title {
  182. let matchTitlePredicate = NSPredicate(format: "title == %@", fetchedPostTitle)
  183. request.predicate = NSCompoundPredicate(
  184. andPredicateWithSubpredicates: [
  185. matchTitlePredicate,
  186. matchBodyPredicate
  187. ]
  188. )
  189. } else {
  190. request.predicate = matchBodyPredicate
  191. }
  192. do {
  193. let cachedPostsResults = try LocalStorageManager.standard.container.viewContext.fetch(request)
  194. guard let cachedPost = cachedPostsResults.first else { return }
  195. importData(from: fetchedPost, into: cachedPost)
  196. DispatchQueue.main.async {
  197. LocalStorageManager.standard.saveContext()
  198. }
  199. } catch {
  200. self.currentError = LocalStoreError.couldNotFetchPosts("cached")
  201. }
  202. }
  203. } catch {
  204. self.currentError = AppError.genericError(error.localizedDescription)
  205. }
  206. }
  207. func updateFromServerHandler(result: Result<WFPost, Error>) {
  208. // We're done with the network request.
  209. DispatchQueue.main.async {
  210. self.isProcessingRequest = false
  211. }
  212. // ⚠️ NOTE:
  213. // The API does not return a collection alias, so we take care not to overwrite the
  214. // cached post's collection alias with the 'nil' value from the fetched post.
  215. // See: https://github.com/writeas/writefreely-swift/issues/20
  216. do {
  217. let fetchedPost = try result.get()
  218. guard let cachedPost = self.selectedPost else { return }
  219. importData(from: fetchedPost, into: cachedPost)
  220. cachedPost.hasNewerRemoteCopy = false
  221. DispatchQueue.main.async {
  222. LocalStorageManager.standard.saveContext()
  223. }
  224. } catch {
  225. self.currentError = AppError.genericError(error.localizedDescription)
  226. }
  227. }
  228. func movePostHandler(result: Result<Bool, Error>) {
  229. // We're done with the network request.
  230. DispatchQueue.main.async {
  231. self.isProcessingRequest = false
  232. }
  233. do {
  234. let succeeded = try result.get()
  235. if succeeded {
  236. if let post = selectedPost {
  237. updateFromServer(post: post)
  238. } else {
  239. return
  240. }
  241. }
  242. } catch {
  243. DispatchQueue.main.async {
  244. LocalStorageManager.standard.container.viewContext.rollback()
  245. }
  246. self.currentError = AppError.genericError(error.localizedDescription)
  247. }
  248. }
  249. private func importData(from fetchedPost: WFPost, into cachedPost: WFAPost) {
  250. cachedPost.appearance = fetchedPost.appearance
  251. cachedPost.body = fetchedPost.body
  252. cachedPost.createdDate = fetchedPost.createdDate
  253. cachedPost.language = fetchedPost.language
  254. cachedPost.postId = fetchedPost.postId
  255. cachedPost.rtl = fetchedPost.rtl ?? false
  256. cachedPost.slug = fetchedPost.slug
  257. cachedPost.status = PostStatus.published.rawValue
  258. cachedPost.title = fetchedPost.title ?? ""
  259. cachedPost.updatedDate = fetchedPost.updatedDate
  260. }
  261. }