1module Brain.User.State exposing (..)
2
3import Alien
4import Brain.Common.State as Common
5import Brain.Ports as Ports
6import Brain.Task.Ports
7import Brain.Types as Brain exposing (..)
8import Brain.User.Hypaethral as Hypaethral
9import Brain.User.Types as User exposing (..)
10import Debouncer.Basic as Debouncer
11import EverySet
12import Json.Decode as Decode
13import Json.Encode as Json
14import Playlists.Encoding as Playlists
15import Return exposing (andThen, return)
16import Return.Ext as Return
17import Settings
18import Sources.Encoding as Sources
19import Syncing
20import Syncing.Services.Dropbox.Token
21import Task exposing (Task)
22import TaskPort.Extra as TaskPort
23import Time
24import Tracks exposing (Track)
25import Tracks.Encoding as Tracks
26import Url exposing (Url)
27import Url.Ext as Url
28import User.Layer as User exposing (..)
29import User.Layer.Methods.Dropbox as Dropbox
30
31
32
33-- 🌳
34
35
36initialCommand : Url -> Cmd Brain.Msg
37initialCommand uiUrl =
38 case Url.action uiUrl of
39 _ ->
40 Cmd.batch
41 [ loadEnclosedData
42 , loadLocalHypaethralData
43 { initialUrl = uiUrl
44 , methodTask = loadSyncMethod
45 }
46 ]
47
48
49{-| Loads the "enclosed" data from cache and sends it to the UI.
50-}
51loadEnclosedData : Cmd Brain.Msg
52loadEnclosedData =
53 Decode.value
54 |> Brain.Task.Ports.fromCache Alien.EnclosedData
55 |> Task.map (Maybe.withDefault Json.null)
56 |> Common.attemptPortTask (Common.giveUICmdMsg Alien.LoadEnclosedUserData)
57
58
59{-| Loads the "sync method".
60-}
61loadSyncMethod : Task String (Maybe Method)
62loadSyncMethod =
63 Decode.value
64 |> Brain.Task.Ports.fromCache Alien.SyncMethod
65 |> Task.mapError TaskPort.errorToStringCustom
66 |> Task.map (Maybe.andThen decodeMethod)
67
68
69{-| Loads the "sync method" and "hypaethral" data,
70see `Commence` Msg what happens next.
71-}
72loadLocalHypaethralData : { initialUrl : Url, methodTask : Task String (Maybe Method) } -> Cmd Brain.Msg
73loadLocalHypaethralData { initialUrl, methodTask } =
74 methodTask
75 |> Task.andThen
76 (\maybeMethod ->
77 Hypaethral.retrieveLocal
78 |> User.retrieveHypaethralData
79 |> Task.map
80 (\bits ->
81 bits
82 |> List.map (\( a, b ) -> ( hypaethralBitKey a, Maybe.withDefault Json.null b ))
83 |> Json.object
84 )
85 |> Task.map (Tuple.pair maybeMethod)
86 )
87 |> Common.attemptTask
88 (\( maybeMethod, hypaethralJson ) ->
89 hypaethralJson
90 |> User.decodeHypaethralData
91 |> Result.map
92 (\hypaethralData ->
93 Commence
94 maybeMethod
95 initialUrl
96 ( hypaethralJson
97 , hypaethralData
98 )
99 )
100 |> Result.mapError Decode.errorToString
101 |> Common.reportErrorToUI UserMsg
102 )
103
104
105
106-- 📣
107
108
109update : User.Msg -> Manager
110update msg =
111 case msg of
112 Commence a b c ->
113 commence a b c
114
115 SetSyncMethod a ->
116 setSyncMethod a
117
118 Sync ->
119 sync { initialTask = Nothing }
120
121 UnsetSyncMethod ->
122 unsetSyncMethod
123
124 -----------------------------------------
125 -- x. Data
126 -----------------------------------------
127 RetrieveEnclosedData ->
128 retrieveEnclosedData
129
130 EnclosedDataRetrieved a ->
131 enclosedDataRetrieved a
132
133 SaveEnclosedData a ->
134 saveEnclosedData a
135
136 -----------------------------------------
137 -- y. Data
138 -----------------------------------------
139 -- The hypaethral user data is received in pieces,
140 -- pieces which are "cached" here in the web worker.
141 --
142 -- The reasons for this are:
143 -- 1. Lesser performance penalty on the UI when saving data
144 -- (ie. this avoids having to encode/decode everything each time)
145 -- 2. The data can be used in the web worker (brain) as well.
146 -- (eg. for track-search index)
147 --
148 SaveFavourites a ->
149 saveFavourites a
150
151 SavePlaylists a ->
152 savePlaylists a
153
154 SaveProgress a ->
155 saveProgress a
156
157 SaveSettings a ->
158 saveSettings a
159
160 SaveSources a ->
161 saveSources a
162
163 SaveTracks a ->
164 saveTracks a
165
166 -----------------------------------------
167 -- z. Data
168 -----------------------------------------
169 FinishedSyncing ->
170 finishedSyncing
171
172 GotHypaethralData a ->
173 gotHypaethralData a
174
175 SaveHypaethralDataBits a ->
176 saveHypaethralDataBits a
177
178 SaveHypaethralDataSlowly a ->
179 saveHypaethralDataSlowly a
180
181 -----------------------------------------
182 -- z. Secret Key
183 -----------------------------------------
184 RemoveEncryptionKey ->
185 removeEncryptionKey
186
187 UpdateEncryptionKey a ->
188 updateEncryptionKey a
189
190 -----------------------------------------
191 -- 📭 Other
192 -----------------------------------------
193 RefreshedDropboxTokens a b c ->
194 refreshedDropboxTokens a b c
195
196
197
198-- 🔱
199
200
201commence : Maybe Method -> Url -> ( Json.Value, HypaethralData ) -> Manager
202commence maybeMethod initialUrl ( hypaethralJson, hypaethralData ) model =
203 -- 🚀
204 -- Initiated from `initialCommand`.
205 -- Loaded the used-sync method and the local hypaethral data.
206 { model | userSyncMethod = maybeMethod }
207 |> sendHypaethralDataToUI hypaethralJson hypaethralData
208 |> andThen
209 (case Url.action initialUrl of
210 _ ->
211 sync { initialTask = Nothing }
212 )
213
214
215setSyncMethod : Json.Value -> Manager
216setSyncMethod json model =
217 -- 🐤
218 -- Set & store method,
219 -- and retrieve data.
220 let
221 decoder =
222 Decode.map2
223 (\a b -> ( a, b ))
224 (Decode.field "method" <| Decode.map methodFromString Decode.string)
225 (Decode.field "passphrase" <| Decode.maybe Decode.string)
226 in
227 case Decode.decodeValue decoder json of
228 Ok ( Just method, Just passphrase ) ->
229 let
230 initialTask =
231 passphrase
232 |> Brain.Task.Ports.fabricateSecretKey
233 |> Task.mapError TaskPort.errorToStringCustom
234 in
235 { model | userSyncMethod = Just method }
236 |> sync { initialTask = Just initialTask }
237 |> andThen (saveMethod method)
238
239 Ok ( Just method, Nothing ) ->
240 { model | userSyncMethod = Just method }
241 |> sync { initialTask = Nothing }
242 |> andThen (saveMethod method)
243
244 Ok ( Nothing, _ ) ->
245 Return.singleton { model | userSyncMethod = Nothing }
246
247 Err _ ->
248 Return.singleton model
249
250
251sync : { initialTask : Maybe (Task.Task String ()) } -> Manager
252sync { initialTask } model =
253 model
254 |> syncCommand (Maybe.withDefault (Task.succeed ()) initialTask)
255 |> return model
256 |> andThen
257 (case model.userSyncMethod of
258 Just method ->
259 Common.giveUI Alien.StartedSyncing (encodeMethod method)
260
261 Nothing ->
262 Return.singleton
263 )
264
265
266syncCommand : Task.Task String a -> Model -> Cmd Brain.Msg
267syncCommand initialTask model =
268 let
269 localData =
270 model.hypaethralUserData
271
272 attemptSync args =
273 args
274 |> Syncing.task
275 initialTask
276 { localData = localData
277 , saveLocal = Hypaethral.saveLocal
278 }
279 |> Common.attemptTask
280 (\maybe ->
281 case maybe of
282 Just data ->
283 UserMsg (GotHypaethralData data)
284
285 Nothing ->
286 UserMsg FinishedSyncing
287 )
288 in
289 case model.userSyncMethod of
290 Just (Dropbox { accessToken, expiresAt, refreshToken }) ->
291 if
292 Syncing.Services.Dropbox.Token.isExpired
293 { currentTime = model.currentTime
294 , expiresAt = expiresAt
295 }
296 then
297 refreshDropboxTokens
298 model.currentTime
299 Sync
300 initialTask
301 refreshToken
302
303 else
304 attemptSync
305 { retrieve = Hypaethral.retrieveDropbox accessToken
306 , save = Hypaethral.saveDropbox accessToken
307 }
308
309 Just (Ipfs { apiOrigin }) ->
310 attemptSync
311 { retrieve = Hypaethral.retrieveIpfs apiOrigin
312 , save = Hypaethral.saveIpfs apiOrigin
313 }
314
315 Just (RemoteStorage args) ->
316 attemptSync
317 { retrieve = Hypaethral.retrieveRemoteStorage args
318 , save = Hypaethral.saveRemoteStorage args
319 }
320
321 Nothing ->
322 Cmd.none
323
324
325unsetSyncMethod : Manager
326unsetSyncMethod model =
327 -- 💀
328 -- Unset & remove stored method.
329 [ Common.attemptPortTask (always Brain.Bypass) (Brain.Task.Ports.removeCache Alien.SyncMethod)
330 , Common.attemptPortTask (always Brain.Bypass) (Brain.Task.Ports.removeCache Alien.SecretKey)
331
332 --
333 , case model.userSyncMethod of
334 Just (Dropbox _) ->
335 Cmd.none
336
337 Just (Ipfs _) ->
338 Cmd.none
339
340 Just (RemoteStorage _) ->
341 Ports.deconstructRemoteStorage ()
342
343 Nothing ->
344 Cmd.none
345 ]
346 |> Cmd.batch
347 |> return { model | userSyncMethod = Nothing }
348
349
350
351-- 🔱 ░░ DATA - ENCLOSED
352
353
354enclosedDataRetrieved : Json.Value -> Manager
355enclosedDataRetrieved json =
356 Common.giveUI Alien.LoadEnclosedUserData json
357
358
359retrieveEnclosedData : Manager
360retrieveEnclosedData =
361 Decode.value
362 |> Brain.Task.Ports.fromCache Alien.EnclosedData
363 |> Common.attemptPortTask
364 (\maybe ->
365 case maybe of
366 Just json ->
367 Brain.UserMsg (EnclosedDataRetrieved json)
368
369 Nothing ->
370 Brain.Bypass
371 )
372 |> Return.communicate
373
374
375saveEnclosedData : Json.Value -> Manager
376saveEnclosedData json =
377 json
378 |> Brain.Task.Ports.toCache Alien.EnclosedData
379 |> Common.attemptPortTask (always Brain.Bypass)
380 |> Return.communicate
381
382
383
384-- 🔱 ░░ DATA - HYPAETHRAL
385
386
387finishedSyncing : Manager
388finishedSyncing model =
389 case model.userSyncMethod of
390 Just userSyncMethod ->
391 Common.giveUI Alien.SyncMethod (encodeMethod userSyncMethod) model
392
393 Nothing ->
394 Return.singleton model
395
396
397gotHypaethralData : HypaethralData -> Manager
398gotHypaethralData hypaethralData model =
399 model
400 |> sendHypaethralDataToUI (User.encodeHypaethralData hypaethralData) hypaethralData
401 |> andThen finishedSyncing
402
403
404saveAllHypaethralDataTask : HypaethralData -> Method -> Task String ()
405saveAllHypaethralDataTask userData method =
406 let
407 save =
408 saveHypaethralDataBitsTask (ModifiedAt :: User.allHypaethralBits) userData
409 in
410 case method of
411 Dropbox { accessToken } ->
412 save (Hypaethral.saveDropbox accessToken)
413
414 Ipfs { apiOrigin } ->
415 save (Hypaethral.saveIpfs apiOrigin)
416
417 RemoteStorage a ->
418 save (Hypaethral.saveRemoteStorage a)
419
420
421saveHypaethralDataBitsTask : List HypaethralBit -> HypaethralData -> (HypaethralBit -> Json.Value -> Task String ()) -> Task String ()
422saveHypaethralDataBitsTask bits userData saveFn =
423 [ --------
424 -- LOCAL
425 --------
426 List.map
427 (\bit ->
428 Hypaethral.saveLocal bit (encodeHypaethralBit bit userData)
429 )
430 bits
431 , ---------
432 -- REMOTE
433 ---------
434 List.map
435 (\bit ->
436 saveFn bit (encodeHypaethralBit bit userData)
437 )
438 bits
439 ]
440 |> List.concat
441 |> List.foldl
442 (\nextTask -> Task.andThen (\_ -> nextTask))
443 (Task.succeed ())
444
445
446{-| Save different parts of hypaethral data,
447one part at a time.
448-}
449saveHypaethralDataBits : List HypaethralBit -> Manager
450saveHypaethralDataBits bitsWithoutModifiedAt model =
451 let
452 bits =
453 ModifiedAt :: bitsWithoutModifiedAt
454
455 userData =
456 model.hypaethralUserData
457
458 updatedUserData =
459 { userData | modifiedAt = Just model.currentTime }
460
461 updatedModel =
462 { model | hypaethralUserData = updatedUserData }
463
464 save saveFn =
465 Time.now
466 |> Task.andThen
467 (\currentTime ->
468 saveHypaethralDataBitsTask
469 bits
470 { updatedUserData | modifiedAt = Just currentTime }
471 saveFn
472 )
473 |> Common.attemptTask (always Brain.Bypass)
474 |> return updatedModel
475 in
476 case model.userSyncMethod of
477 Just (Dropbox { accessToken, expiresAt, refreshToken }) ->
478 if
479 Syncing.Services.Dropbox.Token.isExpired
480 { currentTime = model.currentTime
481 , expiresAt = expiresAt
482 }
483 then
484 refreshToken
485 |> refreshDropboxTokens
486 model.currentTime
487 (SaveHypaethralDataBits bits)
488 (Task.succeed ())
489 |> return model
490
491 else
492 save (Hypaethral.saveDropbox accessToken)
493
494 Just (Ipfs { apiOrigin }) ->
495 save (Hypaethral.saveIpfs apiOrigin)
496
497 Just (RemoteStorage args) ->
498 save (Hypaethral.saveRemoteStorage args)
499
500 Nothing ->
501 -- Only save locally
502 save (\_ _ -> Task.succeed ())
503
504
505saveHypaethralDataBitWithDebounce : HypaethralBit -> Manager
506saveHypaethralDataBitWithDebounce bit =
507 bit
508 |> Debouncer.provideInput
509 |> saveHypaethralDataSlowly
510
511
512saveHypaethralDataSlowly : Debouncer.Msg HypaethralBit -> Manager
513saveHypaethralDataSlowly debouncerMsg model =
514 let
515 ( m, c, e ) =
516 Debouncer.update debouncerMsg model.hypaethralDebouncer
517
518 bits =
519 e
520 |> Maybe.withDefault []
521 |> EverySet.fromList
522 |> EverySet.toList
523 in
524 c
525 |> Cmd.map (SaveHypaethralDataSlowly >> UserMsg)
526 |> return { model | hypaethralDebouncer = m }
527 |> (if not (List.isEmpty bits) then
528 andThen (saveHypaethralDataBits bits)
529
530 else
531 identity
532 )
533
534
535sendHypaethralDataToUI : Json.Value -> HypaethralData -> Manager
536sendHypaethralDataToUI encodedData decodedData model =
537 [ encodedData
538 |> Alien.broadcast Alien.LoadHypaethralUserData
539 |> Ports.toUI
540
541 --
542 , decodedData.tracks
543 |> Json.list Tracks.encodeTrack
544 |> Ports.updateSearchIndex
545 ]
546 |> Cmd.batch
547 |> return { model | hypaethralUserData = decodedData }
548
549
550
551-- 🔱 ░░ DATA - HYPAETHRAL BITS
552
553
554saveFavourites : Json.Value -> Manager
555saveFavourites value model =
556 value
557 |> Decode.decodeValue (Decode.list Tracks.favouriteDecoder)
558 |> Result.withDefault model.hypaethralUserData.favourites
559 |> hypaethralLenses.setFavourites model
560 |> saveHypaethralDataBitWithDebounce Favourites
561
562
563savePlaylists : Json.Value -> Manager
564savePlaylists value model =
565 value
566 |> Decode.decodeValue (Decode.list Playlists.decoder)
567 |> Result.withDefault model.hypaethralUserData.playlists
568 |> hypaethralLenses.setPlaylists model
569 |> saveHypaethralDataBitWithDebounce Playlists
570
571
572saveProgress : Json.Value -> Manager
573saveProgress value model =
574 value
575 |> Decode.decodeValue (Decode.dict Decode.float)
576 |> Result.withDefault model.hypaethralUserData.progress
577 |> hypaethralLenses.setProgress model
578 |> saveHypaethralDataBitWithDebounce Progress
579
580
581saveSettings : Json.Value -> Manager
582saveSettings value model =
583 value
584 |> Decode.decodeValue (Decode.map Just Settings.decoder)
585 |> Result.withDefault model.hypaethralUserData.settings
586 |> hypaethralLenses.setSettings model
587 |> saveHypaethralDataBitWithDebounce Settings
588
589
590saveSources : Json.Value -> Manager
591saveSources value model =
592 value
593 |> Decode.decodeValue (Decode.list Sources.decoder)
594 |> Result.withDefault model.hypaethralUserData.sources
595 |> hypaethralLenses.setSources model
596 |> saveHypaethralDataBitWithDebounce Sources
597
598
599saveTracks : Json.Value -> Manager
600saveTracks value model =
601 saveTracksAndUpdateSearchIndex
602 (value
603 |> Decode.decodeValue (Decode.list Tracks.trackDecoder)
604 |> Result.withDefault model.hypaethralUserData.tracks
605 )
606 model
607
608
609saveTracksAndUpdateSearchIndex : List Track -> Manager
610saveTracksAndUpdateSearchIndex tracks model =
611 tracks
612 -- Store in model
613 |> hypaethralLenses.setTracks model
614 -- Update search index
615 |> Return.communicate
616 (tracks
617 |> Json.list Tracks.encodeTrack
618 |> Ports.updateSearchIndex
619 )
620 -- Save with delay
621 |> andThen (saveHypaethralDataBitWithDebounce Tracks)
622
623
624
625-- 🔱 ░░ DATA - HYPAETHRAL LENSES
626
627
628hypaethralLenses =
629 { setFavourites = makeHypaethralLens (\h f -> { h | favourites = f })
630 , setPlaylists = makeHypaethralLens (\h p -> { h | playlists = p })
631 , setProgress = makeHypaethralLens (\h p -> { h | progress = p })
632 , setSettings = makeHypaethralLens (\h s -> { h | settings = s })
633 , setSources = makeHypaethralLens (\h s -> { h | sources = s })
634 , setTracks = makeHypaethralLens (\h t -> { h | tracks = t })
635 }
636
637
638makeHypaethralLens : (HypaethralData -> a -> HypaethralData) -> Model -> a -> Model
639makeHypaethralLens setter model value =
640 { model | hypaethralUserData = setter model.hypaethralUserData value }
641
642
643
644-- 🔱 ░░ METHOD
645
646
647saveMethod : Method -> Manager
648saveMethod method model =
649 method
650 |> encodeMethod
651 |> Brain.Task.Ports.toCache Alien.SyncMethod
652 |> Common.attemptPortTask (always Brain.Bypass)
653 |> return { model | userSyncMethod = Just method }
654
655
656
657-- 🔱 ░░ SECRET KEY
658
659
660removeEncryptionKey : Manager
661removeEncryptionKey model =
662 Alien.SecretKey
663 |> Brain.Task.Ports.removeCache
664 |> Task.mapError TaskPort.errorToStringCustom
665 |> Task.andThen (\_ -> Time.now)
666 |> Task.andThen
667 (\currentTime ->
668 case model.userSyncMethod of
669 Just method ->
670 let
671 data =
672 model.hypaethralUserData
673 in
674 saveAllHypaethralDataTask { data | modifiedAt = Just currentTime } method
675
676 Nothing ->
677 Task.succeed ()
678 )
679 |> Common.attemptTask (always Brain.Bypass)
680 |> return model
681
682
683updateEncryptionKey : Json.Value -> Manager
684updateEncryptionKey json model =
685 case Decode.decodeValue Decode.string json of
686 Ok passphrase ->
687 passphrase
688 |> Brain.Task.Ports.fabricateSecretKey
689 |> Task.mapError TaskPort.errorToStringCustom
690 |> Task.andThen (\_ -> Time.now)
691 |> Task.andThen
692 (\currentTime ->
693 case model.userSyncMethod of
694 Just method ->
695 let
696 data =
697 model.hypaethralUserData
698 in
699 saveAllHypaethralDataTask { data | modifiedAt = Just currentTime } method
700
701 Nothing ->
702 Task.succeed ()
703 )
704 |> Common.attemptTask (always Brain.Bypass)
705 |> return model
706
707 Err _ ->
708 Return.singleton model
709
710
711
712-- 📭 ░░ OTHER
713
714
715refreshDropboxTokens : Time.Posix -> User.Msg -> Task.Task String a -> String -> Cmd Brain.Msg
716refreshDropboxTokens currentTime msg initialTask refreshToken =
717 initialTask
718 |> Task.andThen
719 (\_ -> Dropbox.refreshAccessToken refreshToken)
720 |> Task.attempt
721 (\result ->
722 case result of
723 Ok tokens ->
724 msg
725 |> RefreshedDropboxTokens
726 { currentTime = Time.posixToMillis currentTime // 1000
727 , refreshToken = refreshToken
728 }
729 tokens
730 |> UserMsg
731
732 Err err ->
733 Common.reportUICmdMsg Alien.ReportError err
734 )
735
736
737refreshedDropboxTokens :
738 { currentTime : Int, refreshToken : String }
739 -> Dropbox.Tokens
740 -> User.Msg
741 -> Manager
742refreshedDropboxTokens { currentTime, refreshToken } tokens msg model =
743 { accessToken = tokens.accessToken
744 , expiresAt = currentTime + tokens.expiresIn
745 , refreshToken = refreshToken
746 }
747 |> Dropbox
748 |> (\m -> saveMethod m model)
749 |> andThen (update msg)