general cleanup
Some checks failed
Build and publish the docker image / build (push) Failing after 13s

Signed-off-by: ngn <ngn@ngn.tf>
This commit is contained in:
ngn 2025-01-21 04:58:44 +03:00
parent 7350155d2d
commit 62183646ae
Signed by: ngn
GPG Key ID: A3654DF5AD9F641D
18 changed files with 152 additions and 457 deletions

View File

@ -0,0 +1,28 @@
name: Build and publish the docker image
on:
push:
branches: ["custom"]
env:
REGISTRY: git.ngn.tf
IMAGE: ${{gitea.repository}}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: "https://github.com/actions/checkout@v4"
- name: Login to container repo
uses: "https://github.com/docker/login-action@v1"
with:
registry: ${{env.REGISTRY}}
username: ${{gitea.actor}}
password: ${{secrets.PACKAGES_TOKEN}}
- name: Build image
run: |
docker build . --tag ${{env.REGISTRY}}/${{env.IMAGE}}:latest
docker push ${{env.REGISTRY}}/${{env.IMAGE}}:latest

3
.github/FUNDING.yml vendored
View File

@ -1,3 +0,0 @@
github: zedeus
liberapay: zedeus
patreon: nitter

View File

@ -1,60 +0,0 @@
name: Build and Publish Docker
on:
push:
branches: ["master"]
paths-ignore: ["README.md"]
pull_request:
branches: ["master"]
paths-ignore: ["README.md"]
env:
IMAGE_NAME: ${{ github.repository }}
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v2
with:
platforms: arm64
- name: Setup Docker buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Log in to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/${{ env.IMAGE_NAME }}
- name: Build and push all platforms Docker image
id: build-and-push
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -1,56 +0,0 @@
name: Tests
on:
push:
paths-ignore:
- "*.md"
branches-ignore:
- master
workflow_call:
jobs:
test:
runs-on: buildjet-2vcpu-ubuntu-2204
strategy:
matrix:
nim:
- "1.6.10"
- "1.6.x"
- "2.0.x"
- "devel"
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Cache nimble
id: cache-nimble
uses: buildjet/cache@v3
with:
path: ~/.nimble
key: ${{ matrix.nim }}-nimble-${{ hashFiles('*.nimble') }}
restore-keys: |
${{ matrix.nim }}-nimble-
- uses: actions/setup-python@v4
with:
python-version: "3.10"
cache: "pip"
- uses: jiro4989/setup-nim-action@v1
with:
nim-version: ${{ matrix.nim }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
- run: nimble build -d:release -Y
- run: pip install seleniumbase
- run: seleniumbase install chromedriver
- uses: supercharge/redis-github-action@1.5.0
- name: Prepare Nitter
run: |
sudo apt install libsass-dev -y
cp nitter.example.conf nitter.conf
sed -i 's/enableDebug = false/enableDebug = true/g' nitter.conf
nimble md
nimble scss
echo '${{ secrets.GUEST_ACCOUNTS }}' > ./guest_accounts.jsonl
- name: Run tests
run: |
./nitter &
pytest -n8 tests

20
.gitignore vendored
View File

@ -1,14 +1,16 @@
nitter
*.html
*.db
/tests/__pycache__
/tests/geckodriver.log
/tests/downloaded_files
/tests/latest_logs
/tools/gencss
/tools/rendermd
/public/css/style.css
/public/md/*.html
data
tests/__pycache__
tests/geckodriver.log
tests/downloaded_files
tests/latest_logs
tools/gencss
tools/rendermd
public/css/style.css
public/md/*.html
nitter.conf
guest_accounts.json*
compose.yml
accounts.*
dump.rdb

View File

@ -1,25 +1,26 @@
FROM nimlang/nim:2.0.0-alpine-regular as nim
LABEL maintainer="setenforce@protonmail.com"
FROM nimlang/nim:2.0.0-alpine-regular as build
RUN apk --no-cache add libsass-dev pcre
WORKDIR /src/nitter
WORKDIR /src
COPY nitter.nimble .
RUN nimble install -y --depsOnly
COPY . .
RUN nimble build -d:danger -d:lto -d:strip \
&& nimble scss \
&& nimble md
RUN nimble build -d:danger -d:lto -d:strip
RUN nimble scss
RUN nimble md
FROM alpine:latest
WORKDIR /src/
RUN apk --no-cache add pcre ca-certificates
COPY --from=nim /src/nitter/nitter ./
COPY --from=nim /src/nitter/nitter.example.conf ./nitter.conf
COPY --from=nim /src/nitter/public ./public
EXPOSE 8080
RUN adduser -h /src/ -D -s /bin/sh nitter
RUN useradd -d /src -u 1001 nitter
WORKDIR /srv
COPY --from=build /srv/nitter ./
COPY --from=build /srv/public ./public
USER nitter
CMD ./nitter

View File

@ -1,25 +0,0 @@
FROM alpine:3.18 as nim
LABEL maintainer="setenforce@protonmail.com"
RUN apk --no-cache add libsass-dev pcre gcc git libc-dev "nim=1.6.14-r0" "nimble=0.13.1-r2"
WORKDIR /src/nitter
COPY nitter.nimble .
RUN nimble install -y --depsOnly
COPY . .
RUN nimble build -d:danger -d:lto -d:strip \
&& nimble scss \
&& nimble md
FROM alpine:3.18
WORKDIR /src/
RUN apk --no-cache add pcre ca-certificates openssl1.1-compat
COPY --from=nim /src/nitter/nitter ./
COPY --from=nim /src/nitter/nitter.example.conf ./nitter.conf
COPY --from=nim /src/nitter/public ./public
EXPOSE 8080
RUN adduser -h /src/ -D -s /bin/sh nitter
USER nitter
CMD ./nitter

199
README.md
View File

@ -1,198 +1,5 @@
# Nitter
# [ngn.tf] | nitter
[![Test Matrix](https://github.com/zedeus/nitter/workflows/Tests/badge.svg)](https://github.com/zedeus/nitter/actions/workflows/run-tests.yml)
[![Test Matrix](https://github.com/zedeus/nitter/workflows/Docker/badge.svg)](https://github.com/zedeus/nitter/actions/workflows/build-docker.yml)
[![License](https://img.shields.io/github/license/zedeus/nitter?style=flat)](#license)
![](https://git.ngn.tf/ngn/nitter/actions/workflows/build.yml/badge.svg)
A free and open source alternative Twitter front-end focused on privacy and
performance. \
Inspired by the [Invidious](https://github.com/iv-org/invidious)
project.
- No JavaScript or ads
- All requests go through the backend, client never talks to Twitter
- Prevents Twitter from tracking your IP or JavaScript fingerprint
- Uses Twitter's unofficial API (no rate limits or developer account required)
- Lightweight (for [@nim_lang](https://nitter.net/nim_lang), 60KB vs 784KB from twitter.com)
- RSS feeds
- Themes
- Mobile support (responsive design)
- AGPLv3 licensed, no proprietary instances permitted
Liberapay: https://liberapay.com/zedeus \
Patreon: https://patreon.com/nitter \
BTC: bc1qp7q4qz0fgfvftm5hwz3vy284nue6jedt44kxya \
ETH: 0x66d84bc3fd031b62857ad18c62f1ba072b011925 \
LTC: ltc1qhsz5nxw6jw9rdtw9qssjeq2h8hqk2f85rdgpkr \
XMR: 42hKayRoEAw4D6G6t8mQHPJHQcXqofjFuVfavqKeNMNUZfeJLJAcNU19i1bGdDvcdN6romiSscWGWJCczFLe9RFhM3d1zpL
## Roadmap
- Embeds
- Account system with timeline support
- Archiving tweets/profiles
- Developer API
## New Features
- Likes tab
## Resources
The wiki contains
[a list of instances](https://github.com/zedeus/nitter/wiki/Instances) and
[browser extensions](https://github.com/zedeus/nitter/wiki/Extensions)
maintained by the community.
## Why?
It's impossible to use Twitter without JavaScript enabled. For privacy-minded
folks, preventing JavaScript analytics and IP-based tracking is important, but
apart from using a VPN and uBlock/uMatrix, it's impossible. Despite being behind
a VPN and using heavy-duty adblockers, you can get accurately tracked with your
[browser's fingerprint](https://restoreprivacy.com/browser-fingerprinting/),
[no JavaScript required](https://noscriptfingerprint.com/). This all became
particularly important after Twitter [removed the
ability](https://www.eff.org/deeplinks/2020/04/twitter-removes-privacy-option-and-shows-why-we-need-strong-privacy-laws)
for users to control whether their data gets sent to advertisers.
Using an instance of Nitter (hosted on a VPS for example), you can browse
Twitter without JavaScript while retaining your privacy. In addition to
respecting your privacy, Nitter is on average around 15 times lighter than
Twitter, and in most cases serves pages faster (eg. timelines load 2-4x faster).
In the future a simple account system will be added that lets you follow Twitter
users, allowing you to have a clean chronological timeline without needing a
Twitter account.
## Screenshot
![nitter](/screenshot.png)
## Installation
### Dependencies
- libpcre
- libsass
- redis
To compile Nitter you need a Nim installation, see
[nim-lang.org](https://nim-lang.org/install.html) for details. It is possible to
install it system-wide or in the user directory you create below.
To compile the scss files, you need to install `libsass`. On Ubuntu and Debian,
you can use `libsass-dev`.
Redis is required for caching and in the future for account info. It should be
available on most distros as `redis` or `redis-server` (Ubuntu/Debian).
Running it with the default config is fine, Nitter's default config is set to
use the default Redis port and localhost.
Here's how to create a `nitter` user, clone the repo, and build the project
along with the scss and md files.
```bash
# useradd -m nitter
# su nitter
$ git clone https://github.com/zedeus/nitter
$ cd nitter
$ nimble build -d:release
$ nimble scss
$ nimble md
$ cp nitter.example.conf nitter.conf
```
Edit `twitter_oauth.sh` with your Twitter account name and password.
```
$ ./twitter_oauth.sh | tee -a guest_accounts.jsonl
```
Set your hostname, port, HMAC key, https (must be correct for cookies), and
Redis info in `nitter.conf`. To run Redis, either run
`redis-server --daemonize yes`, or `systemctl enable --now redis` (or
redis-server depending on the distro). Run Nitter by executing `./nitter` or
using the systemd service below. You should run Nitter behind a reverse proxy
such as [Nginx](https://github.com/zedeus/nitter/wiki/Nginx) or
[Apache](https://github.com/zedeus/nitter/wiki/Apache) for security and
performance reasons.
### Docker
Page for the Docker image: https://hub.docker.com/r/zedeus/nitter
#### NOTE: For ARM64 support, please use the separate ARM64 docker image: [`zedeus/nitter:latest-arm64`](https://hub.docker.com/r/zedeus/nitter/tags).
To run Nitter with Docker, you'll need to install and run Redis separately
before you can run the container. See below for how to also run Redis using
Docker.
To build and run Nitter in Docker:
```bash
docker build -t nitter:latest .
docker run -v $(pwd)/nitter.conf:/src/nitter.conf -d --network host nitter:latest
```
Note: For ARM64, use this Dockerfile: [`Dockerfile.arm64`](https://github.com/zedeus/nitter/blob/master/Dockerfile.arm64).
A prebuilt Docker image is provided as well:
```bash
docker run -v $(pwd)/nitter.conf:/src/nitter.conf -d --network host zedeus/nitter:latest
```
Using docker-compose to run both Nitter and Redis as different containers:
Change `redisHost` from `localhost` to `nitter-redis` in `nitter.conf`, then run:
```bash
docker-compose up -d
```
Note the Docker commands expect a `nitter.conf` file in the directory you run
them.
### systemd
To run Nitter via systemd you can use this service file:
```ini
[Unit]
Description=Nitter (An alternative Twitter front-end)
After=syslog.target
After=network.target
[Service]
Type=simple
# set user and group
User=nitter
Group=nitter
# configure location
WorkingDirectory=/home/nitter/nitter
ExecStart=/home/nitter/nitter/nitter
Restart=always
RestartSec=15
[Install]
WantedBy=multi-user.target
```
Then enable and run the service:
`systemctl enable --now nitter.service`
### Logging
Nitter currently prints some errors to stdout, and there is no real logging
implemented. If you're running Nitter with systemd, you can check stdout like
this: `journalctl -u nitter.service` (add `--follow` to see just the last 15
lines). If you're running the Docker image, you can do this:
`docker logs --follow *nitter container id*`
## Contact
Feel free to join our [Matrix channel](https://matrix.to/#/#nitter:matrix.org).
You can email me at zedeus@pm.me if you wish to contact me personally.
A fork of the [libmedium](https://github.com/PrivacyDevel/nitter) project, with my personal changes.

32
compose.example.yml Normal file
View File

@ -0,0 +1,32 @@
services:
nitter:
container_name: nitter
image: git.ngn.tf/ngn/nitter
ports:
- 80:8080
volumes:
- ./nitter.conf:/srv/nitter.conf:Z,ro
- ./accounts.jsonl:/srv/accounts.jsonl:Z,ro
depends_on:
- nitter-redis
restart: unless-stopped
user: 998:998
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
nitter_redis:
container_name: nitter_redis
image: redis:6-alpine
command: redis-server --save 60 1 --loglevel warning
volumes:
- ./data:/data
restart: unless-stopped
user: 999:1000
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true

View File

@ -1,47 +0,0 @@
version: "3"
services:
nitter:
image: ghcr.io/privacydevel/nitter:master
container_name: nitter
ports:
- "127.0.0.1:8080:8080" # Replace with "8080:8080" if you don't use a reverse proxy
volumes:
- ./nitter.conf:/src/nitter.conf:Z,ro
depends_on:
- nitter-redis
restart: unless-stopped
healthcheck:
test: wget -nv --tries=1 --spider http://127.0.0.1:8080/Jack/status/20 || exit 1
interval: 30s
timeout: 5s
retries: 2
user: "998:998"
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
nitter-redis:
image: redis:6-alpine
container_name: nitter-redis
command: redis-server --save 60 1 --loglevel warning
volumes:
- nitter-redis:/data
restart: unless-stopped
healthcheck:
test: redis-cli ping
interval: 30s
timeout: 5s
retries: 2
user: "999:1000"
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
volumes:
nitter-redis:

View File

@ -1,4 +1,4 @@
[Server]
[server]
hostname = "nitter.net" # for generating links, change this to your own domain/ip
title = "nitter"
address = "0.0.0.0"
@ -6,8 +6,9 @@ port = 8080
https = false # disable to enable cookies when not using https
httpMaxConnections = 100
staticDir = "./public"
accountsFile = "./accounts.jsonl"
[Cache]
[cache]
listMinutes = 240 # how long to cache list info (not the tweets, so keep it high)
rssMinutes = 10 # how long to cache rss queries
redisHost = "localhost" # Change to "nitter-redis" if using docker-compose
@ -19,7 +20,7 @@ redisMaxConnections = 30
# goes above this, they're closed when released. don't worry about this unless
# you receive tons of requests per second
[Config]
[config]
hmacKey = "secretkey" # random key for cryptographic signing of video urls
base64Media = false # use base64 encoding for proxied media urls
enableRSS = true # set this to false to disable RSS feeds
@ -34,7 +35,7 @@ tokenCount = 10
# major bursts all the time and don't have a rate limiting setup via e.g. nginx
# Change default preferences here, see src/prefs_impl.nim for a complete list
[Preferences]
[preferences]
theme = "Nitter"
replaceTwitter = "nitter.net"
replaceYouTube = "piped.video"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 797 KiB

32
twitter_oauth.sh → scripts/oauth.sh Normal file → Executable file
View File

@ -1,15 +1,16 @@
#!/bin/bash
# Grab oauth token for use with Nitter (requires Twitter account).
#!/bin/bash -e
# Grab oauth token for use with Nitter (requires Twitter account).
# results: {"oauth_token":"xxxxxxxxxx-xxxxxxxxx","oauth_token_secret":"xxxxxxxxxxxxxxxxxxxxx"}
username=""
password=""
if [[ -z "$username" || -z "$password" ]]; then
echo "needs username and password"
if [ $# -ne 2 ]; then
echo "please specify a username and password"
exit 1
fi
username="${1}"
password="${2}"
bearer_token='AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F'
guest_token=$(curl -s -XPOST https://api.twitter.com/1.1/guest/activate.json -H "Authorization: Bearer ${bearer_token}" | jq -r '.guest_token')
base_url='https://api.twitter.com/1.1/onboarding/task.json'
@ -22,15 +23,30 @@ flow_1=$(curl -si -XPOST "${base_url}?flow_name=login" "${header[@]}")
att=$(sed -En 's/^att: (.*)\r/\1/p' <<< "${flow_1}")
flow_token=$(sed -n '$p' <<< "${flow_1}" | jq -r .flow_token)
if [[ -z "$flow_1" || -z "$flow_token" ]]; then
echo "Couldn't retrieve flow token (twitter not reachable?)"
exit 1
fi
# username
token_2=$(curl -s -XPOST "${base_url}" -H "att: ${att}" "${header[@]}" \
-d '{"flow_token":"'"${flow_token}"'","subtask_inputs":[{"subtask_id":"LoginEnterUserIdentifierSSO","settings_list":{"setting_responses":[{"key":"user_identifier","response_data":{"text_data":{"result":"'"${username}"'"}}}],"link":"next_link"}}]}' | jq -r .flow_token)
if [[ -z "$token_2" || "$token_2" == "null" ]]; then
echo "Couldn't retrieve user token (check if login is correct)"
exit 1
fi
# password
token_3=$(curl -s -XPOST "${base_url}" -H "att: ${att}" "${header[@]}" \
-d '{"flow_token":"'"${token_2}"'","subtask_inputs":[{"enter_password":{"password":"'"${password}"'","link":"next_link"},"subtask_id":"LoginEnterPassword"}]}' | jq -r .flow_token)
if [[ -z "$token_3" || "$token_3" == "null" ]]; then
echo "Couldn't retrieve user token (check if password is correct)"
exit 1
fi
# finally print oauth_token and secret
curl -s -XPOST "${base_url}" -H "att: ${att}" "${header[@]}" \
-d '{"flow_token":"'"${token_3}"'","subtask_inputs":[{"check_logged_in_account":{"link":"AccountDuplicationCheck_false"},"subtask_id":"AccountDuplicationCheck"}]}' | \
jq -c '.subtasks[0]|if(.open_account) then {oauth_token: .open_account.oauth_token, oauth_token_secret: .open_account.oauth_token_secret} else empty end'
jq -c '.subtasks[0]|if(.open_account) then [{oauth_token: .open_account.oauth_token, oauth_token_secret: .open_account.oauth_token_secret}] else empty end'

View File

@ -185,22 +185,22 @@ proc setRateLimit*(account: GuestAccount; api: Api; remaining, reset: int) =
account.apis[api] = RateLimit(remaining: remaining, reset: reset)
proc initAccountPool*(cfg: Config; path: string) =
proc initAccountPool*(cfg: Config) =
let path = cfg.accountsFile
enableLogging = cfg.enableDebug
let jsonlPath = if path.endsWith(".json"): (path & 'l') else: path
if fileExists(jsonlPath):
log "Parsing JSONL guest accounts file: ", jsonlPath
for line in jsonlPath.lines:
accountPool.add parseGuestAccount(line)
elif fileExists(path):
log "Parsing JSON guest accounts file: ", path
accountPool = parseGuestAccounts(path)
else:
echo "[accounts] ERROR: ", path, " not found. This file is required to authenticate API requests."
if !path.endswith(".jsonl"):
log "Accounts file should be formated with JSONL"
quit 1
if !fileExists(path):
log "Failed to access the accounts file (", path, ")"
quit 1
log "Parsing JSONL accounts file: ", path
for line in path.lines:
accountPool.add parseGuestAccount(line)
let accountsPrePurge = accountPool.len
#accountPool.keepItIf(not it.hasExpired)

View File

@ -16,32 +16,33 @@ proc getConfig*(path: string): (Config, parseCfg.Config) =
let conf = Config(
# Server
address: cfg.get("Server", "address", "0.0.0.0"),
port: cfg.get("Server", "port", 8080),
useHttps: cfg.get("Server", "https", true),
httpMaxConns: cfg.get("Server", "httpMaxConnections", 100),
staticDir: cfg.get("Server", "staticDir", "./public"),
title: cfg.get("Server", "title", "Nitter"),
hostname: cfg.get("Server", "hostname", "nitter.net"),
address: cfg.get("server", "address", "0.0.0.0"),
port: cfg.get("server", "port", 8080),
useHttps: cfg.get("server", "https", true),
httpMaxConns: cfg.get("server", "httpMaxConnections", 100),
staticDir: cfg.get("server", "staticDir", "./public"),
accountsFile: cfg.get("server", "accountsFile", "./accounts.jsonl"),
title: cfg.get("server", "title", "Nitter"),
hostname: cfg.get("server", "hostname", "nitter.net"),
# Cache
listCacheTime: cfg.get("Cache", "listMinutes", 120),
rssCacheTime: cfg.get("Cache", "rssMinutes", 10),
listCacheTime: cfg.get("cache", "listMinutes", 120),
rssCacheTime: cfg.get("cache", "rssMinutes", 10),
redisHost: cfg.get("Cache", "redisHost", "localhost"),
redisPort: cfg.get("Cache", "redisPort", 6379),
redisConns: cfg.get("Cache", "redisConnections", 20),
redisMaxConns: cfg.get("Cache", "redisMaxConnections", 30),
redisPassword: cfg.get("Cache", "redisPassword", ""),
redisHost: cfg.get("cache", "redisHost", "localhost"),
redisPort: cfg.get("cache", "redisPort", 6379),
redisConns: cfg.get("cache", "redisConnections", 20),
redisMaxConns: cfg.get("cache", "redisMaxConnections", 30),
redisPassword: cfg.get("cache", "redisPassword", ""),
# Config
hmacKey: cfg.get("Config", "hmacKey", "secretkey"),
base64Media: cfg.get("Config", "base64Media", false),
minTokens: cfg.get("Config", "tokenCount", 10),
enableRss: cfg.get("Config", "enableRSS", true),
enableDebug: cfg.get("Config", "enableDebug", false),
proxy: cfg.get("Config", "proxy", ""),
proxyAuth: cfg.get("Config", "proxyAuth", "")
hmacKey: cfg.get("config", "hmacKey", "secretkey"),
base64Media: cfg.get("config", "base64Media", false),
minTokens: cfg.get("config", "tokenCount", 10),
enableRss: cfg.get("config", "enableRSS", true),
enableDebug: cfg.get("config", "enableDebug", false),
proxy: cfg.get("config", "proxy", ""),
proxyAuth: cfg.get("config", "proxyAuth", "")
)
return (conf, cfg)

View File

@ -16,10 +16,7 @@ import routes/[
const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances"
const issuesUrl = "https://github.com/zedeus/nitter/issues"
let
accountsPath = getEnv("NITTER_ACCOUNTS_FILE", "./guest_accounts.json")
initAccountPool(cfg, accountsPath)
initAccountPool(cfg)
if not cfg.enableDebug:
# Silence Jester's query warning

View File

@ -265,6 +265,7 @@ type
title*: string
hostname*: string
staticDir*: string
accountsFile*: string
hmacKey*: string
base64Media*: bool