Page Menu
Home
Musing Studio
Search
Configure Global Search
Log In
Files
F14177116
WriteFreelyModel.swift
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
11 KB
Subscribers
None
WriteFreelyModel.swift
View Options
import
Foundation
import
WriteFreely
import
Security
//
MARK:
- WriteFreelyModel
class
WriteFreelyModel
:
ObservableObject
{
@
Published
var
account
=
AccountModel
()
@
Published
var
preferences
=
PreferencesModel
()
@
Published
var
store
=
PostStore
()
@
Published
var
collections
=
CollectionListModel
()
@
Published
var
isLoggingIn
:
Bool
=
false
@
Published
var
selectedPost
:
Post
?
private
var
client
:
WFClient
?
private
let
defaults
=
UserDefaults
.
standard
init
()
{
// Set the color scheme based on what's been saved in UserDefaults.
DispatchQueue
.
main
.
async
{
self
.
preferences
.
appearance
=
self
.
defaults
.
integer
(
forKey
:
self
.
preferences
.
colorSchemeIntegerKey
)
}
#if
DEBUG
// for post in testPostData { store.add(post) }
#endif
DispatchQueue
.
main
.
async
{
self
.
account
.
restoreState
()
if
self
.
account
.
isLoggedIn
{
guard
let
serverURL
=
URL
(
string
:
self
.
account
.
server
)
else
{
print
(
"Server URL not found"
)
return
}
guard
let
token
=
self
.
fetchTokenFromKeychain
(
username
:
self
.
account
.
username
,
server
:
self
.
account
.
server
)
else
{
print
(
"Could not fetch token from Keychain"
)
return
}
self
.
account
.
login
(
WFUser
(
token
:
token
,
username
:
self
.
account
.
username
))
self
.
client
=
WFClient
(
for
:
serverURL
)
self
.
client
?.
user
=
self
.
account
.
user
if
self
.
collections
.
userCollections
.
count
==
0
{
self
.
fetchUserCollections
()
}
self
.
fetchUserPosts
()
}
}
}
}
//
MARK:
- WriteFreelyModel API
extension
WriteFreelyModel
{
func
login
(
to
server
:
URL
,
as
username
:
String
,
password
:
String
)
{
isLoggingIn
=
true
account
.
server
=
server
.
absoluteString
client
=
WFClient
(
for
:
server
)
client
?.
login
(
username
:
username
,
password
:
password
,
completion
:
loginHandler
)
}
func
logout
()
{
guard
let
loggedInClient
=
client
else
{
do
{
try
purgeTokenFromKeychain
(
username
:
account
.
username
,
server
:
account
.
server
)
account
.
logout
()
}
catch
{
fatalError
(
"Failed to log out persisted state"
)
}
return
}
loggedInClient
.
logout
(
completion
:
logoutHandler
)
}
func
fetchUserCollections
()
{
guard
let
loggedInClient
=
client
else
{
return
}
loggedInClient
.
getUserCollections
(
completion
:
fetchUserCollectionsHandler
)
}
func
fetchUserPosts
()
{
guard
let
loggedInClient
=
client
else
{
return
}
loggedInClient
.
getPosts
(
completion
:
fetchUserPostsHandler
)
}
func
publish
(
post
:
Post
)
{
guard
let
loggedInClient
=
client
else
{
return
}
if
let
existingPostId
=
post
.
wfPost
.
postId
{
// This is an existing post.
loggedInClient
.
updatePost
(
postId
:
existingPostId
,
updatedPost
:
post
.
wfPost
,
completion
:
publishHandler
)
}
else
{
// This is a new local draft.
loggedInClient
.
createPost
(
post
:
post
.
wfPost
,
in
:
post
.
collection
?.
alias
,
completion
:
publishHandler
)
}
}
func
updateFromServer
(
post
:
Post
)
{
guard
let
loggedInClient
=
client
else
{
return
}
guard
let
postId
=
post
.
wfPost
.
postId
else
{
return
}
DispatchQueue
.
main
.
async
{
self
.
selectedPost
=
post
}
loggedInClient
.
getPost
(
byId
:
postId
,
completion
:
updateFromServerHandler
)
}
}
private
extension
WriteFreelyModel
{
func
loginHandler
(
result
:
Result
<
WFUser
,
Error
>)
{
DispatchQueue
.
main
.
async
{
self
.
isLoggingIn
=
false
}
do
{
let
user
=
try
result
.
get
()
fetchUserCollections
()
fetchUserPosts
()
saveTokenToKeychain
(
user
.
token
,
username
:
user
.
username
,
server
:
account
.
server
)
DispatchQueue
.
main
.
async
{
self
.
account
.
login
(
user
)
}
}
catch
WFError
.
notFound
{
DispatchQueue
.
main
.
async
{
self
.
account
.
currentError
=
AccountError
.
usernameNotFound
}
}
catch
WFError
.
unauthorized
{
DispatchQueue
.
main
.
async
{
self
.
account
.
currentError
=
AccountError
.
invalidPassword
}
}
catch
{
if
(
error
as
NSError
).
domain
==
NSURLErrorDomain
,
(
error
as
NSError
).
code
==
-
1003
{
DispatchQueue
.
main
.
async
{
self
.
account
.
currentError
=
AccountError
.
serverNotFound
}
}
}
}
func
logoutHandler
(
result
:
Result
<
Bool
,
Error
>)
{
do
{
_
=
try
result
.
get
()
do
{
try
purgeTokenFromKeychain
(
username
:
account
.
user
?.
username
,
server
:
account
.
server
)
client
=
nil
DispatchQueue
.
main
.
async
{
self
.
account
.
logout
()
self
.
collections
.
clearUserCollection
()
self
.
store
.
purgeAllPosts
()
}
}
catch
{
print
(
"Something went wrong purging the token from the Keychain."
)
}
}
catch
WFError
.
notFound
{
// The user token is invalid or doesn't exist, so it's been invalidated by the server. Proceed with
// purging the token from the Keychain, destroying the client object, and setting the AccountModel to its
// logged-out state.
do
{
try
purgeTokenFromKeychain
(
username
:
account
.
user
?.
username
,
server
:
account
.
server
)
client
=
nil
DispatchQueue
.
main
.
async
{
self
.
collections
.
clearUserCollection
()
self
.
account
.
logout
()
self
.
store
.
purgeAllPosts
()
}
}
catch
{
print
(
"Something went wrong purging the token from the Keychain."
)
}
}
catch
{
// We get a 'cannot parse response' (similar to what we were seeing in the Swift package) NSURLError here,
// so we're using a hacky workaround — if we get the NSURLError, but the AccountModel still thinks we're
// logged in, try calling the logout function again and see what we get.
// Conditional cast from 'Error' to 'NSError' always succeeds but is the only way to check error properties.
if
(
error
as
NSError
).
domain
==
NSURLErrorDomain
,
(
error
as
NSError
).
code
==
NSURLErrorCannotParseResponse
{
if
account
.
isLoggedIn
{
self
.
logout
()
}
}
}
}
func
fetchUserCollectionsHandler
(
result
:
Result
<
[
WFCollection
],
Error
>)
{
DispatchQueue
.
main
.
async
{
self
.
collections
.
loadCachedUserCollections
()
}
do
{
let
fetchedCollections
=
try
result
.
get
()
for
fetchedCollection
in
fetchedCollections
{
DispatchQueue
.
main
.
async
{
let
localCollection
=
WFACollection
(
context
:
PersistenceManager
.
persistentContainer
.
viewContext
)
localCollection
.
alias
=
fetchedCollection
.
alias
localCollection
.
blogDescription
=
fetchedCollection
.
description
localCollection
.
email
=
fetchedCollection
.
email
localCollection
.
isPublic
=
fetchedCollection
.
isPublic
??
false
localCollection
.
styleSheet
=
fetchedCollection
.
styleSheet
localCollection
.
title
=
fetchedCollection
.
title
localCollection
.
url
=
fetchedCollection
.
url
}
}
DispatchQueue
.
main
.
async
{
PersistenceManager
().
saveContext
()
}
}
catch
{
print
(
error
)
}
}
func
fetchUserPostsHandler
(
result
:
Result
<
[
WFPost
],
Error
>)
{
do
{
let
fetchedPosts
=
try
result
.
get
()
var
fetchedPostsArray
:
[
Post
]
=
[]
for
fetchedPost
in
fetchedPosts
{
var
post
:
Post
if
let
matchingAlias
=
fetchedPost
.
collectionAlias
{
let
matchingCachedCollection
=
(
collections
.
userCollections
.
filter
{
$0
.
alias
==
matchingAlias
}
).
first
post
=
Post
(
wfPost
:
fetchedPost
,
in
:
matchingCachedCollection
)
}
else
{
post
=
Post
(
wfPost
:
fetchedPost
)
}
fetchedPostsArray
.
append
(
post
)
}
DispatchQueue
.
main
.
async
{
self
.
store
.
updateStore
(
with
:
fetchedPostsArray
)
}
}
catch
{
print
(
error
)
}
}
func
publishHandler
(
result
:
Result
<
WFPost
,
Error
>)
{
do
{
let
wfPost
=
try
result
.
get
()
let
foundPostIndex
=
store
.
posts
.
firstIndex
(
where
:
{
$0
.
wfPost
.
title
==
wfPost
.
title
&&
$0
.
wfPost
.
body
==
wfPost
.
body
})
guard
let
index
=
foundPostIndex
else
{
return
}
DispatchQueue
.
main
.
async
{
self
.
store
.
posts
[
index
].
wfPost
=
wfPost
}
}
catch
{
print
(
error
)
}
}
func
updateFromServerHandler
(
result
:
Result
<
WFPost
,
Error
>)
{
do
{
let
fetchedPost
=
try
result
.
get
()
DispatchQueue
.
main
.
async
{
guard
let
selectedPost
=
self
.
selectedPost
else
{
return
}
self
.
store
.
replace
(
post
:
selectedPost
,
with
:
fetchedPost
)
}
}
catch
{
print
(
error
)
}
}
}
private
extension
WriteFreelyModel
{
//
MARK:
- Keychain Helpers
func
saveTokenToKeychain
(
_
token
:
String
,
username
:
String
?,
server
:
String
)
{
let
query
:
[
String
:
Any
]
=
[
kSecClass
as
String
:
kSecClassGenericPassword
,
kSecValueData
as
String
:
token
.
data
(
using
:
.
utf8
)
!
,
kSecAttrAccount
as
String
:
username
??
"anonymous"
,
kSecAttrService
as
String
:
server
]
let
status
=
SecItemAdd
(
query
as
CFDictionary
,
nil
)
guard
status
==
errSecDuplicateItem
||
status
==
errSecSuccess
else
{
fatalError
(
"Error storing in Keychain with OSStatus:
\(
status
)
"
)
}
}
func
purgeTokenFromKeychain
(
username
:
String
?,
server
:
String
)
throws
{
let
query
:
[
String
:
Any
]
=
[
kSecClass
as
String
:
kSecClassGenericPassword
,
kSecAttrAccount
as
String
:
username
??
"anonymous"
,
kSecAttrService
as
String
:
server
]
let
status
=
SecItemDelete
(
query
as
CFDictionary
)
guard
status
==
errSecSuccess
||
status
==
errSecItemNotFound
else
{
fatalError
(
"Error deleting from Keychain with OSStatus:
\(
status
)
"
)
}
}
func
fetchTokenFromKeychain
(
username
:
String
?,
server
:
String
)
->
String
?
{
let
query
:
[
String
:
Any
]
=
[
kSecClass
as
String
:
kSecClassGenericPassword
,
kSecAttrAccount
as
String
:
username
??
"anonymous"
,
kSecAttrService
as
String
:
server
,
kSecMatchLimit
as
String
:
kSecMatchLimitOne
,
kSecReturnAttributes
as
String
:
true
,
kSecReturnData
as
String
:
true
]
var
secItem
:
CFTypeRef
?
let
status
=
SecItemCopyMatching
(
query
as
CFDictionary
,
&
secItem
)
guard
status
!=
errSecItemNotFound
else
{
return
nil
}
guard
status
==
errSecSuccess
else
{
fatalError
(
"Error fetching from Keychain with OSStatus:
\(
status
)
"
)
}
guard
let
existingSecItem
=
secItem
as
?
[
String
:
Any
],
let
tokenData
=
existingSecItem
[
kSecValueData
as
String
]
as
?
Data
,
let
token
=
String
(
data
:
tokenData
,
encoding
:
.
utf8
)
else
{
return
nil
}
return
token
}
}
File Metadata
Details
Attached
Mime Type
text/x-c++
Expires
Thu, Apr 2, 5:41 AM (8 h, 24 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3678488
Attached To
rWFSUI WriteFreely SwiftUI
Event Timeline
Log In to Comment