Initial public commit
This commit is contained in:
commit
0f3b5a6bac
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
keyring.gpg
|
||||
ignored.txt
|
||||
0_secrets.yaml
|
||||
*~
|
||||
~*
|
||||
.#*
|
9
config.d/1_config.yaml
Normal file
9
config.d/1_config.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
base_url: https://glowers.club/_matrix
|
||||
synapse_base_url: http://nowhere.local/_synapse
|
||||
user_id: '@test:glowers.club'
|
||||
keyring: ./keyring.gpg
|
||||
audit_room: '!GzPMBYkwPFXlIwuYlH:glowers.club'
|
||||
dry_run: true
|
||||
policy_rooms:
|
||||
#- '!GzPMBYkwPFXlIwuYlH:glowers.club'
|
||||
- '!iINqXGsJxTyFNZOCQK:glowers.club'
|
18
config.d/2_action_high_risk_porn.yaml
Normal file
18
config.d/2_action_high_risk_porn.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
actions:
|
||||
- tag: high_risk_porn
|
||||
room:
|
||||
archive:
|
||||
- media
|
||||
- members
|
||||
delist: true
|
||||
shutdown: true
|
||||
silent: true
|
||||
notify: true
|
||||
quarantine: true
|
||||
user:
|
||||
archive:
|
||||
- media
|
||||
- rooms
|
||||
deactivate: true
|
||||
notify: true
|
||||
quarantine: true
|
6
config.d/3_action_nsfw.yaml
Normal file
6
config.d/3_action_nsfw.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
actions:
|
||||
- tag: nsfw
|
||||
room:
|
||||
delist: true
|
||||
shutdown: true
|
||||
reason: 'This room has been closed due to being in violation of our acceptable usage policy.'
|
145
docs/debian_11.md
Normal file
145
docs/debian_11.md
Normal file
@ -0,0 +1,145 @@
|
||||
**NOTE:** This is out of date and will need to be updated.
|
||||
|
||||
# Debian 11 Install Guide for hotpocket
|
||||
The following guide is intended for Debian or debian-like (Ubuntu) distributions. Non-debian users may follow this guide as well, but they may need to perform additional steps during setup.
|
||||
|
||||
## Dependencies
|
||||
Hotpocket requires `curl`, `mktemp`, `gpg`, `jq`, and `yq` to run.
|
||||
|
||||
`mktemp` should already be available on your system. You can install `curl`, `gpg`, and `jq` from the debian repository
|
||||
|
||||
```sh
|
||||
$ apt install curl gpg jq
|
||||
```
|
||||
|
||||
`curl`, `gpg`, and `jq` should now appear in your environment.
|
||||
|
||||
```sh
|
||||
$ which curl gpg jq
|
||||
/usr/bin/curl
|
||||
/usr/bin/gpg
|
||||
/usr/bin/jq
|
||||
```
|
||||
|
||||
To install `yq`, the `yq` developers suggest you use a an ubuntu ppa. You may also install `yq` through `pip3` by running `pip3 install yq`
|
||||
|
||||
```sh
|
||||
$ apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 9a2d61f6bb03ced7522b8e7d6657dbe0cc86bb64
|
||||
$ echo 'http://ppa.launchpad.net/rmescandon/yq/ubuntu focal main' > /etc/apt/sources.list.d/rmescandon-ubuntu-yq-focal.list
|
||||
```
|
||||
|
||||
Running `apt update` should now show the PPA among your sources.
|
||||
|
||||
```
|
||||
$ apt update
|
||||
Get:1 http://ppa.launchpad.net/rmescandon/yq/ubuntu focal InRelease [18.0 kB]
|
||||
Hit:2 http://deb.debian.org/debian bullseye InRelease
|
||||
Hit:3 http://security.debian.org/debian-security bullseye-security InRelease
|
||||
Hit:4 http://deb.debian.org/debian bullseye-updates InRelease
|
||||
Get:5 http://ppa.launchpad.net/rmescandon/yq/ubuntu focal/main amd64 Packages [488 B]
|
||||
Get:6 http://ppa.launchpad.net/rmescandon/yq/ubuntu focal Translation-en [264 B]
|
||||
Fetched 18.8 kB in 1s (29.1 kB/s)
|
||||
```
|
||||
|
||||
You should now be able to install `yq`.
|
||||
|
||||
```sh
|
||||
$ apt install yq
|
||||
```
|
||||
|
||||
Once installed, `yq` should appear in your environment.
|
||||
|
||||
```sh
|
||||
$ which yq
|
||||
/usr/bin/yq
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Create a `hotpocket` user, and a `hotpocket-data` group.
|
||||
|
||||
```sh
|
||||
$ groupadd hotpocket-data -r
|
||||
$ useradd hotpocket -g hotpocket-data -d /etc/hotpocket -s /usr/sbin/nologin -MNr
|
||||
```
|
||||
|
||||
You can check `/etc/passwd` and `/etc/shadow` to make sure that the user is properly configured.
|
||||
|
||||
*The UID of the hotpocket user will likely be different*
|
||||
|
||||
```sh
|
||||
$ cat /etc/passwd | grep hotpocket
|
||||
hotpocket:x:998:998::/etc/hotpocket:/usr/sbin/nologin
|
||||
$ cat /etc/shadow | grep hotpocket
|
||||
hotpocket:!:19020::::::
|
||||
$ groups hotpocket
|
||||
hotpocket : hotpocket-data
|
||||
```
|
||||
|
||||
Next, create the `hotpocket` directory in `etc`.
|
||||
|
||||
```sh
|
||||
$ mkdir /etc/hotpocket
|
||||
$ chown root:hotpocket-data /etc/hotpocket
|
||||
$ chmod 750 /etc/hotpocket
|
||||
```
|
||||
|
||||
Your new directory should look like this:
|
||||
|
||||
```sh
|
||||
$ ls -l /etc | grep hotpocket
|
||||
drwxr-x--- 2 root hotpocket-data 4096 Jan 28 05:50 hotpocket
|
||||
```
|
||||
|
||||
Next, copy in the supplied `config.yaml`, `secrets.yaml`, and `hotpocket.sh`. You do not need to copy `mkpolicy.sh`, you may store that elsewhere.
|
||||
|
||||
```sh
|
||||
$ cd /etc/hotpocket
|
||||
$ cp /mnt/hotpocket/*.yaml /mnt/hotpocket/hotpocket.sh .
|
||||
$ touch keyring.gpg
|
||||
$ chown root:hotpocket-data *
|
||||
$ chmod 640 *
|
||||
$ chmod 650 hotpocket.sh
|
||||
```
|
||||
|
||||
Your file permissions should look like this:
|
||||
|
||||
```sh
|
||||
$ ls -l
|
||||
drw-r----- 1 root hotpocket-data 218 Jan 28 05:52 config.yaml
|
||||
drw-r-x--- 1 root hotpocket-data 5671 Jan 28 05:52 hotpocket.sh
|
||||
drw-r----- 1 root hotpocket-data 0 Jan 28 05:52 keyring.gpg
|
||||
drw-r----- 1 root hotpocket-data 55 Jan 28 05:52 secrets.yaml
|
||||
```
|
||||
|
||||
Next, we're going to want to change some values in `config.yaml` and `secrets.yaml`.
|
||||
|
||||
You'll need to change `base_url`, `synapse_base_url`, and `policy_rooms` to sensible values. Ensure that the `base_url` and `synapse_base_url` do not end with `/`.
|
||||
|
||||
You will also need to create a synapse admin account for hotpocket to use, then to fill in the `access_token` in `secrets.yaml`. Do not include the `"Bearer "` prefix!
|
||||
|
||||
Once done, you can begin setting up your keyring.
|
||||
|
||||
## Keyring setup
|
||||
|
||||
Hotpocket requires policies to be signed, hotpocket uses `gpg` to validate any policies it finds in your defined policy rooms.
|
||||
|
||||
As the user which owns `keyring.gpg` (root in this case), add Jon's public key to the keyring.
|
||||
|
||||
```
|
||||
$ # The hotpocket archive should include the `jon_at_glowers_club.asc` public key.
|
||||
$ gpg --no-default-keyring --keyring "$PWD/keyring.gpg" --import /mnt/hotpocket/jon_at_glowers_club.asc
|
||||
gpg: key 1A4A0CC4CE53281B public key "Jonathan (@jon:glowers.club) <[email protected]>" imported
|
||||
gpg: Total number processed: 1
|
||||
gpg: imported: 1
|
||||
$ gpg --no-default-keyring --keyring "$PWD/keyring.gpg" --list-keys
|
||||
./keyring.gpg
|
||||
-------------
|
||||
pub rsa4096 2022-01-27 [SC] [expires: 2025-01-11]
|
||||
5C5E17B334E084FE822007D71A4A0CC4CE53281B
|
||||
uid [ unknown] Jonathan (@jon:glowers.club) <[email protected]>
|
||||
sub rsa5096 2022-01-27 [E] [expires: 2025-01-11]
|
||||
|
||||
```
|
||||
|
||||
At this stage you may also wish to import your own public key, or the public keys of other policy rooms admins.
|
BIN
docs/hotpocket.png
Normal file
BIN
docs/hotpocket.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.6 KiB |
28
docs/tags.md
Normal file
28
docs/tags.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Standardized tags
|
||||
|
||||
The following tags are defined to better ensure intercompatibility between hotpocket policies deployed by different administrators.
|
||||
|
||||
| Tag | Liability | Description
|
||||
--- | --- | ---
|
||||
| `csam` | Front Door | Child Sexual Abuse Material. i.e: cp
|
||||
| `jailbait` | High <sup title="Jailbait comprises of legal images of minors, such as child models, however the collection of such images is highly suspect. Jailbait rooms tend to include users who trade CSAM. At minimum, these rooms are a heavy liability">?</sup> | Suggestive images of minors, not technically `casm` but contextually borderline.
|
||||
| `beastiality` | High <sup title="See local laws">?</sup> | Pornography with features animals.
|
||||
| `3d_loli` | High <sup title="Questionably legal in the west. As 3D loli tends to be realistically stylised, it may be illegal to posses. See local laws">?</sup> | 3DCG suggestive or explicit depictions of fictional minors.
|
||||
| `loli` | Moderate <sup title="Questionably legal in the west. See local laws">?</sup> | Drawn suggestive or explicit depictions of fictional minors.
|
||||
| `anarchy` | Moderate | Rooms which have no room admins or moderators.
|
||||
| `irl_porn` | Moderate <sup title="Legal with few exceptions, see local laws.">?</sup> | Pornography which features real people.
|
||||
| `drawn_porn` | Low | Pornography which depicts fictional characters.
|
||||
| `gore` | Low | Rooms which exists to host gore.
|
||||
| `commerical` | Low | Rooms which exist exclusively to promote a specific commerical product or group.
|
||||
| `spam` | Low | Rooms which exist primarily to host spam content.
|
||||
|
||||
## Topics:
|
||||
These additional tags apply to groups of tags. See `scripts/schema/topics.yaml`
|
||||
|
||||
| Topic | Description
|
||||
--- | ---
|
||||
| `high_risk_porn` | Similar to `high_risk`, limited to porn.
|
||||
| `high_risk` | Content which has a high risk to host.
|
||||
| `porn` | Any kind of pornagraphic content.
|
||||
| `nsfl` | `gore` + `high_risk`
|
||||
| `nsfw` | `porn` + `nsfl`
|
438
hotpocket.sh
Executable file
438
hotpocket.sh
Executable file
@ -0,0 +1,438 @@
|
||||
#!/usr/bin/env bash
|
||||
VERSION="1.0.0"
|
||||
CONFIG_DIR="$HOTPOCKET_CONFIG_DIR"
|
||||
set -u
|
||||
set +H
|
||||
|
||||
# This will be removed, dont rely on it
|
||||
[ -z "$CONFIG_DIR" ] && echo "main: HOTPOCKET_CONFIG_DIR is not defined, assuming working directory. This is potentially unsafe"
|
||||
[ -z "$CONFIG_DIR" ] && CONFIG_DIR="$PWD"
|
||||
|
||||
SCHEMA_DIR="$CONFIG_DIR/scripts/schema"
|
||||
CONFIGD_DIR="$CONFIG_DIR/config.d"
|
||||
IGNORED_FILE="$CONFIG_DIR/ignored.txt"
|
||||
|
||||
echo "main: Running as $(whoami). Version $VERSION. Directory at $CONFIG_DIR"
|
||||
die () { echo "$@" 1>&2; exit 1; }
|
||||
stripCtrlChars () { sed 's/\x1b/\\x1b/g' /dev/stdin; return $?; }
|
||||
|
||||
[ -d "$CONFIG_DIR" ] || die "main: No hotpocket directory defined! See readme."
|
||||
[ -d "$SCHEMA_DIR" ] || die "main: No schema directory defined! See readme."
|
||||
[ -d "$CONFIGD_DIR" ] || die "main: No config.d directory defined! See readme."
|
||||
[ -f "$IGNORED_FILE" ] || die "main: No ignored.txt exists! You'll need to create one that $(whoami) can write to."
|
||||
|
||||
str_startswith () {
|
||||
# str_startsswith: prefix string
|
||||
local alen
|
||||
local blen
|
||||
local end
|
||||
alen=${#1}
|
||||
blen=${#2}
|
||||
|
||||
if [ "$alen" -gt "$blen" ]; then
|
||||
return 1
|
||||
else
|
||||
if [ "$1" == "${2:0:$alen}" ]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
str_endswith () {
|
||||
# str_endswith: affix string
|
||||
local alen
|
||||
local blen
|
||||
local end
|
||||
alen=${#1}
|
||||
blen=${#2}
|
||||
|
||||
if [ "$alen" -gt "$blen" ]; then
|
||||
return 1
|
||||
else
|
||||
end=$((blen - alen))
|
||||
if [ "$1" == "${2:$end:$alen}" ]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# Merge all files in config.d/
|
||||
createConfig () {
|
||||
# createConfig:
|
||||
local filepath
|
||||
local filebase
|
||||
local json
|
||||
local code
|
||||
local config
|
||||
config="{}"
|
||||
|
||||
for filepath in "$CONFIGD_DIR/"*; do
|
||||
filebase="$(basename "$filepath")"
|
||||
if [ -d "$filepath" ]; then
|
||||
echo "createConfig: Ignoring $filebase, Is a directory." 1>&2
|
||||
elif str_startswith "~" "$filebase" || str_endswith "~" "$filebase"; then
|
||||
echo "createConfig: Ignoring $filebase. Is a temporary file!" 1>&2
|
||||
elif str_startswith "." "$filebase"; then
|
||||
echo "createConfig: Ignoring $filebase. Is a hidden file!" 1>&2
|
||||
elif str_endswith ".yml" "$filebase" || str_endswith ".yaml" "$filebase" || str_endswith ".json" "$filebase"; then
|
||||
json="$(yq . "$filepath" -c)"
|
||||
code="$?"
|
||||
if [ $code -eq 0 ]; then
|
||||
config="$(jq --argjson a "$config" --argjson b "$json" \
|
||||
'$a * $b * {
|
||||
policy_rooms: ($a.policy_rooms + $b.policy_rooms),
|
||||
actions: ($a.actions + $b.actions)
|
||||
}' -nc)" || die "createConfig: Failed to merge $filebase with in-memory config. Exited with code $?"
|
||||
else
|
||||
echo "createConfig: Failed to load $filebase. Exited with code $code" 1>&2
|
||||
exit $code
|
||||
fi
|
||||
else
|
||||
echo "createConfig: Ignoring $(basename "$filebase")." 1>&2
|
||||
fi
|
||||
done
|
||||
#jq . 1>&2 <<< "$config"
|
||||
jq . <<< "$config"
|
||||
}
|
||||
|
||||
CONFIG="$(createConfig)" || die "main: Failed to load config.d! Exited with $?"
|
||||
yajsv -q -s "$SCHEMA_DIR/config.yaml" /dev/stdin <<< "$CONFIG" || die "main: Failed to load config.yaml! Defined config does not validate against schema"
|
||||
|
||||
BASE_URL="$(jq '.base_url' -r <<< "$CONFIG")" || die "main: Failed to load config.yaml! Failed to read base_url"
|
||||
SYNAPSE_BASE_URL="$(jq '.synapse_base_url' -r <<< "$CONFIG")" || die "main: Failed to load config.yaml! Failed to read synapse_base_url"
|
||||
KEYRING_FILE="$(jq '.keyring' -r <<< "$CONFIG")" || die "main: Failed to load config.yaml! Failed to read keyring"
|
||||
KEYRING_FILE="$(realpath "$(realpath --relative-to="$CONFIG_DIR" "$KEYRING_FILE")")"
|
||||
|
||||
[ -f "$KEYRING_FILE" ] || die "main: Defined keyring does not exist! Should exist at $KEYRING_FILE"
|
||||
|
||||
HAS_AUDIT_ROOM=""; jq -e '.audit_room | type == "string"' 1>/dev/null <<< "$CONFIG" && HAS_AUDIT_ROOM="1"
|
||||
DRY_RUN=""; jq -e '.dry_run' 1>/dev/null <<< "$CONFIG" && DRY_RUN="1"
|
||||
|
||||
ACTIONS="$(jq .actions -c <<< "$CONFIG")" || die "main: Failed to load config.yaml! Failed to read actions"
|
||||
ACCESS_TOKEN="$(jq '.access_token' -r <<< "$CONFIG")" || die "main: Failed to read access_token!"
|
||||
USER_ID="$(jq '.user_id' -r <<< "$CONFIG")" || die "main: Failed to read user_id!"
|
||||
TOPICS="$(yq -c . "$SCHEMA_DIR/topics.yaml")" || die "main: Failed to load topics!"
|
||||
|
||||
echo "main: This is a dry run, any actions will fail!" 1>&2
|
||||
|
||||
readonly CONFIG
|
||||
readonly ACTIONS
|
||||
readonly ACCESS_TOKEN
|
||||
readonly TOPICS
|
||||
readonly USER_ID
|
||||
readonly DRY_RUN
|
||||
|
||||
fetchRoomState () {
|
||||
# fetchRoomState: room_id
|
||||
local room_id
|
||||
room_id="$(jq '@uri' -Rr <<< "$1")" || return 1
|
||||
curl -sf -H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL/client/r0/rooms/$room_id/state"
|
||||
return $?
|
||||
}
|
||||
|
||||
fetchRoomPolicies () {
|
||||
# fetchRoomPolicies: room_id
|
||||
local states
|
||||
states="$(fetchRoomState "$1")" || return 1
|
||||
jq '[ .[] | select(.type == "club.glowers.policy.rule.collection") ]' -c <<< "$states"
|
||||
return $?
|
||||
}
|
||||
|
||||
arrayIndexList () {
|
||||
# eachInArray: array
|
||||
local length
|
||||
length="$(jq 'length' -r <<< "$1")"
|
||||
if [ -n "$length" ]; then
|
||||
seq 1 "$length"
|
||||
fi
|
||||
}
|
||||
|
||||
# satan
|
||||
# TODO: Rewrite this fucking abomination as a jq filter
|
||||
arrayAndIncludes () {
|
||||
# arrayAndIncludes: Array<String> Array<String>
|
||||
local bset
|
||||
local aset_index ; local a
|
||||
local bset_index ; local b
|
||||
bset="$(arrayIndexList "$2")"
|
||||
|
||||
for aset_index in $(arrayIndexList "$1"); do
|
||||
a="$(jq ".[$((aset_index-1))]" -rc <<< "$1")"
|
||||
for bset_index in $bset; do
|
||||
b="$(jq ".[$((bset_index-1))]" -rc <<< "$2")"
|
||||
if [ "$a" == "$b" ]; then
|
||||
echo "$a"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2155
|
||||
genIgnorefileLine () {
|
||||
# genIgnorefileLine: state
|
||||
local room_id="$(jq .room_id -r <<< "$1")"
|
||||
local state_key="$(jq .state_key -r <<< "$1")"
|
||||
echo "$room_id $state_key"
|
||||
}
|
||||
|
||||
checkPolicyIgnored () {
|
||||
# checkPolicyIgnored: state
|
||||
grep -Fx "$(genIgnorefileLine "$1")" "$IGNORED_FILE" 1>/dev/null # || return 0
|
||||
}
|
||||
|
||||
checkSignature () {
|
||||
# checkSignature: state
|
||||
local content
|
||||
local signature
|
||||
local mask
|
||||
local wd
|
||||
local code
|
||||
|
||||
content="$(jq '.content | { rules: .rules, signature: null }' -c <<< "$1")" || return 1
|
||||
signature="$(jq .content.signature -r <<< "$1")" || return 2
|
||||
|
||||
mask="$(umask)"
|
||||
umask 0077
|
||||
wd="$(mktemp -d)" || return 3
|
||||
echo "$signature" > "$wd/content.json.sig"
|
||||
echo "$content" > "$wd/content.json"
|
||||
|
||||
umask "$mask"
|
||||
|
||||
gpg --no-default-keyring --keyring "$KEYRING_FILE" --verify "$wd/content.json.sig"
|
||||
code="$?"
|
||||
if [ "$code" != "0" ]; then
|
||||
echo "checkSignature: Exception when verifying policy signature. Exit code: $?" 1>&2
|
||||
rm "$wd/content.json.sig" "$wd/content.json"
|
||||
rmdir "$wd"
|
||||
return 4
|
||||
fi
|
||||
|
||||
rm "$wd/content.json.sig" "$wd/content.json"
|
||||
rmdir "$wd"
|
||||
return 0
|
||||
}
|
||||
|
||||
checkPolicyAgainstSchema () {
|
||||
# checkPolicyAgainstSchema: policy
|
||||
yajsv -q -s "$SCHEMA_DIR/policy.yaml" /dev/stdin <<< "$(jq .content -c <<< "$1")"
|
||||
}
|
||||
|
||||
oneOfTagsAsArray () {
|
||||
# oneOfTagsAsArray: policy
|
||||
jq 'if (.tag | type == "string") then [.tag] else if (.tags | type == "array") then .tags else [] end end' -c < /dev/stdin
|
||||
}
|
||||
|
||||
applyTopicsToTags () {
|
||||
# applyTopicsToTags:
|
||||
jq --argjson aliases "$TOPICS" '[foreach ($aliases | to_entries[]) as $alias (.; if any($alias.value[] == .) then . + [$alias.key] else . end)] | last' --< /dev/stdin
|
||||
return $?
|
||||
}
|
||||
|
||||
deployRoomShutdown () {
|
||||
# deployRoomShutdown: room_id action
|
||||
[ -n "$DRY_RUN" ] && return 0
|
||||
local room_id
|
||||
local status
|
||||
local payload
|
||||
local reason
|
||||
room_id="$(jq '@uri' -Rr <<< "$1")" || return 1
|
||||
# FIXME: No proper error handling if an error thrown here.
|
||||
status="$(curl -sf -H "Authorization: Bearer $ACCESS_TOKEN" "$SYNAPSE_BASE_URL/admin/v1/rooms/$room_id/block")" || return 1
|
||||
#status='{"block": false}'
|
||||
|
||||
if ! jq -e '.block' 1>/dev/null <<< "$status"; then
|
||||
payload='{"block": true}'
|
||||
|
||||
# TODO: Add flag for .purge?
|
||||
|
||||
if ! jq -e '.room.silent' 1>/dev/null <<< "$2"; then
|
||||
payload="$(jq --argjson pl "$payload" --arg user_id "$USER_ID" '$pl * { new_room_user_id: $user_id }' -nc)"
|
||||
fi
|
||||
|
||||
reason="$(jq '.room.reason' -r <<< "$2")"
|
||||
if jq -e '.room.reason' 1>/dev/null <<< "$2"; then
|
||||
payload="$(jq --argjson pl "$payload" --arg reason "$reason" '$pl * { reason: $reason }' -nc)"
|
||||
fi
|
||||
|
||||
#echo "$payload" 1>&2
|
||||
|
||||
curl -sf -X DELETE -d "$payload" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN" "$SYNAPSE_BASE_URL/admin/v1/rooms/$room_id/block"
|
||||
return $?
|
||||
fi
|
||||
}
|
||||
|
||||
deployRoomDelist () {
|
||||
# deployRoomDelist: room_id action
|
||||
[ -n "$DRY_RUN" ] && return 0
|
||||
local room_id
|
||||
room_id="$(jq '@uri' -Rr <<< "$1")" || return 1
|
||||
# FIXME: No error handling if an error thrown here.
|
||||
# TODO: Check if synapse ACTUALLY lets you do this. Docs say you can but who travis probably wrote them
|
||||
curl -sf -X PUT -d '{"visibility":"private"}' \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL/client/r0/directory/list/room/$room_id"
|
||||
return $?
|
||||
}
|
||||
|
||||
deployRoomQuarantine () {
|
||||
# deployRoomQuarantine: room_id action
|
||||
[ -n "$DRY_RUN" ] && return 0
|
||||
local room_id
|
||||
room_id="$(jq '@uri' -Rr <<< "$1")" || return 1
|
||||
# FIXME: No error handling if an error thrown here.
|
||||
curl -sf -X POST -d '{}' -H "Authorization: Bearer $ACCESS_TOKEN" "$SYNAPSE_BASE_URL/admin/v1/room/$room_id/media/quarantine"
|
||||
return $?
|
||||
}
|
||||
|
||||
deployRoomRule () {
|
||||
# deployRoomRule: room_id action
|
||||
local action_taken
|
||||
local code
|
||||
|
||||
# TODO: Does synapse delist if shutting down? If so we should just skip delist if shutdown is defined
|
||||
if jq -e '.room.delist' 1>/dev/null <<< "$2"; then
|
||||
action_taken='1'
|
||||
deployRoomDelist "$1" "$2"
|
||||
# TODO: Failure shouldn't block quarantine or shutdown, not the best way of handling.
|
||||
#code="$?"
|
||||
#[ "$code" -ne 0 ] && return $code
|
||||
fi
|
||||
|
||||
if jq -e '.room.quarantine' 1>/dev/null <<< "$2"; then
|
||||
action_taken='1'
|
||||
deployRoomQuarantine "$1" "$2"
|
||||
code="$?"
|
||||
[ "$code" -ne 0 ] && return $code
|
||||
fi
|
||||
|
||||
if jq -e '.room.shutdown' 1>/dev/null <<< "$2"; then
|
||||
action_taken='1'
|
||||
deployRoomShutdown "$1" "$2"
|
||||
code="$?"
|
||||
[ "$code" -ne 0 ] && return $code
|
||||
fi
|
||||
|
||||
[ -z "$action_taken" ] && echo "deployRoomRule: Action defines no action. Odd" 2>&1
|
||||
return 0
|
||||
}
|
||||
|
||||
deployUserRule () {
|
||||
# deployUserRule: user_id action
|
||||
echo "deployUserRule: Not Implemented!" 1>&2
|
||||
return 255
|
||||
}
|
||||
|
||||
deployRule () {
|
||||
# deployRule: rule action
|
||||
local type
|
||||
type="$(jq .type -r <<< "$1")"
|
||||
if [ "$type" == "m.user" ]; then
|
||||
deployUserRule "$1" "$2"
|
||||
return $?
|
||||
elif [ "$type" == "m.room" ]; then
|
||||
deployRoomRule "$1" "$2"
|
||||
return $?
|
||||
else
|
||||
die "deployRule: Internal logic error. Schema check let something slip through!"
|
||||
fi
|
||||
}
|
||||
|
||||
ingestRule () {
|
||||
# ingestRule: rule index state_key
|
||||
local rule_tags
|
||||
local actions_index
|
||||
local action
|
||||
local action_tags
|
||||
local match
|
||||
local code
|
||||
rule_tags="$(oneOfTagsAsArray <<< "$1" | applyTopicsToTags)" || return 1
|
||||
|
||||
for actions_index in $(arrayIndexList "$ACTIONS"); do
|
||||
action="$(jq ".[$((actions_index-1))]" -rc <<< "$ACTIONS")" || return 2
|
||||
action_tags="$(oneOfTagsAsArray <<< "$action" | applyTopicsToTags)" || return 3
|
||||
match="$(arrayAndIncludes "$rule_tags" "$action_tags")"
|
||||
if [ -n "$match" ]; then
|
||||
echo "ingestRule[$3:$2]: Found matching tag \"$match\"."
|
||||
deployRule "$1" "$action"
|
||||
code="$?"
|
||||
[ "$code" -ne 0 ] && echo "ingestRule[$3:$2]: Unexpected exit code of $code when handling action." 1>&2
|
||||
return $code
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
ingestPolicy () {
|
||||
# ingestPolicy: state state_key
|
||||
local rule
|
||||
local retcode="0"
|
||||
local rules
|
||||
rules="$(jq .content.rules -c <<< "$1")"
|
||||
for policy_index in $(arrayIndexList "$rules"); do
|
||||
rule="$(jq ".[$((policy_index-1))]" -rc <<< "$rules")" || return 1
|
||||
ingestRule "$rule" "$policy_index" "$2"
|
||||
local code="$?"
|
||||
if [ "$code" != "0" ]; then
|
||||
echo "ingestPolicy[$policy_index]: Unexpected exit code of $code when handling rule."
|
||||
local retcode="2"
|
||||
fi
|
||||
done
|
||||
return $retcode
|
||||
}
|
||||
|
||||
ingestState () {
|
||||
# ingestState: state
|
||||
local state_key
|
||||
state_key="$(stripCtrlChars <<< "$(jq .state_key -r <<< "$1")")"
|
||||
if ! checkPolicyIgnored "$1"; then
|
||||
if checkPolicyAgainstSchema "$1"; then
|
||||
if checkSignature "$1"; then
|
||||
ingestPolicy "$1" "$state_key" && genIgnorefileLine "$1" >> "$IGNORED_FILE"
|
||||
else
|
||||
echo "ingestState[$state_key]: Unexpected GnuPG exit code $?." 1>&2
|
||||
fi
|
||||
else
|
||||
echo "ingestState[$state_key]: Unexpected yajsv exit code $?. This policy is improperly formatted!" 1>&2
|
||||
genIgnorefileLine "$1" >> "$IGNORED_FILE"
|
||||
fi
|
||||
else
|
||||
echo "ingestState[$state_key]: Ignored."
|
||||
fi
|
||||
}
|
||||
|
||||
ingestPolicyRoom () {
|
||||
# ingestPolicyRoom: room_id
|
||||
local policies
|
||||
local state
|
||||
policies="$(fetchRoomPolicies "$1")" || return 1
|
||||
for policies_index in $(arrayIndexList "$policies"); do
|
||||
state="$(jq ".[$((policies_index-1))]" -rc <<< "$policies")" || return 2
|
||||
ingestState "$state" || echo "ingestPolicyRoom[$1]: Unexpected exit code of $? when processing state." 1>&2
|
||||
done
|
||||
}
|
||||
|
||||
init () {
|
||||
local room_id
|
||||
local policy_rooms_index
|
||||
|
||||
for policy_rooms_index in $(arrayIndexList "$(jq '.policy_rooms' -c <<< "$CONFIG")"); do
|
||||
room_id="$(jq ".policy_rooms[$((policy_rooms_index-1))]" -r <<< "$CONFIG")" || return 1
|
||||
echo "main: Requesting policies for $room_id"
|
||||
ingestPolicyRoom "$room_id" || die "init[$room_id]: Unexpected exit code when processing policy room. $?"
|
||||
|
||||
# processPolicyRoom "$room_id"
|
||||
#code="$?"
|
||||
#if [ "$code" != "0" ]; then echo "main: Unexpected exit code when processing policy room \"$room_id\": $code" 1>&2; fi
|
||||
done
|
||||
}
|
||||
|
||||
init
|
45
readme.md
Normal file
45
readme.md
Normal file
@ -0,0 +1,45 @@
|
||||
**NOTE:** Hotpocket is still a work in progress, the documentation and the script are not to be trusted.
|
||||
|
||||
<center>
|
||||
<br />
|
||||
<!-- Apostrophe doesn't render this properly because it's retarded. WONTFIX -->
|
||||
<img src="docs/hotpocket.png" alt="Hotpocket" />
|
||||
<br /><br />
|
||||
<p>Daemon for automated room management</p>
|
||||
<a href="docs/debian_11.md">Installation</a> |
|
||||
<a href="docs/configuration.md">Configuration</a> |
|
||||
<a href="docs/policies.md">Policy deployment</a> |
|
||||
<a href="docs/tags.md">Tags</a> |
|
||||
<a href="https://matrix.to/#/#loj:glowers.club">#loj:glowers.club</a>
|
||||
<br /><br />
|
||||
</center>
|
||||
|
||||
Hotpocket is a room management daemon for matrix, which allows homeserver operators to more effectively deal with unwanted content.
|
||||
|
||||
### Todo
|
||||
- [x] Logo (very important)
|
||||
- [ ] Documentation. Everything changed needs lookover
|
||||
- [x] config.d
|
||||
- [x] Policy querying
|
||||
- [x] Basic Actions
|
||||
- [ ] Advanced Actions (archiving, audit log)
|
||||
- [ ] Audit log. This *was* implemented but everything got rewritten
|
||||
- [ ] Mjolnir forwarding
|
||||
|
||||
### Faq
|
||||
|
||||
#### Why?
|
||||
Existing tools like Mjolnir do not implement `m.policy.rule.room`, meaning room management through Mjolnir is tedious and inefficient. Additionally, Mjolnir can only act on one homeserver at a time, meaning homeserver operators can't effectively crowd-source homeserver moderation.
|
||||
|
||||
#### Why in bash?
|
||||
Hotpocket is intended to be easily deployable, with as few dependencies as possible. Shell scripts are not only highly portable, but easily auditable.
|
||||
|
||||
#### Is this more complicated than Mjolnir?
|
||||
The only effective difference between Mjolnir's room management and Hotpocket, is that hotpocket requires all policies to be signed by a trusted public key.
|
||||
|
||||
If you're running your own homeserver, the small jump in complexity shouldn't be an issue.
|
||||
|
||||
#### Can I use this with my <attr title="Element Matrix Services">EMS</attr> homeserver?
|
||||
If you want to run hotpocket on a <attr title="Matrix-as-a-Service">MaaS</attr> provider, you will need access to `/_synapse` and a server to install hotpocket to.
|
||||
|
||||
As `/_synapse` will be exposed to the internet, this is not considered a secure configuration. If possible, you should migrate to hosting your matrix homeserver yourself.
|
58
scripts/polmake.sh
Executable file
58
scripts/polmake.sh
Executable file
@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
if [ -z "$EDITOR" ]; then echo "polmake: No EDITOR set in environment. Please set one :)" 1>&2; exit 1; fi
|
||||
ext=".yaml"
|
||||
|
||||
newfile () {
|
||||
local prev_mask="$(umask)"
|
||||
umask 0077
|
||||
local file="$(mktemp --suffix "$ext")"
|
||||
umask "$prev_mask"
|
||||
echo "$file"
|
||||
}
|
||||
|
||||
extswap () {
|
||||
local text="$(cat < /dev/stdin)"
|
||||
if [ "$ext" == ".yaml" ]; then
|
||||
echo "$text" | yq .
|
||||
elif [ "$ext" == ".json" ]; then
|
||||
echo "$text"
|
||||
else
|
||||
echo "polmake: Unsure how to process extension typeof \"$ext\"." 1>&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
file="$(newfile)"
|
||||
while :; do
|
||||
set +u
|
||||
$EDITOR "$file"
|
||||
set -u
|
||||
|
||||
json="$(cat "$file")"
|
||||
code="$?"
|
||||
if [ "$code" != 0 ]; then exit 0; fi
|
||||
if [ -z "$json" ]; then exit 0; fi
|
||||
json="$(echo "$json" | extswap)"
|
||||
echo "$json
|
||||
|
||||
Type EDIT to open the editor
|
||||
Type SIGN to sign this policy
|
||||
Type QUIT to cancel"
|
||||
read -p "> " cmd
|
||||
if [ "$cmd" == "EDIT" ] || [ "$cmd" == "edit" ] || [ "$cmd" == "e" ]; then
|
||||
continue
|
||||
elif [ "$cmd" == "SIGN" ] || [ "$cmd" == "sign" ] || [ "$cmd" == "s" ]; then
|
||||
rm "$file"
|
||||
read -p "gpgid> " gpgid
|
||||
./polsign.sh "$gpgid" <<< "$json"
|
||||
break
|
||||
else
|
||||
rm "$file"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
|
||||
|
24
scripts/polsign.sh
Executable file
24
scripts/polsign.sh
Executable file
@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
KEY_ID="$1"
|
||||
if [ -z "$KEY_ID" ]; then echo "polsign: A signing key id must be defined!" 1>&2; exit 255; fi
|
||||
# echo "{}" | mkpolicy key_id
|
||||
|
||||
JSON="$(jq . < /dev/stdin)"
|
||||
if [ "$?" != "0" ]; then echo "polsign: Unexpected exit code when loading JSON from stdin: $?" 1>&2; exit 255; fi
|
||||
|
||||
_JSON="$(echo "$JSON" | jq '{ rules: .rules, signature: null }' -c)"
|
||||
if [ "$?" != "0" ]; then echo "polsign: Unexpected exit code when stripping JSON: $?" 1>&2; exit 255; fi
|
||||
|
||||
SIGNATURE="$(echo "$_JSON" | gpg --local-user "$KEY_ID" --sign --armor --detach-sig)"
|
||||
if [ "$?" != "0" ]; then echo "polsign: Unexpected exit code when signing JSON: $?" 1>&2; exit 255; fi
|
||||
|
||||
SIGNED="$(jq --null-input \
|
||||
--argjson event "$_JSON" \
|
||||
--arg signature "$SIGNATURE" \
|
||||
'$event * { signature: $signature }')"
|
||||
if [ "$?" != "0" ]; then echo "polsign: Unexpected exit code when signing JSON: $?" 1>&2; exit 255; fi
|
||||
|
||||
echo "$SIGNED"
|
||||
|
||||
|
109
scripts/schema/config.yaml
Normal file
109
scripts/schema/config.yaml
Normal file
@ -0,0 +1,109 @@
|
||||
title: config.yaml
|
||||
type: object
|
||||
required: [ access_token, user_id, base_url, synapse_base_url, keyring, policy_rooms, actions ]
|
||||
properties:
|
||||
base_url:
|
||||
type: string
|
||||
example: http://127.0.0.1:3080/_matrix
|
||||
format: uri
|
||||
pattern: ^https?://.+/_matrix$
|
||||
synapse_base_url:
|
||||
type: string
|
||||
example: http://127.0.0.1:3880/_synapse
|
||||
format: uri
|
||||
pattern: ^https?://.+/_synapse$
|
||||
keyring:
|
||||
type: string
|
||||
example: ./keyring.gpg
|
||||
access_token:
|
||||
type: string
|
||||
dry_run:
|
||||
type: boolean
|
||||
user_id:
|
||||
comment: 'The user_id tied to the access_token. Used for room shutdowns'
|
||||
type: string
|
||||
pattern: '@[\w\-]+:.+'
|
||||
policy_rooms:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '![\w\-]+:.+'
|
||||
actions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
oneOf:
|
||||
- required: [ tag ]
|
||||
properties:
|
||||
tag: { type: string, pattern: ^\w+$ }
|
||||
- required: [ tags ]
|
||||
properties:
|
||||
tags:
|
||||
type: array
|
||||
items: [ { type: string, pattern: ^\w+$ } ]
|
||||
anyOf:
|
||||
- required: [ user ]
|
||||
- required: [ room ]
|
||||
properties:
|
||||
user:
|
||||
properties:
|
||||
archive:
|
||||
properties:
|
||||
media:
|
||||
comment: 'Save a copy of the identifiers for the last 255 media uploads by this user to disk. This does not save the actual uploads'
|
||||
type: boolean
|
||||
rooms:
|
||||
comment: 'Save a copy of all rooms this user was participating in to disk.'
|
||||
type: boolean
|
||||
deactivate:
|
||||
type: boolean
|
||||
comment: 'Deactivate the User'
|
||||
notify:
|
||||
comment: 'Create an audit log in the audit room about the user.'
|
||||
type: boolean
|
||||
quarantine:
|
||||
comment: 'Quarantine all media uploaded by the user.'
|
||||
type: boolean
|
||||
# TODO: Add mjolnir list forwarding
|
||||
room:
|
||||
properties:
|
||||
archive:
|
||||
properties:
|
||||
media:
|
||||
comment: 'Save a copy of the identifiers for the last 255 media uploads in this room to disk. This does not save the actual uploads'
|
||||
type: boolean
|
||||
members:
|
||||
comment: 'Save a copy of all users that were participating in this room to disk.'
|
||||
type: boolean
|
||||
delist:
|
||||
comment: 'Remove the room from the room directory.'
|
||||
type: boolean
|
||||
notify:
|
||||
comment: 'Create an audit log in the audit room about this room.'
|
||||
type: boolean
|
||||
reason:
|
||||
comment: 'For shutdown. A publically visible reason.'
|
||||
type: string
|
||||
shutdown:
|
||||
comment: 'Shutdown the room'
|
||||
type: boolean
|
||||
silent:
|
||||
comment: 'For shutdown. If a notice room should not be created for the closed room.'
|
||||
type: boolean
|
||||
quarantine:
|
||||
comment: 'Quarantine all media uploaded in the room.'
|
||||
type: boolean
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
15
scripts/schema/policy.example.json
Normal file
15
scripts/schema/policy.example.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"type": "m.room",
|
||||
"entity": "!id:example.com",
|
||||
"tags": [ "csam" ]
|
||||
},
|
||||
{
|
||||
"type": "m.user",
|
||||
"entity": "@user-_:example.com",
|
||||
"tag": "high_risk"
|
||||
}
|
||||
],
|
||||
"signature": "-----BEGIN PUBLIC KEY SIGNATURE-----..."
|
||||
}
|
28
scripts/schema/policy.yaml
Normal file
28
scripts/schema/policy.yaml
Normal file
@ -0,0 +1,28 @@
|
||||
title: policy.yaml
|
||||
type: object
|
||||
required: [ signature, rules ]
|
||||
properties:
|
||||
signature:
|
||||
type: string
|
||||
rules:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [ type, entity ]
|
||||
allOf:
|
||||
- oneOf:
|
||||
- properties:
|
||||
type: { type: string, const: m.room }
|
||||
entity: { type: string, pattern: '^![\w\-]+:.+$' }
|
||||
- properties:
|
||||
type: { type: string, const: m.user }
|
||||
entity: { type: string, pattern: '^@[\w\-]+:.+$' }
|
||||
- oneOf:
|
||||
- required: [ tag ]
|
||||
properties:
|
||||
tag: { type: string, pattern: ^\w+$ }
|
||||
- required: [ tags ]
|
||||
properties:
|
||||
tags:
|
||||
type: array
|
||||
items: [ { type: string, pattern: ^\w+$ } ]
|
22
scripts/schema/topics.yaml
Normal file
22
scripts/schema/topics.yaml
Normal file
@ -0,0 +1,22 @@
|
||||
# This file contains a list of "tag aliases", called topics.
|
||||
# If any entry values are found, the entry key is added to the tags.
|
||||
# This is mainly used for making actions less verbose
|
||||
high_risk_porn:
|
||||
- 3d_loli
|
||||
- csam
|
||||
- beastiality
|
||||
high_risk:
|
||||
- anarchy
|
||||
- jailbait
|
||||
- high_risk_porn
|
||||
porn:
|
||||
- drawn_porn
|
||||
- high_risk_porn
|
||||
- irl_porn
|
||||
- loli
|
||||
nsfl:
|
||||
- gore
|
||||
- high_risk
|
||||
nsfw:
|
||||
- porn
|
||||
- nsfl
|
9
scripts/systemd/hotpocket.service
Normal file
9
scripts/systemd/hotpocket.service
Normal file
@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Matrix Homeserver Policy Manager
|
||||
Wants=hotpocket.timer
|
||||
|
||||
[Service]
|
||||
User=hotpocket
|
||||
Type=simple
|
||||
Environment="HOTPOCKET_CONFIG_DIR=/etc/hotpocket"
|
||||
ExecStart=./etc/hotpocket.sh
|
9
scripts/systemd/hotpocket.timer
Normal file
9
scripts/systemd/hotpocket.timer
Normal file
@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Hotpocket Timer
|
||||
|
||||
[Timer]
|
||||
OnBootSec=30s
|
||||
OnCalendar=*:0/10
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
Reference in New Issue
Block a user