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.
 
 
 

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