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
|
# walint: lint & adjust workadventure maps
`walint` is intended as a simple linter that will check workadventure maps for
common errors, such as non-existent map entrypoints or missing asset files, and
additionally suggest changes to improve accessability.
Optionally, it can also *adjust* maps — e.g. to automatically insert property
values or help enforce an event's map policies — and then write them out again,
copying all needed assets and minifying the map's json. This is used to simulate
a `bbbRoom` property (via `openWebsite`), collect and remove badge tokens before
maps are published, and to resolve special-schema URIs (e.g. `world://`).
## Installing
### From the CI pipeline
Gitlab [automatically builds a version](https://git.cccv.de/hub/walint/-/jobs)
of `walint` each time something is pushed to this repository. The resulting
binary should work fine on most linux systems, especially if they're vaguely
debian-like.
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.
### Build using stack
This uses a lockfile to pin versions of dependencies (as well as `ghc`, the haskell
compiler). You will need [stack](https://docs.haskellstack.org/en/stable/README/).
Then just run
```
stack build
```
If you lack `ghc` in the correct version and don't know how to install it, you can
pass it `--install-ghc` to take care of that for you (note that on Nix, `stack` may
automatically use a fitting `ghc` derivation if it finds one).
To install into your `$PATH`, use
```
stack install
```
Alternatively, run `walint` via stack:
```
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, which will make it slower to start.
### Build using cabal
Note that this does not pin dependencies, and `walint` currently does not even
define semver ranges to ensure it compiles at all! Even so, you can use
[cabal](https://www.haskell.org/cabal/) if for some reason you absolutely must,
as long as your package list is sufficiently recent.
Run:
```
cabal update
cabal build
```
Note that `cabal` might decide to pull in an older version of Aeson which is
still vulnerable to hash flooding; in that case `walint` will print a warning
on startup.
## Usage
``` sh
walint --config-file config.json --repository path \
[--out path] [--json] [--pretty] [--entrypoint main.json]
```
### Options
- `--repository`: path to a map repository
- `--entrypoint`: entrypoint of a map repository, i.e. a tiled map in its root
directory. `walint` will lint all maps reachable from it. If not given, it
defaults to `main.json`
- `--lintLevel`: limit output only to messages that have at most the given
level. Valid levels are `Info`, `Suggestion`, `Warning`, `Forbidden`, `Error`,
`Fatal`. Defaults to `Suggestion` if not given.
- `--json`: print output as json instead of the normal more human-friendly format
- `--pretty`: if used with `--json`, insert line breaks and indentation to make
the output more readable. Otherwise no effect.
- `--out path`: write the linted & adjusted repository to the given path. Any
json written to this path will be minimised, and *only those maps and assets
which are reachable from the entrypoint* will be writen at all. If not given,
`walint` will only run the linter and do nothing else. If `walint` encounters
any references to local map assets which are not present in the repository,
or if the map generates lints above the maximum lint level, it will instead
exit with code 1 without touching the output path at all.
- `--config-file file`: path to a configuation file. Required (for now).
- `--config json`: takes a string which should contain a json object conforming
to the same schema as the configuration file, except that all keys are
optional. Keys given here will override whatever values are set in the
configuration file.
## Configuation
Take a look at `config.json` for an example. Most keys are required, and do not
have default values. In `config.json`, all possible keys are given.
Most options should be reasonably self-explanatory. Note that `MaxLintLevel`
differs from the option `--lintLevel`: the latter merely determines what is
*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
copy it to the path given to `--out`.
### Uri Schemas
`walint` supports (limited) rewriting of URIs contained in the map json via
the `UriSchemas` option, which takes a map from uri schemas to a rule describing
what to do with such links, depending on the scope in which they appear.
`walint` takes a very reductive view or URIs: `schema://domain/tail`
#### Rewrite Rules
For now there are three types of such rules:
- `schema: {"scope":[scopes]}`: if in a scope listed in `scopes`, allow any
links of the given `schema`
- `schema: {"scope":[scopes], "allowed":[allowed], "blocked":[blocked], "prefix":prefix}`:
if in a scope listed in `scopes`, prefix any URIs of the given `schema` with
the given `prefix`, unless the URI's domain occurs in `allowed` (in which case
leave it untouched), or it occurs in `blocked`, in which
case it will be rejected as a lint error.
- `schema: {"scope":[scopes], "subst":{domain: prefix, ...}}`: if in a scope
listed in `scopes` and given a URI with the domain `domain`, concatenate
`prefix` with the tail of this URI.
In case an URI is encountered and there is no applicable rule, it will be rejected
(note that this means you'll have to explicitly allow `https://` for links!)
There are currently four possible scopes: `map` applies to tiled map links
(i.e. `exitUrl`), `website` to `openWebsite`, `audio` to `playAudio`, and
`bbb` to Big Blue Button rooms (though that last one may be changed again,
depending on the bbb deployment in use).
## Output
By default `walint` prints lints in a hopefully human-readable manner. Its exit
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
to the path passed to `--out`. If the path given to `--out` already exists,
`walint` will print its normal output but refuse to write anything to that path,
and exit with code 2.
If the `--json` option is given, output to stdout will be printed as json, which
should conform to the following schema (here defined in a quasi-haskell syntax):
```haskell
-- | The main output type. All json output will conform to it.
type Output =
{ severity :: Level
-- ^ the maximum Lint level that occurred
, badges :: List Badge
-- ^ a list of badges occurring in any of the maps
, result :: Result
-- ^ detailed lints in a structured way
, resultText :: String
-- ^ all lints in a human-readable text block
}
-- | A detailed description of which errors occurred
type Result =
{ mapLints :: Map FilePath MapLint
-- ^ an object of per-map lints. Each key is a filepath from the repository's root
, missingAssets :: List Asset
-- ^ a list of missing assets
, missingDeps :: List Entrypoint
-- ^ a list of other missing dependencies (for now, just entrypoints)
}
-- | An object containing map lints
type MapLint =
{ general :: List Lint
-- ^ general lints (most often empty)
, layer :: Map Message Where
-- ^ an object of per-layer lints. Each key is a lint message
, tileset :: Map Message Where
-- ^ an object of per-tileset lints. Again, each key is a lint message
}
-- | Further desription of a single lint
type Where =
{ in :: List String
-- ^ where did this lint occur? (list of layers / tileset names)
, level :: Level
-- ^ what is this lint's level?
}
-- | Valid lint levels. Encoded as strings, listed in ascending order here.
data Level = Info | Suggestion | Warning | Forbidden | Error | Fatal
-- | description of a single (missing) asset
type Asset =
{ asset :: FilePath
-- ^ the filename, as referenced somewhere within the repository
, neededBy :: List FilePath
-- ^ list of filenames of maps which reference this asset
}
-- | description of a single (missing) entrypoint
type Entrypoint =
{ entrypoint :: String
-- ^ the entrypoint, as a string, e.g. path/to/map#entrypoint
, neededBy :: List FilePath
-- ^ list of filenames of maps which reference this entrypoint
}
-- | Lints that don't come grouped by place (for now, just those in generalLints)
type Lint =
{ level :: Level
-- ^ this lint's level
, msg :: String
-- ^ a human-readable (single-line) message
}
type Badge =
{ type :: AreaType
-- ^ type of the badge's area
, token :: String
-- ^ this badge's token
, x :: Number
-- ^ x position on the map
, y :: Number
-- ^ y position on the map
, width :: Maybe Number
-- ^ width of the rectangle/ellipse (not present if type=point)
, height :: Maybe Number
-- ^ height of the rectangle/ellipse (not present if type=point)
}
-- | types of "areas" for badges, encoded as lower-cased strings
data AreaType = Point | Rectangle | Ellipse
```
|