Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
No results found
Select Git revision
Show changes
Commits on Source (15)
image: haskell:8.10.7 image: haskell:9.0.2
stages: stages:
- build - build
- trigger # - trigger
build-job: build-job:
stage: build stage: build
...@@ -23,10 +23,10 @@ build-job: ...@@ -23,10 +23,10 @@ build-job:
- .stack-root/ - .stack-root/
trigger-mapservice-pipeline: # trigger-mapservice-pipeline:
only: # only:
refs: # refs:
- main # - main
stage: trigger # stage: trigger
script: # script:
- curl -X POST -F token=$MAPSERVICE_TOKEN -F ref=main https://git.cccv.de/api/v4/projects/252/trigger/pipeline # - curl -X POST -F token=$MAPSERVICE_TOKEN -F ref=main https://git.cccv.de/api/v4/projects/252/trigger/pipeline
...@@ -7,43 +7,44 @@ errors (such as non-existent map entrypoints or missing asset files) and makes ...@@ -7,43 +7,44 @@ errors (such as non-existent map entrypoints or missing asset files) and makes
suggestions to improve accessability. suggestions to improve accessability.
It can also *adjust* maps — e.g. to automatically insert property values or help It can also *adjust* maps — e.g. to automatically insert property values or help
enforce an event's map policies (among other things, this is used to resolve enforce an event's map policies (among other things, this was used to resolve
special inter-assembly `world://` links). special inter-assembly `world://` links at rc3).
`walint-mapserver` is a minimal implementation of a server that periodically `walint-mapserver` is a minimal implementation of a server that periodically
fetches, lints, and adjusts maps from a set of git repositories, writing them fetches, lints, and adjusts maps from a set of git repositories, writing them
to a path that can then be served by a webserver. It can be used as a (very to a path that can then be served as static map files for a workadventure
simple) replacement for rc3's hub and mapservice at smaller events. deployment. It can be used as a (very simple) replacement for rc3's hub and
mapservice at smaller events — to get started, manually list all map
repositories in `config.toml`, then visit `localhost:8080/admin/overview`.
`cwality-maps` is a small server for type-safe map templating, to be used if
maps need to be generated on-the-fly — for example, to provide custom intermediate
maps displaying CWs before another map can be reached.
## Installing ## Installing
### From the CI pipeline ### From the CI pipeline
Gitlab [automatically builds a version](https://git.cccv.de/hub/walint/-/jobs) Gitlab [automatically builds a version](https://git.cccv.de/hub/walint/-/jobs)
of `walint` each time something is pushed to this repository. The resulting of `walint` each time something is pushed to the version of this repository
binary should work fine on most linux systems, especially if they're vaguely kept at the CCCV infra. The resulting binary should work fine on most linux
debian-like. systems, especially if they're vaguely debian-like.
In case you get an incomprehensible or confusing error when executing it, try In case you get an incomprehensible or confusing error when executing it, try
running `ldd walint` and see if anything is marked as not found, then install it. running `ldd walint` and see if anything is marked as not found, then install
it.
### Build using stack ### Build using stack
This uses a lockfile to pin versions of dependencies (as well as `ghc`, the haskell This uses a lockfile to pin versions of dependencies (as well as `ghc`, the
compiler). You will need [the haskell stack](https://docs.haskellstack.org/en/stable/README/). haskell compiler). You will need
[the haskell stack](https://docs.haskellstack.org/en/stable/README/).
Then just run Run
``` ```
stack build stack build
``` ```
If you lack `ghc` in the correct version and don't know how to install it, you can If you lack `ghc` and don't know how to install it, you can add `--install-ghc`,
pass it `--install-ghc` to take care of that for you (note that on NixOS, `stack` may and `stack` will take care of that for you (note that on NixOS, `stack` may
use a fitting `ghc` derivation if it finds one, even without `--install-ghc`). use a fitting `ghc` derivation if it finds one, even without `--install-ghc`).
To install into your `$PATH`, use To install into your `$PATH`, use
...@@ -58,9 +59,6 @@ Alternatively, run `walint` via stack: ...@@ -58,9 +59,6 @@ Alternatively, run `walint` via stack:
stack run -- walint [options as normal] stack run -- walint [options as normal]
``` ```
However, in this case stack will re-check files every time to ensure your build
is up to date with the sources, increasing startup time.
### Build using cabal ### Build using cabal
You can, but probably should not. Beware of older Aeson versions! You can, but probably should not. Beware of older Aeson versions!
...@@ -79,8 +77,8 @@ walint --config-file config.json --repository path \ ...@@ -79,8 +77,8 @@ walint --config-file config.json --repository path \
defaults to `main.json` defaults to `main.json`
- `--lintLevel`: limit output only to messages that have at most the given - `--lintLevel`: limit output only to messages that have at most the given
level. Valid levels are `Info`, `Suggestion`, `Warning`, `Forbidden`, `Error`, level. Valid levels are `Info`, `Suggestion`, `Warning`, `Forbidden`, `Error`,
`Fatal`. Defaults to `Suggestion` if not given. `Fatal`. Defaults to `Suggestion`.
- `--json`: print output as json instead of the normal more human-friendly format - `--json`: print output as json instead of the default human-friendly format
- `--pretty`: if used with `--json`, insert line breaks and indentation to make - `--pretty`: if used with `--json`, insert line breaks and indentation to make
the output more readable. Otherwise no effect. the output more readable. Otherwise no effect.
- `--out path`: write the linted & adjusted repository to the given path. Any - `--out path`: write the linted & adjusted repository to the given path. Any
...@@ -103,40 +101,45 @@ have default values. In `config.json`, all possible keys are given. ...@@ -103,40 +101,45 @@ have default values. In `config.json`, all possible keys are given.
Most options should be reasonably self-explanatory. Note that `MaxLintLevel` Most options should be reasonably self-explanatory. Note that `MaxLintLevel`
differs from the option `--lintLevel`: the latter merely determines what is differs from the option `--lintLevel`: the latter merely determines what is
*printed* (in case json output is not enabled), the former determines the *printed* (in case json output is not enabled), the former determines the
maximum lint level allowed before the linter rejects the map and does not maximum lint level allowed before the linter rejects the map and will refuse to
copy it to the path given to `--out`. copy it to the path given as `--out`.
### Uri Schemas ### Uri Schemas
`walint` supports (limited) rewriting of URIs contained in the map json via `walint` supports (basic) rewriting of URIs contained in the map json via the
the `UriSchemas` option, which takes a map from uri schemas to a rule describing `UriSchemas` option, which maps schemas to rules describing what to do with such
what to do with such links, depending on the scope in which they appear. links, depending on the scope in which they appear.
`walint` takes a very reductive view or URIs: `schema://domain/tail` `walint` takes a very reductive view or URIs: `schema://domain/tail`
#### Rewrite Rules #### Rewrite Rules
For now there are three types of such rules: For now there are four types of such rules:
- `schema: {"scope":[scopes]}`: if in a scope listed in `scopes`, allow any - `schema: {"scope":[scopes]}`: if in a scope listed in `scopes`, allow any
links of the given `schema` links of the given `schema`
- `schema: {"scope":[scopes], "allowed":[allowed]}`: if in a scope listed in
`scopes`, only allow URIs whose domain occurs in `allowed`.
- `schema: {"scope":[scopes], "allowed":[allowed], "blocked":[blocked], "prefix":prefix}`: - `schema: {"scope":[scopes], "allowed":[allowed], "blocked":[blocked], "prefix":prefix}`:
if in a scope listed in `scopes`, prefix any URIs of the given `schema` with if in a scope listed in `scopes`, allow URIs whose domain occurs in `allowed`,
the given `prefix`, unless the URI's domain occurs in `allowed` (in which case disallow all whose domain occurs in `blocked`, and for all others, prefix
leave it untouched), or it occurs in `blocked`, in which with the string given as `prefix`
case it will be rejected as a lint error. - `schema: {"scope":[scopes], "substs":{domain: prefix, ...}}`: if in a
- `schema: {"scope":[scopes], "subst":{domain: prefix, ...}}`: if in a scope scope listed in `scopes` and given a URI with the domain `domain`,
listed in `scopes` and given a URI with the domain `domain`, concatenate concatenate `prefix` with the tail of this URI.
`prefix` with the tail of this URI.
In case an URI is encountered and there is no applicable rule, it will be rejected In case an URI is encountered and there is no applicable rule, it will be
(note that this means you'll have to explicitly allow `https://` for links!) rejected (note that this means you'll have to explicitly allow `https://` for
links!)
There are currently three possible scopes: `map` applies to tiled map links There are currently four scopes:
(i.e. `exitUrl`), `website` to `openWebsite`, `audio` to `playAudio`. - `map` applies to tiled map links (i.e. `exitUrl`)
- `website` to `openWebsite`
- `audio` to `playAudio`
- `script` to scripts
## Output ## Output
By default `walint` prints lints in a hopefully human-readable manner. Its exit By default `walint` prints lints in a (hopefully) human-readable format. Its exit
code will be 1 if the maximum lint level set in its config was exceeded, and 0 code will be 1 if the maximum lint level set in its config was exceeded, and 0
otherwise. Only in the latter case will it write out an adjusted map respository otherwise. Only in the latter case will it write out an adjusted map respository
to the path passed to `--out`. If the path given to `--out` already exists, to the path passed to `--out`. If the path given to `--out` already exists,
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
}, },
{ {
"scope" : [ "script" ], "scope" : [ "script" ],
"allowed" : [ "scripts.world.di.c3voc.de" ] "allowed" : [ "example.org" ]
}] }]
} }
} }
...@@ -17,29 +17,29 @@ token = "hello, world!" ...@@ -17,29 +17,29 @@ token = "hello, world!"
slug = "divoc" slug = "divoc"
# baseurl of maps as seen by the frontend # baseurl of maps as seen by the frontend
url = "https://world.di.c3voc.de/maps/" url = "https://example.org/maps/"
# webdir into which maps should be written # webdir into which maps should be written
webdir = "/tmp/var/www/divoc" webdir = "/tmp/var/www/example"
webdir_human = "/tmp/var/www-human/divoc"
# increment this if you change the server / linter config # increment this if you change the server / linter config
# (part of urls for linted maps; allows indefinite browser caching) # (part of urls for linted maps; allows indefinite browser caching)
generation = 1 generation = 1
backlink_prefix = "world://lobby#start_" backlink_prefix = "world://lobby#start_"
contact_mail = "world@muc.hacc.space" contact_mail = "world@example.org"
howto_link = "https://di.c3voc.de/howto:world" howto_link = "https://example.org"
# linter's config for this org # linter's config for this org
lintconfig = "./config.json" lintconfig = "./config.json"
# map's entrypoint (only maps reachable from here are included) # map's entrypoint (only maps reachable from here are included)
entrypoint = "main.json" entrypoint = "main.json"
[[org.repo]] # I hate TOML
url = "https://gitlab.infra4future.de/hacc/events/hacc-map"
ref = "master"
name = "hacc"
[[org.repo]] [[org.repo]]
url = "https://github.com/namiko/assembly_2021" url = "https://github.com/namiko/assembly_2021"
ref = "master" ref = "master"
name = "haecksen" name = "haecksen"
# add more repos here …
verbose = true
port = 8080
# directory containing template maps.
# all .json files therein will be interpreted as maps;
# other files are served statically
template = "./example-templates"
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
module Config ( loadConfig
, Config, port, verbose, template
) where
import Universum
import Data.List (isSuffixOf)
import qualified Data.Map.Strict as M
import Data.Tiled (Tiledmap,
loadTiledmap)
import Lens.Micro.Platform (makeLenses, traverseOf)
import System.Directory (listDirectory)
import System.FilePath ((</>))
import Toml (TomlCodec, (.=))
import qualified Toml as T
type family ConfigRes (b :: Bool) a where
ConfigRes True a = a
ConfigRes False a = FilePath
-- | the server's configuration
data Config (loaded :: Bool) = Config
{ _port :: Int
, _verbose :: Bool
, _template :: ConfigRes loaded (FilePath, Map Text Tiledmap)
} deriving Generic
makeLenses ''Config
configCodec :: TomlCodec (Config False)
configCodec = Config
<$> T.int "port" .= _port
<*> T.bool "verbose" .= _verbose
<*> T.string "template" .= _template
loadConfig :: FilePath -> IO (Config True)
loadConfig path = do
T.decodeFileEither configCodec path >>= \case
Right c -> traverseOf template loadMaps c
Left err -> error (show err)
where loadMaps path = do
maps <- listDirectory path
<&> filter (".json" `isSuffixOf`)
list <- forM maps $ \mapname ->
loadTiledmap (path </> mapname) >>= \case
Right tmap -> pure (toText mapname, tmap)
err -> error (show err)
pure (path, M.fromList list)
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
-- | simple server offering linting "as a service"
module Main where
import Universum
import Config (Config, loadConfig, port,
template, verbose)
import Data.Aeson (FromJSON)
import qualified Data.Aeson as A
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import Data.Text.Encoding.Base64.URL (decodeBase64Unpadded)
import Data.Tiled (Tiledmap)
import Network.Wai.Handler.Warp (defaultSettings,
runSettings, setPort)
import Network.Wai.Middleware.Gzip (def)
import Network.Wai.Middleware.RequestLogger (OutputFormat (..),
RequestLoggerSettings (..),
mkRequestLogger)
import Servant (Application, Capture,
CaptureAll,
FromHttpApiData (parseUrlPiece),
Get, Handler, JSON, Raw,
Server, err400, err404,
serve, throwError,
type (:<|>) (..),
type (:>))
import Servant.Server.StaticFiles (serveDirectoryWebApp)
import Substitute (Substitutable (substitute),
SubstitutionError)
import Control.Monad.Logger
-- | a map's filename ending in .json
-- (a newtype to differentiate between maps and assets in a route)
newtype JsonFilename = JsonFilename Text
instance FromHttpApiData JsonFilename where
parseUrlPiece url =
if ".json" `T.isSuffixOf` url
then Right (JsonFilename url)
else Left url
newtype Tag = Tag Text
deriving (Generic, FromJSON)
newtype MapParams = MapParams
{ substs :: Map Text Text
} deriving (Generic, FromJSON)
instance FromHttpApiData MapParams where
parseUrlPiece urltext =
case decodeBase64Unpadded urltext of
Right text -> case A.decode (encodeUtf8 text) of
Just params -> params
Nothing -> Left "decoding params failed?"
-- for fun (and testing) also allow non-encoded json
Left _err -> case A.decode (encodeUtf8 urltext) of
Just params -> Right params
Nothing -> Left "decoding MapParams failed"
-- | actual set of routes: api for json & html + static pages from disk
type Routes =
"generate" :> Capture "params" MapParams :>
(Capture "map.json" JsonFilename :> Get '[JSON] Tiledmap
-- explicitly capture broken json to return 400 instead of looking for files
:<|> Capture "map.json" JsonFilename :> CaptureAll "rest" Text :> Get '[JSON] Void
:<|> Raw)
mkMap :: Config True -> Tiledmap -> MapParams -> ([SubstitutionError], Tiledmap)
mkMap _config basemap params =
substitute basemap (substs params)
mapHandler :: MapParams -> Config True -> JsonFilename -> Handler Tiledmap
mapHandler params config (JsonFilename mapname) =
case M.lookup mapname (snd $ view template config) of
Just basemap -> runStdoutLoggingT $
logWarnN (pretty errors) >> pure tiledmap
where (errors, tiledmap) = mkMap config basemap params
pretty errors = T.concat
. intersperse "\n "
$ concatMap (lines . show) errors
Nothing -> throwError err404
-- | Complete set of routes: API + HTML sites
server :: Config True -> Server Routes
server config params =
mapHandler params config
:<|> (\_ _ -> throwError err400)
:<|> serveDirectoryWebApp (fst . view template $ config)
app :: Config True -> Application
app = serve (Proxy @Routes) . server
main :: IO ()
main = do
config <- loadConfig "./cwality-config.toml"
loggerMiddleware <- mkRequestLogger
$ def { outputFormat = Detailed (view verbose config) }
let warpsettings =
setPort (view port config)
defaultSettings
runSettings warpsettings
. loggerMiddleware
$ app config
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Typeclasses for (generic) substitution on all strings contained in an ADT,
-- failsafe, but with error reporting
module Substitute (SubstitutionError, Substitutable(..)) where
import Universum
import qualified Data.Aeson as A
import qualified Data.Foldable as Fold
import Data.Tiled (GlobalId, LocalId)
import GHC.Generics (Generic (Rep, from, to), K1 (K1),
M1 (M1), U1, type (:*:) ((:*:)),
type (:+:) (..))
import qualified Text.Mustache as MU
import qualified Text.Mustache.Render as MU
import Text.Parsec.Error (ParseError)
-- | errors that might be encountered. SubstitutionErrors occur during substitution
-- and a generally non-fatal (but might result e.g. in empty strings being inserted
-- instead of variables), while CompileErrors may indicate that (invalid) template
-- syntax got leaked into the output
data SubstitutionError = CompileError ParseError | Mustache MU.SubstitutionError
deriving Show
class Substitutable s where
substitute :: s -> Map Text Text -> ([SubstitutionError], s)
instance Substitutable Text where
substitute orig substs = case MU.compileTemplate "" orig of
Right template -> first (map Mustache) $ MU.checkedSubstitute template substs
Left err -> ([CompileError err], orig) -- just ignore syntactic errors (TODO: add a log message?)
instance {-# OVERLAPS #-} Substitutable String where
substitute orig substs = second toString (substitute (toText orig) substs)
instance {-# OVERLAPPING #-} (Functor a, Substitutable b, Foldable a) => Substitutable (a b) where
substitute orig substs = (Fold.fold $ map fst orig',) $ map snd orig'
where orig' = map (`substitute` substs) orig
-- | helper: don't substitute anything, don't produce errors
trivial :: t -> b -> ([a], t)
trivial = const . ([],)
instance {-# OVERLAPS #-} Substitutable A.Value where
substitute (A.Object fields) params =
second A.Object $ traverse (`substitute` params) fields
substitute (A.String str) params =
second A.String $ substitute str params
substitute other params = ([], other)
instance Substitutable Int where
substitute = trivial
instance Substitutable GlobalId where
substitute = trivial
instance Substitutable LocalId where
substitute = trivial
instance Substitutable Double where
substitute = trivial
instance Substitutable Float where
substitute = trivial
class GSubstitutable i where
gsubstitute :: i p -> Map Text Text -> ([SubstitutionError], i p)
instance Substitutable c => GSubstitutable (K1 i c) where
gsubstitute (K1 text) = second K1 . substitute text
instance (GSubstitutable a, GSubstitutable b) => GSubstitutable (a :*: b) where
gsubstitute (a :*: b) substs = (e1 <> e2, a' :*: b')
where (e1, a') = gsubstitute a substs
(e2, b') = gsubstitute b substs
instance (GSubstitutable a, GSubstitutable b) => GSubstitutable (a :+: b) where
gsubstitute (L1 a) = second L1 . gsubstitute a
gsubstitute (R1 a) = second R1 . gsubstitute a
instance (GSubstitutable a) => GSubstitutable (M1 x y a) where
gsubstitute (M1 a) = second M1 . gsubstitute a
instance GSubstitutable U1 where
gsubstitute = trivial
instance {-# OVERLAPPABLE #-} (GSubstitutable (Rep a), Generic a) => Substitutable a where
substitute a substs = second to (gsubstitute (from a) substs)
{ nixpkgs ? import <nixpkgs> {}, compiler ? "default", doBenchmark ? false }:
let let
sources = import ./nix/sources.nix {};
haskellNix = import sources.haskellNix {};
# Import nixpkgs and pass the haskell.nix provided nixpkgsArgs inherit (nixpkgs) pkgs;
pkgs = import
# use haskell.nix's nixpkgs, which may (?) have more substitutes available f = { mkDerivation, aeson, aeson-pretty, async, base, base-compat
haskellNix.sources.nixpkgs-unstable , base64-bytestring, bytestring, containers, cryptohash-sha1
# args for nixpkgs; includes the haskell.nix overlay , deepseq, directory, dotgen, either, extra, filepath, fmt
(haskellNix.nixpkgsArgs // { system = "x86_64-linux"; }); , getopt-generics, hpack, http-client, http-types, lib, lucid
, microlens-platform, monad-logger, network-uri, process
, regex-tdfa, servant, servant-client, servant-lucid
, servant-server, servant-websockets, stm, template-haskell, text
, text-metrics, time, tomland, transformers, universum, uri-encode
, uuid, vector, wai, wai-extra, warp, websockets
}:
mkDerivation {
pname = "walint";
version = "0.1";
src = ./.;
isLibrary = true;
isExecutable = true;
libraryHaskellDepends = [
aeson base bytestring containers deepseq dotgen either extra
filepath getopt-generics network-uri regex-tdfa text text-metrics
transformers universum uri-encode vector
];
libraryToolDepends = [ hpack ];
executableHaskellDepends = [
aeson aeson-pretty async base base-compat base64-bytestring
bytestring containers cryptohash-sha1 directory extra filepath fmt
getopt-generics http-client http-types lucid microlens-platform
monad-logger process servant servant-client servant-lucid
servant-server servant-websockets stm template-haskell text time
tomland universum uuid wai wai-extra warp websockets
];
doHaddock = false;
prePatch = "hpack";
homepage = "https://stuebinm.eu/git/walint";
license = "unknown";
hydraPlatforms = lib.platforms.none;
};
haskellPackages = if compiler == "default"
then pkgs.haskellPackages
else pkgs.haskell.packages.${compiler};
variant = if doBenchmark then pkgs.haskell.lib.doBenchmark else pkgs.lib.id;
drv = variant (haskellPackages.callPackage f {});
drvs = pkgs.haskell-nix.project {
# 'cleanGit' cleans a source directory based on the files known by git
src = pkgs.haskell-nix.haskellLib.cleanGit {
src = ./.;
name = "walint";
};
modules = [{
packages.walint.components.exes = {
# don't include gcc or ghc in the dependency closure …
walint-server.dontStrip = false;
walint.dontStrip = false;
};
}];
stack-sha256 = "0bp3dwj2kl6n0swz5yf9kjy5ahp6i5qrnb39hkrsqgf0682i9nk1";
};
in in
{ drv
walint = drvs.walint.components.exes.walint;
walint-server = pkgs.stdenvNoCC.mkDerivation {
name = "walint-server-with-assets";
src = drvs.walint.components.exes.walint-mapserver;
phases = [ "buildPhase" ];
buildPhase = ''
mkdir -p $out
cp -r $src/* $out
cp -r ${pkgs.copyPathToStore ./static} $out/static
cp ${pkgs.copyPathToStore ./config.json} $out/config.json
cp ${pkgs.copyPathToStore ./config.toml} $out/config.toml
'';
};
}
{ nixpkgs ? import <nixpkgs> {} }:
with nixpkgs;
stdenv.mkDerivation {
name = "walint-fixed";
buildInputs = [ ghc stack zlib zlib.dev git openssl cacert ];
src = ./.;
buildPhase = ''
cp -r $src .
mkdir /tmp/stack-home
HOME=/tmp/stack-home stack build --no-nix --system-ghc
'';
installPhase = ''
HOME=/tmp/stack-home stack install --local-bin-path $out --no-nix --system-ghc
mkdir -p $out/share/walint
cp -r static $out/share/walint
cp config.json $out/share/walint
cp config.toml $out/share/walint
'';
outputHashAlgo = "sha256";
outputHashMode = "recursive";
# replace this with the correct SHA256
outputHash = "sha256-Qd7MDGslrS6zs6WWI9sjzDous0nUbrjdK2fF747KLq8=";
dontShrink = true;
}
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1665466769,
"narHash": "sha256-L+qcHpb4Ac3PipMXJY/Ktbu1+KXy23WCZ8pXWmsf7zY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0b20bf89e0035b6d62ad58f9db8fdbc99c2b01e8",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-22.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
{
description = "walint: workadventure map linting";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/release-22.05";
outputs = { self, nixpkgs }:
{
defaultPackage.x86_64-linux = import ./default.nix {
nixpkgs = import nixpkgs { system = "x86_64-linux"; };
};
};
}
...@@ -35,12 +35,11 @@ import qualified Data.Text as T ...@@ -35,12 +35,11 @@ import qualified Data.Text as T
import Data.Tiled (Tiledmap) import Data.Tiled (Tiledmap)
import Dirgraph (graphToDot, invertGraph, resultToGraph, import Dirgraph (graphToDot, invertGraph, resultToGraph,
takeSubGraph, unreachableFrom) takeSubGraph, unreachableFrom)
import GHC.Generics (Generic)
import LintConfig (LintConfig', configMaxLintLevel) import LintConfig (LintConfig', configMaxLintLevel)
import Paths (normalise, normaliseWithFrag) import Paths (normalise, normaliseWithFrag)
import System.Directory.Extra (doesFileExist) import System.Directory.Extra (doesFileExist)
import System.FilePath (splitPath, (</>))
import qualified System.FilePath as FP import qualified System.FilePath as FP
import System.FilePath (splitPath, (</>))
import System.FilePath.Posix (takeDirectory) import System.FilePath.Posix (takeDirectory)
import Text.Dot (showDot) import Text.Dot (showDot)
import Types (Dep (Local, LocalMap), Hint (Hint), import Types (Dep (Local, LocalMap), Hint (Hint),
......
...@@ -32,8 +32,8 @@ import Data.Tiled (Layer (layerLayers, layerName), ...@@ -32,8 +32,8 @@ import Data.Tiled (Layer (layerLayers, layerName),
import LintConfig (LintConfig (..), LintConfig') import LintConfig (LintConfig (..), LintConfig')
import LintWriter (LintResult, invertLintResult, import LintWriter (LintResult, invertLintResult,
resultToAdjusted, resultToBadges, resultToAdjusted, resultToBadges,
resultToDeps, resultToLints, resultToOffers, resultToCWs, resultToDeps, resultToJitsis,
runLintWriter, resultToCWs) resultToLints, resultToOffers, runLintWriter)
import Properties (checkLayer, checkMap, checkTileset) import Properties (checkLayer, checkMap, checkTileset)
import System.FilePath (takeFileName) import System.FilePath (takeFileName)
import Types (Dep (MapLink), import Types (Dep (MapLink),
...@@ -44,7 +44,7 @@ import Util (PrettyPrint (prettyprint), prettyprint) ...@@ -44,7 +44,7 @@ import Util (PrettyPrint (prettyprint), prettyprint)
data ResultKind = Full | Shrunk data ResultKind = Full | Shrunk
type family Optional (a :: ResultKind) (b :: *) where type family Optional (a :: ResultKind) (b :: Type) where
Optional Full b = b Optional Full b = b
Optional Shrunk b = () Optional Shrunk b = ()
...@@ -64,6 +64,8 @@ data MapResult (kind :: ResultKind) = MapResult ...@@ -64,6 +64,8 @@ data MapResult (kind :: ResultKind) = MapResult
-- ^ badges that can be found on this map -- ^ badges that can be found on this map
, mapresultCWs :: [Text] , mapresultCWs :: [Text]
-- ^ collected CWs that apply to this map -- ^ collected CWs that apply to this map
, mapresultJitsis :: [Text]
-- ^ all jitsi room slugs mentioned in this map
, mapresultGeneral :: [Hint] , mapresultGeneral :: [Hint]
-- ^ general-purpose lints that didn't fit anywhere else -- ^ general-purpose lints that didn't fit anywhere else
} deriving (Generic) } deriving (Generic)
...@@ -105,9 +107,8 @@ shrinkMapResult !res = res { mapresultAdjusted = () } ...@@ -105,9 +107,8 @@ shrinkMapResult !res = res { mapresultAdjusted = () }
-- layers upwards in the file hierarchy -- layers upwards in the file hierarchy
loadAndLintMap :: LintConfig' -> FilePath -> Int -> IO (Maybe (MapResult Full)) loadAndLintMap :: LintConfig' -> FilePath -> Int -> IO (Maybe (MapResult Full))
loadAndLintMap config path depth = loadTiledmap path <&> \case loadAndLintMap config path depth = loadTiledmap path <&> \case
Left err -> Just (MapResult mempty mempty mempty mempty Nothing mempty mempty Left err -> Just (MapResult mempty mempty mempty mempty Nothing mempty mempty mempty
[ Hint Fatal . toText $ [ Hint Fatal . toText $ "Fatal: " <> err
path <> ": Fatal: " <> err
]) ])
Right waMap -> Right waMap ->
Just (runLinter (takeFileName path == "main.json") config waMap depth) Just (runLinter (takeFileName path == "main.json") config waMap depth)
...@@ -127,6 +128,8 @@ runLinter isMain config@LintConfig{..} tiledmap depth = MapResult ...@@ -127,6 +128,8 @@ runLinter isMain config@LintConfig{..} tiledmap depth = MapResult
, mapresultProvides = concatMap resultToOffers layer , mapresultProvides = concatMap resultToOffers layer
, mapresultAdjusted = Just adjustedMap , mapresultAdjusted = Just adjustedMap
, mapresultCWs = resultToCWs generalResult , mapresultCWs = resultToCWs generalResult
, mapresultJitsis = concatMap resultToJitsis tileset
<> concatMap resultToJitsis layer
, mapresultBadges = concatMap resultToBadges layer , mapresultBadges = concatMap resultToBadges layer
<> resultToBadges generalResult <> resultToBadges generalResult
} }
......
...@@ -8,13 +8,13 @@ module Dirgraph where ...@@ -8,13 +8,13 @@ module Dirgraph where
import Universum import Universum
import CheckMap (MapResult (mapresultDepends)) import CheckMap (MapResult (mapresultDepends))
import Data.Map.Strict (mapMaybeWithKey, mapWithKey, traverseWithKey) import Data.Map.Strict (mapMaybeWithKey, mapWithKey)
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Data.Set ((\\)) import Data.Set ((\\))
import qualified Data.Set as S import qualified Data.Set as S
import Paths (normalise) import Paths (normalise)
import Text.Dot (Dot, (.->.))
import qualified Text.Dot as D import qualified Text.Dot as D
import Text.Dot (Dot, (.->.))
import Types (Dep (LocalMap)) import Types (Dep (LocalMap))
-- | a simple directed graph -- | a simple directed graph
......
...@@ -40,7 +40,7 @@ module LintWriter ...@@ -40,7 +40,7 @@ module LintWriter
, lintConfig , lintConfig
-- * adjust the linter's context -- * adjust the linter's context
, adjust , adjust
,offersCWs,resultToCWs) where ,offersCWs,resultToCWs,offersJitsi,resultToJitsis) where
import Universum import Universum
...@@ -126,6 +126,9 @@ resultToCWs :: LintResult a -> [Text] ...@@ -126,6 +126,9 @@ resultToCWs :: LintResult a -> [Text]
resultToCWs (LinterState a) = fold $ mapMaybe lintToCW $ fst a resultToCWs (LinterState a) = fold $ mapMaybe lintToCW $ fst a
where lintToCW = \case (CW cw) -> Just cw; _ -> Nothing where lintToCW = \case (CW cw) -> Just cw; _ -> Nothing
resultToJitsis :: LintResult a -> [Text]
resultToJitsis (LinterState a) = mapMaybe lintToJitsi $ fst a
where lintToJitsi = \case (Jitsi room) -> Just room; _ -> Nothing
-- | convert a lint result into a flat list of lints -- | convert a lint result into a flat list of lints
resultToLints :: LintResult a -> [Lint] resultToLints :: LintResult a -> [Lint]
...@@ -160,6 +163,9 @@ offersBadge badge = tell' $ Badge badge ...@@ -160,6 +163,9 @@ offersBadge badge = tell' $ Badge badge
offersCWs :: [Text] -> LintWriter a offersCWs :: [Text] -> LintWriter a
offersCWs = tell' . CW offersCWs = tell' . CW
offersJitsi :: Text -> LintWriter a
offersJitsi = tell' . Jitsi
-- | get the context as it was initially, without any modifications -- | get the context as it was initially, without any modifications
askContext :: LintWriter' a a askContext :: LintWriter' a a
......
...@@ -37,8 +37,9 @@ import LayerData (Collision, layerOverlaps) ...@@ -37,8 +37,9 @@ import LayerData (Collision, layerOverlaps)
import LintConfig (LintConfig (..)) import LintConfig (LintConfig (..))
import LintWriter (LintWriter, adjust, askContext, import LintWriter (LintWriter, adjust, askContext,
askFileDepth, complain, dependsOn, forbid, askFileDepth, complain, dependsOn, forbid,
lintConfig, offersBadge, offersEntrypoint, lintConfig, offersBadge, offersCWs,
suggest, warn, zoom, offersCWs) offersEntrypoint, offersJitsi, suggest,
warn, zoom)
import Paths (PathResult (..), RelPath (..), import Paths (PathResult (..), RelPath (..),
getExtension, isOldStyle, parsePath) getExtension, isOldStyle, parsePath)
import Types (Dep (Link, Local, LocalMap, MapLink)) import Types (Dep (Link, Local, LocalMap, MapLink))
...@@ -363,9 +364,12 @@ checkTileThing removeExits p@(Property name _value) = case name of ...@@ -363,9 +364,12 @@ checkTileThing removeExits p@(Property name _value) = case name of
suggestProperty $ Property "jitsiTrigger" "onaction" suggestProperty $ Property "jitsiTrigger" "onaction"
-- prevents namespace clashes for jitsi room names -- prevents namespace clashes for jitsi room names
unless ("shared" `isPrefixOf` jitsiRoom) $ do if not ("shared" `isPrefixOf` jitsiRoom) then do
assemblyname <- lintConfig configAssemblyTag assemblyname <- lintConfig configAssemblyTag
setProperty "jitsiRoom" (assemblyname <> "-" <> jitsiRoom) setProperty "jitsiRoom" (assemblyname <> "-" <> jitsiRoom)
offersJitsi (assemblyname <> "-" <> jitsiRoom)
else
offersJitsi jitsiRoom
"jitsiTrigger" -> do "jitsiTrigger" -> do
isString p isString p
unlessHasProperty "jitsiTriggerMessage" unlessHasProperty "jitsiTriggerMessage"
...@@ -430,7 +434,7 @@ checkTileThing removeExits p@(Property name _value) = case name of ...@@ -430,7 +434,7 @@ checkTileThing removeExits p@(Property name _value) = case name of
complain $ complain $
"Old-Style inter-repository links (using {<placeholder>}) \ "Old-Style inter-repository links (using {<placeholder>}) \
\cannot be used at "<>eventslug<>"; please use world:// \ \cannot be used at "<>eventslug<>"; please use world:// \
\instead (see https://di.c3voc.de/howto:world)." \instead (see the howto)."
| ext == "tmx" -> | ext == "tmx" ->
complain "Cannot use .tmx map format; use Tiled's json export instead." complain "Cannot use .tmx map format; use Tiled's json export instead."
| ext /= "json" -> | ext /= "json" ->
......
...@@ -54,8 +54,8 @@ instance HasArguments Level where ...@@ -54,8 +54,8 @@ instance HasArguments Level where
-- | a hint comes with an explanation (and a level), or is a dependency -- | a hint comes with an explanation (and a level), or is a dependency
-- (in which case it'll be otherwise treated as an info hint) -- (in which case it'll be otherwise treated as an info hint)
data Lint = Depends Dep | Offers Text | Lint Hint | Badge Badge | CW [Text] data Lint = Depends Dep | Offers Text | Lint Hint | Badge Badge | CW [Text] | Jitsi Text
deriving (Ord, Eq, Generic, ToJSONKey) deriving (Ord, Eq, Generic)
data Dep = Local RelPath | Link Text | MapLink Text | LocalMap RelPath data Dep = Local RelPath | Link Text | MapLink Text | LocalMap RelPath
deriving (Generic, Ord, Eq, NFData) deriving (Generic, Ord, Eq, NFData)
...@@ -78,35 +78,35 @@ lintLevel _ = Info ...@@ -78,35 +78,35 @@ lintLevel _ = Info
lintsToHints :: [Lint] -> [Hint] lintsToHints :: [Lint] -> [Hint]
lintsToHints = mapMaybe (\case {Lint hint -> Just hint ; _ -> Nothing}) lintsToHints = mapMaybe (\case {Lint hint -> Just hint ; _ -> Nothing})
instance PrettyPrint Lint where -- instance PrettyPrint Lint where
prettyprint (Lint Hint { hintMsg, hintLevel } ) = -- prettyprint (Lint Hint { hintMsg, hintLevel } ) =
" " <> show hintLevel <> ": " <> hintMsg -- " " <> show hintLevel <> ": " <> hintMsg
prettyprint (Depends dep) = -- prettyprint (Depends dep) =
" Info: found dependency: " <> prettyprint dep -- " Info: found dependency: " <> prettyprint dep
prettyprint (Offers dep) = -- prettyprint (Offers dep) =
" Info: map offers entrypoint " <> prettyprint dep -- " Info: map offers entrypoint " <> prettyprint dep
prettyprint (Badge _) = -- prettyprint (Badge _) =
" Info: found a badge." -- " Info: found a badge."
prettyprint (CW cws) = -- prettyprint (CW cws) =
" CWs: " <> show cws -- " CWs: " <> show cws
instance PrettyPrint Hint where instance PrettyPrint Hint where
prettyprint (Hint level msg) = " " <> show level <> ": " <> msg prettyprint (Hint level msg) = " " <> show level <> ": " <> msg
instance ToJSON Lint where -- instance ToJSON Lint where
toJSON (Lint h) = toJSON h -- toJSON (Lint h) = toJSON h
toJSON (Depends dep) = A.object -- toJSON (Depends dep) = A.object
[ "msg" .= prettyprint dep -- [ "msg" .= prettyprint dep
, "level" .= A.String "Dependency Info" ] -- , "level" .= A.String "Dependency Info" ]
toJSON (Offers l) = A.object -- toJSON (Offers l) = A.object
[ "msg" .= prettyprint l -- [ "msg" .= prettyprint l
, "level" .= A.String "Entrypoint Info" ] -- , "level" .= A.String "Entrypoint Info" ]
toJSON (Badge _) = A.object -- toJSON (Badge _) = A.object
[ "msg" .= A.String "found a badge" -- [ "msg" .= A.String "found a badge"
, "level" .= A.String "Badge Info"] -- , "level" .= A.String "Badge Info"]
toJSON (CW cws) = A.object -- toJSON (CW cws) = A.object
[ "msg" .= A.String "Content Warning" -- [ "msg" .= A.String "Content Warning"
, "level" .= A.String "CW Info" ] -- , "level" .= A.String "CW Info" ]
instance ToJSON Hint where instance ToJSON Hint where
toJSON (Hint l m) = A.object toJSON (Hint l m) = A.object
......
...@@ -15,8 +15,8 @@ import Universum ...@@ -15,8 +15,8 @@ import Universum
import Data.Aeson as Aeson import Data.Aeson as Aeson
import qualified Data.Set as S import qualified Data.Set as S
import qualified Data.Text as T import qualified Data.Text as T
import Data.Tiled (Layer (layerData), PropertyValue (..), import Data.Tiled (Layer, PropertyValue (..), Tileset (tilesetName),
Tileset (tilesetName), layerName, mkTiledId) layerName)
-- | helper function to create proxies -- | helper function to create proxies
mkProxy :: a -> Proxy a mkProxy :: a -> Proxy a
......
...@@ -18,8 +18,8 @@ import Paths (normalise) ...@@ -18,8 +18,8 @@ import Paths (normalise)
import System.Directory.Extra (copyFile, createDirectoryIfMissing, import System.Directory.Extra (copyFile, createDirectoryIfMissing,
doesDirectoryExist) doesDirectoryExist)
import System.Exit (ExitCode (..)) import System.Exit (ExitCode (..))
import System.FilePath (takeDirectory)
import qualified System.FilePath as FP import qualified System.FilePath as FP
import System.FilePath (takeDirectory)
import System.FilePath.Posix ((</>)) import System.FilePath.Posix ((</>))
import Types (Dep (Local)) import Types (Dep (Local))
......