A music player that connects to your cloud/distributed storage.
at main 749 lines 22 kB view raw
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)