Eventually-decentralized project hosting and management platform

[[ 🗃 ^WvWbo vervis ]] :: [📥 Inbox] [📤 Outbox] [🐤 Followers] [🤝 Collaborators] [🛠 Changes]

Clone

HTTPS: darcs clone https://vervis.peers.community/repos/WvWbo

SSH: darcs clone USERNAME@vervis.peers.community:WvWbo

Tags

TODO

src / Vervis /

Application.hs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
{- This file is part of Vervis.
 -
 - Written in 2016, 2018, 2019, 2020, 2022, 2023
 - by fr33domlover <fr33domlover@riseup.net>.
 -
 - ♡ Copying is an act of love. Please copy, reuse and share.
 -
 - The author(s) have dedicated all copyright and related and neighboring
 - rights to this software to the public domain worldwide. This software is
 - distributed without any warranty.
 -
 - You should have received a copy of the CC0 Public Domain Dedication along
 - with this software. If not, see
 - <http://creativecommons.org/publicdomain/zero/1.0/>.
 -}

{-# OPTIONS_GHC -fno-warn-orphans #-}

{- LANGUAGE RankNTypes #-}

module Vervis.Application
    ( getApplicationDev
    , appMain
    , develMain
    , makeFoundation
    , makeLogWare
    -- * for DevelMain
    , getApplicationRepl
    , shutdownApp
    -- * for GHCI
    , handler
    , db
    )
where

import Control.Concurrent.Chan
import Control.Concurrent.STM
import Control.Concurrent.STM.TVar
import Control.Exception hiding (Handler)
import Control.Monad
import Control.Monad.Logger                 (liftLoc, runLoggingT, logInfo, logError)
import Control.Monad.Trans.Except
import Control.Monad.Trans.Reader
import Data.Bifunctor
import Data.Default.Class
import Data.Foldable
import Data.Git.Repository (isRepo)
import Data.List
import Data.List.NonEmpty (nonEmpty)
import Data.Maybe
import Data.Proxy
import Data.String
import Data.Text (Text)
import Data.Time.Clock
import Data.Traversable
import Database.Persist.Postgresql
import Graphics.SVGFonts.Fonts (lin2)
import Graphics.SVGFonts.ReadFont (loadFont)
import Language.Haskell.TH.Syntax           (qLocation)
import Network.HTTP.Client (newManager)
import Network.HTTP.Client.TLS (tlsManagerSettings)
import Network.Wai
import Network.Wai.Handler.Warp             (Settings, defaultSettings,
                                             defaultShouldDisplayException,
                                             runSettings, setHost,
                                             setOnException, setPort, getPort)
import Network.Wai.Middleware.RequestLogger (Destination (Logger),
                                             IPAddrSource (..),
                                             OutputFormat (..), destination,
                                             mkRequestLogger, outputFormat)
import System.Directory
import System.FilePath
import System.Log.FastLogger
import Yesod.Auth
import Yesod.Core
import Yesod.Core.Dispatch
import Yesod.Core.Types hiding (Logger)
import Yesod.Default.Config2
import Yesod.Persist.Core
import Yesod.Static

import qualified Data.CaseInsensitive as CI
import qualified Data.HashMap.Strict as HM
import qualified Data.Text as T
import qualified Database.Esqueleto as E

import Database.Persist.Schema.PostgreSQL (schemaBackend)
import Dvara
import Yesod.Mail.Send (runMailer)

import Control.Concurrent.Actor
import Control.Concurrent.ResultShare
import Crypto.ActorKey
import Data.KeyFile
import Development.PatchMediaType
import Network.FedURI
import Web.Actor.Deliver
import Yesod.ActivityPub
import Yesod.Hashids
import Yesod.MonadSite

import Control.Concurrent.Local
import Data.List.NonEmpty.Local
import Web.Hashids.Local

import Vervis.Actor
import Vervis.Actor.Deck
import Vervis.Actor.Group
import Vervis.Actor.Loom
import Vervis.Actor.Person
import Vervis.Actor.Repo
import Vervis.Darcs
import Vervis.Data.Actor
import Vervis.Foundation
import Vervis.Git
import Vervis.Hook
import Vervis.KeyFile (isInitialSetup)
import Vervis.RemoteActorStore

-- Import all relevant handler modules here.
-- Don't forget to add new modules to your cabal file!
import Vervis.Handler.Client
import Vervis.Handler.Common
import Vervis.Handler.Cloth
import Vervis.Handler.Deck
--import Vervis.Handler.Git
import Vervis.Handler.Group
import Vervis.Handler.Key
import Vervis.Handler.Loom
import Vervis.Handler.Person
import Vervis.Handler.Repo
--import Vervis.Handler.Role
--import Vervis.Handler.Sharer
import Vervis.Handler.Ticket
--import Vervis.Handler.Wiki
--import Vervis.Handler.Workflow

import Vervis.Migration (migrateDB)
import Vervis.Model
import Vervis.Model.Ident
import Vervis.Path
import Vervis.Persist.Actor
import Vervis.Settings
import Vervis.Ssh (runSsh)
import Vervis.Web.Delivery

-- This line actually creates our YesodDispatch instance. It is the second half
-- of the call to mkYesodData which occurs in Foundation.hs. Please see the
-- comments there for more details.
mkYesodDispatch "App" resourcesApp

loggingFunction :: App -> LogFunc
loggingFunction app = messageLoggerSource app (appLogger app)

-- | This function allocates resources (such as a database connection pool),
-- performs initialization and returns a foundation datatype value. This is also
-- the place to put your migrate statements to have automatic database
-- migrations handled by Yesod.
makeFoundation :: AppSettings -> IO App
makeFoundation appSettings = do
    -- Some basic initializations: HTTP connection manager, logger, and static
    -- subsite.
    appHttpManager <- newManager tlsManagerSettings
    appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger
    appStatic <-
        (if appMutableStatic appSettings then staticDevel else static)
        appStaticDir

    appMailQueue <-
        case appMail appSettings of
            Nothing -> return Nothing
            Just _  -> Just <$> newChan

    appSvgFont <-
        if appLoadFontFromLibData appSettings
            then lin2
            else loadFont "data/LinLibertineCut.svg"

    appActorKeys <-
        if appPerActorKeys appSettings
            then pure Nothing
            else Just <$> do
                keys <- (,,)
                    <$> generateActorKey
                    <*> generateActorKey
                    <*> pure True
                newTVarIO keys

    appInstanceMutex <- newInstanceMutex

    appHookSecret <- generateKey

    appActorFetchShare <- newResultShare actorFetchShareAction

    appActivities <-
        case appInboxDebugReportLength appSettings of
            Nothing -> return Nothing
            Just n -> Just . (n,) <$> newTVarIO mempty

    -- We need a log function to create a connection pool. We need a connection
    -- pool to create our foundation. And we need our foundation to get a
    -- logging function. To get out of this loop, we initially create a
    -- temporary foundation without a real connection pool, get a log function
    -- from there, and then create the real foundation.
    let mkFoundation
            appConnPool
            appCapSignKey
            appHashidsContext
            appTheater =
                App {..}
        -- The App {..} syntax is an example of record wild cards. For more
        -- information, see:
        -- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html
        tempFoundation =
            mkFoundation
                (error "connPool forced in tempFoundation")
                (error "capSignKey forced in tempFoundation")
                (error "hashidsContext forced in tempFoundation")
                (error "theater forced in tempFoundation")
        logFunc = loggingFunction tempFoundation

    -- Create the database connection pool
    pool <- flip runLoggingT logFunc $ createPostgresqlPool
        (pgConnStr  $ appDatabaseConf appSettings)
        (pgPoolSize $ appDatabaseConf appSettings)

    setup <- isInitialSetup pool schemaBackend
    loadMode <- determineKeyFileLoadMode setup

    capSignKey <- loadKeyFile loadMode $ appCapabilitySigningKeyFile appSettings
    hashidsSalt <- loadKeyFile loadMode $ appHashidsSaltFile appSettings
    let hashidsCtx = hashidsContext hashidsSalt

        app = mkFoundation pool capSignKey hashidsCtx (error "theater")

    -- Perform database migration using our application's logging settings.
    --runLoggingT (runSqlPool (runMigration migrateAll) pool) logFunc
    let hLocal = appInstanceHost appSettings
    flip runWorker app $ runSiteDB $ do
        migrate "Vervis" $ migrateDB hLocal hashidsCtx
        migrate "Dvara" $ migrateDvara (Proxy :: Proxy App) schemaBackend
        verifyRepoDir
        --fixRunningDeliveries
        deleteUnusedURAs
        writePostReceiveHooks
        writePostApplyHooks

    -- Launch actor threads and fill the actor map
    let delieryStateDir = appDeliveryStateDir appSettings
    exists <- doesDirectoryExist delieryStateDir
    unless exists $ error $ "delivery-state-dir not found: " ++ delieryStateDir
    delivery <- do
        micros <- intervalMicros $ appDeliveryRetryBase appSettings
        startDeliveryTheater
            (sitePostSignedHeaders app) micros appHttpManager logFunc delieryStateDir
    let root = renderObjURI $ flip ObjURI topLocalURI $ appInstanceHost appSettings
        --render :: Yesod y => y -> Route y -> [(Text, Text)] -> Text
        render = yesodRender app root
        env = Env appSettings pool hashidsCtx appActorKeys delivery render
    actors <- flip runWorker app $ runSiteDB $ loadTheater env
    theater <- startTheater logFunc actors

    let hostString = T.unpack $ renderAuthority hLocal
    writeHookConfig hostString Config
        { configSecret     = hookSecretText appHookSecret
        , configPort       = fromIntegral $ appPort appSettings
        , configMaxCommits = 20
        }

    -- Return the foundation
    return app { appTheater = theater }
    where
    verifyRepoDir = do
        repos <- lift reposFromDir
        repos' <- reposFromDB
        unless (repos == repos') $ liftIO $ do
            putStrLn "Repo tree based on filesystem:"
            printRepos repos
            putStrLn "Repo tree based on database:"
            printRepos repos'
            throwIO $ userError "Repo dir check failed!"
        liftIO $ printRepos repos
        where
        printRepos = traverse_ $ \ (rp, vcs) ->
            putStrLn $
                "Found repo " ++ rp ++
                " [" ++ T.unpack (versionControlSystemName vcs) ++ "]"
    reposFromDir = do
        dir <- askRepoRootDir
        exists <- liftIO $ doesDirectoryExist dir
        unless exists $ error $ "repo-dir not found: " ++ dir
        subdirs <- liftIO $ sort <$> listDirectory dir
        for subdirs $ \ subdir -> do
            checkDir $ dir </> subdir
            vcs <- do
                mvcs <- detectVcs $ dir </> subdir
                let ref = dir ++ "/" ++ subdir
                case mvcs of
                    Left False -> error $ "Failed to detect VCS: " ++ ref
                    Left True -> error $ "Detected both VCSs: " ++ ref
                    Right v -> return v
            return (subdir, vcs)
        where
        checkDir path = liftIO $ do
            isdir <- doesDirectoryExist path
            islink <- pathIsSymbolicLink path
            unless (isdir && not islink) $
                error $ "Non-dir file: " ++ path
        detectVcs path = liftIO $ do
            darcs <- doesDirectoryExist $ path </> "_darcs"
            git <- isRepo $ fromString path
            return $
                case (darcs, git) of
                    (True, False) -> Right VCSDarcs
                    (False, True) -> Right VCSGit
                    (False, False) -> Left False
                    (True, True) -> Left True
    reposFromDB = do
        hashRepo <- getEncodeKeyHashid
        sortOn fst . map (adapt hashRepo) <$> selectList [] []
        where
        adapt hashRepo (Entity repoID repo) =
            (T.unpack $ keyHashidText $ hashRepo repoID, repoVcs repo)
    migrate :: MonadLogger m => Text -> ReaderT b m (Either Text (Int, Int)) -> ReaderT b m ()
    migrate name a = do
        r <- a
        case r of
            Left err -> do
                let msg = "DB migration failed: " <> name <> ": " <> err
                $logError msg
                error $ T.unpack msg
            Right (from, to) ->
                $logInfo $ T.concat
                    [ "DB migration success: ", name, ": "
                    , T.pack $ show from, " ==> ", T.pack $ show to
                    ]

    loadTheater :: Env -> WorkerDB [(LocalActorBy Key, Env, Verse -> Act (Either Text Text, Act (), Next))]
    loadTheater env = concat <$> sequenceA
        [ selectAll LocalActorPerson personBehavior
        , selectAll LocalActorGroup  groupBehavior
        , selectAll LocalActorRepo   repoBehavior
        , selectAll LocalActorDeck   deckBehavior
        , selectAll LocalActorLoom   loomBehavior
        ]
        where
        selectAll
            :: PersistRecordBackend a SqlBackend
            => (Key a -> LocalActorBy Key)
            -> (UTCTime -> Key a -> Verse -> ActE (Text, Act (), Next))
            -> WorkerDB [(LocalActorBy Key, Env, Verse -> Act (Either Text Text, Act (), Next))]
        selectAll makeLocalActor behavior =
            map (\ xid -> (makeLocalActor xid, env, behave behavior xid)) <$>
                selectKeysList [] []

-- | Convert our foundation to a WAI Application by calling @toWaiAppPlain@ and
-- applying some additional middlewares.
makeApplication :: App -> IO Application
makeApplication foundation = do
    logWare <- makeLogWare foundation
    -- Create the WAI application and apply middlewares
    appPlain <- toWaiAppPlain foundation
    return $ logWare $ defaultMiddlewaresNoLogging appPlain

makeLogWare :: App -> IO Middleware
makeLogWare foundation =
    mkRequestLogger def
        { outputFormat =
            if appDetailedRequestLogging $ appSettings foundation
                then Detailed True
                else Apache
                        (if appIpFromHeader $ appSettings foundation
                            then FromFallback
                            else FromSocket)
        , destination = Logger $ loggerSet $ appLogger foundation
        }


-- | Warp settings for the given foundation value.
warpSettings :: App -> Settings
warpSettings foundation =
      setPort (appPort $ appSettings foundation)
    $ setHost (appHost $ appSettings foundation)
    $ setOnException (\_req e ->
        when (defaultShouldDisplayException e) $ loggingFunction
            foundation
            $(qLocation >>= liftLoc)
            "yesod"
            LevelError
            (toLogStr $ "Exception from Warp: " ++ show e))
      defaultSettings

-- | For yesod devel, return the Warp settings and WAI Application.
getApplicationDev :: IO (Settings, Application)
getApplicationDev = do
    settings <- getAppSettings
    foundation <- makeFoundation settings
    wsettings <- getDevSettings $ warpSettings foundation
    app <- makeApplication foundation
    return (wsettings, app)

getAppSettings :: IO AppSettings
getAppSettings = loadYamlSettings [configSettingsYml] [] useEnv

-- | main function for use by yesod devel
develMain :: IO ()
develMain = develMainHelper getApplicationDev

actorKeyPeriodicRotator :: App -> Maybe (IO ())
actorKeyPeriodicRotator app =
    actorKeyRotator (appActorKeyRotation $ appSettings app) <$> appActorKeys app

{-
deliveryRunner :: App -> IO ()
deliveryRunner app =
    let interval = appDeliveryRetryFreq $ appSettings app
    in  runWorker (periodically interval retryOutboxDelivery) app
-}

sshServer :: App -> IO ()
sshServer foundation =
    runSsh
        (appSettings foundation)
        (appHashidsContext foundation)
        (appConnPool foundation)
        (loggingFunction foundation)

mailer :: App -> IO ()
mailer foundation =
    case (appMail $ appSettings foundation, appMailQueue foundation) of
        (Nothing  , Nothing)    -> return ()
        (Nothing  , Just _)     -> error "Mail queue unnecessarily created"
        (Just _   , Nothing)    -> error "Mail queue wasn't created"
        (Just mail, Just queue) ->
            runMailer
                mail
            --  (appConnPool foundation)
                (loggingFunction foundation)
                (readChan queue)

-- | The @main@ function for an executable running this site.
appMain :: IO ()
appMain = do
    -- Get the settings from all relevant sources
    settings <- loadYamlSettings
        -- Read settings from the settings file
        [configSettingsYml]

        -- Fall back to compile-time values, set to [] to require values at
        -- runtime
        --[configSettingsYmlValue]
        []

        -- Allow environment variables to override
        useEnv

    -- Generate the foundation from the settings
    foundation <- makeFoundation settings

    -- Generate a WAI Application from the foundation
    app <- makeApplication foundation

    -- Run actor signature key periodic generation thread
    traverse_ forkCheck $ actorKeyPeriodicRotator foundation

    -- If we're using per-actor keys, generate keys for local actors that don't
    -- have a key and insert to DB
    runWorker fillPerActorKeys foundation

    -- Run periodic activity delivery retry runner
    -- Disabled because we're using the DeliveryTheater now
    {-
    when (appFederation $ appSettings foundation) $
        forkCheck $ deliveryRunner foundation
    -}

    -- Run SSH server
    forkCheck $ sshServer foundation

    -- Run mailer if SMTP is enabled
    forkCheck $ mailer foundation

    -- Run the application with Warp
    runSettings (warpSettings foundation) app


--------------------------------------------------------------
-- Functions for DevelMain.hs (a way to run the app from GHCi)
--------------------------------------------------------------
getApplicationRepl :: IO (Int, App, Application)
getApplicationRepl = do
    settings <- getAppSettings
    foundation <- makeFoundation settings
    wsettings <- getDevSettings $ warpSettings foundation
    app1 <- makeApplication foundation
    return (getPort wsettings, foundation, app1)

shutdownApp :: App -> IO ()
shutdownApp _ = return ()


---------------------------------------------
-- Functions for use in development with GHCi
---------------------------------------------

-- | Run a handler
handler :: Handler a -> IO a
handler h = getAppSettings >>= makeFoundation >>= flip unsafeHandler h

-- | Run DB queries
db :: ReaderT SqlBackend (HandlerFor App) a -> IO a
db = handler . runDB
[See repo JSON]