12 Commits

8 changed files with 93 additions and 71 deletions

View File

@@ -10,7 +10,7 @@ platform:
steps: steps:
- name: build - name: build
commands: commands:
- docker build -t gcrkrause/mastodon-blocklist-deploy . - docker build -t gcrkrause/fediverse-blocklist-deploy .
- name: push - name: push
environment: environment:
USERNAME: USERNAME:
@@ -19,5 +19,11 @@ steps:
from_secret: docker-hub-pw from_secret: docker-hub-pw
commands: commands:
- docker login -u $USERNAME -p $PASSWORD - docker login -u $USERNAME -p $PASSWORD
- docker push gcrkrause/mastodon-blocklist-deploy - docker push gcrkrause/fediverse-blocklist-deploy
- docker image prune -a -f - docker image prune -a -f
when:
event:
exclude:
- pull_request
branch:
- main

View File

@@ -4,14 +4,14 @@
In order to have a common development environment, its nice to use docker. Its quite easy. To build a new image, simply run In order to have a common development environment, its nice to use docker. Its quite easy. To build a new image, simply run
`docker build . -t mastodon_blocklist_deploy` `docker build . -t fediverse_blocklist_deploy`
Now you can execute any commands using Now you can execute any commands using
`docker run --rm mastodon_blocklist_deploy --help` `docker run --rm fediverse_blocklist_deploy --help`
If you want to avoid building new containers for each change, simply mount your code into the container using If you want to avoid building new containers for each change, simply mount your code into the container using
`docker run --rm -v $(pwd):/app mastodon_blocklist_deploy` `docker run --rm -v $(pwd):/app fediverse_blocklist_deploy`
Please be aware that changes to the package itself require a rebuild anyways. Please be aware that changes to the package itself require a rebuild anyways.

View File

@@ -4,9 +4,9 @@ ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
COPY pyproject.toml poetry.lock README.md /app/ COPY pyproject.toml poetry.lock README.md /app/
COPY mastodon_blocklist_deploy /app/mastodon_blocklist_deploy COPY fediverse_blocklist_deploy /app/fediverse_blocklist_deploy
WORKDIR /app WORKDIR /app
ENTRYPOINT ["mastodon_blocklist_deploy"] ENTRYPOINT ["fediverse_blocklist_deploy"]
RUN pip install -e . RUN pip install -e .

View File

@@ -1,35 +1,40 @@
# mastodon-blocklist-deploy # fediverse-blocklist-deploy
A small tool to deploy blocklist updates to a mastodon server using its API. A small tool to deploy blocklist updates to a fediverse server using its API.
## Concept ## Concept
The idea is to maintain a blocklist in a simple structured file in this repository. All changes need to be deployed to The idea is to maintain a blocklist in a simple structured file in this repository. All changes need to be deployed to
the mastodon server, this is supposed to be automated with Drone CI. the fediverse server, this is supposed to be automated with Drone CI.
In order to compare the list entries, we can read the whole blocklist In order to compare the list entries, we can read the whole blocklist
using [the get endpoint](https://docs.joinmastodon.org/methods/admin/domain_blocks/#get). At the same time we read the using [the get endpoint](https://docs.joinmastodon.org/methods/admin/domain_blocks/#get). At the same time we read the
whole file in the repository, make a comparision whole file in the repository, make a comparison
and [remove](https://docs.joinmastodon.org/methods/admin/domain_blocks/#delete) unblocked domains from the blocklist and [remove](https://docs.joinmastodon.org/methods/admin/domain_blocks/#delete) unblocked domains from the blocklist
and [add](https://docs.joinmastodon.org/methods/admin/domain_blocks/#create) newly added. and [add](https://docs.joinmastodon.org/methods/admin/domain_blocks/#create) newly added.
Since we have several attributes for a domain blog, a simple `.txt` file might not be sufficient. We probably want to Since we have several attributes for a domain block, a simple `.txt` file might not be sufficient. We probably want to
set the severity, reject_media, reject_reports and comments. This means we need a human-readable, easily python-readable set the severity, reject_media, reject_reports and comments. This means we need a human-readable, easily python-readable
and structured file format. Since Python 3.11 got native support for [toml](https://toml.io/) and it and structured file format. Since Python 3.11 got native support for [toml](https://toml.io/) and it
supports [Array of Tables](https://toml.io/en/v1.0.0#array-of-tables), I'd prefer to use this. supports [Array of Tables](https://toml.io/en/v1.0.0#array-of-tables), I'd prefer to use this.
# Supported server types
- [x] Mastodon
- [X] GoToSocial
# Basic usage # Basic usage
## ##
``` ```
usage: mastodon_blocklist_deploy [-h] [-s SERVER] [-t TOKEN] [-i INPUT_FILE] [-r REMOTE_BLOCKLIST] [-o OUTPUT] [-v] [-n] usage: fediverse_blocklist_deploy [-h] [-s SERVER] [-t TOKEN] [-i INPUT_FILE] [-r REMOTE_BLOCKLIST] [-o OUTPUT] [-v] [-n]
[--format FORMAT] [--private] [--format FORMAT] [--private]
{diff,deploy,export} {diff,deploy,export}
Deploy blocklist updates to a mastodon server Deploy blocklist updates to a fediverse server
positional arguments: positional arguments:
{diff,deploy,export} Either use 'diff' to check the difference between local blockĺist and the blocklist on the server, 'deploy' {diff,deploy,export} Either use 'diff' to check the difference between local blockĺist and the blocklist on the server, 'deploy'
@@ -49,7 +54,7 @@ options:
Filename where to export the blocklist Filename where to export the blocklist
-v, --verbose -v, --verbose
-n, --no-delete Do not delete existing blocks -n, --no-delete Do not delete existing blocks
--format FORMAT Export format: toml|markdown|csv --format FORMAT Export format: toml|markdown|csv|json
--private When the flag is set, private comment will also be exported. --private When the flag is set, private comment will also be exported.
``` ```
@@ -64,7 +69,7 @@ options:
1. **Export the current blocklist from the server** 1. **Export the current blocklist from the server**
``` ```
mastodon_blocklist_deploy export -s yourserver -t yourtoken -o blocklist.toml fediverse_blocklist_deploy export -s yourserver -t yourtoken -o blocklist.toml
``` ```
2. **Manually add something to the blocklist** 2. **Manually add something to the blocklist**
@@ -83,12 +88,12 @@ private_comment = "We discussed this after X and Y and now that Z happend we dec
3. **Check the difference between the local and remote blocklist** 3. **Check the difference between the local and remote blocklist**
``` ```
mastodon_blocklist_deploy diff -s yourserver -t yourtoken -i blocklist.toml fediverse_blocklist_deploy diff -s yourserver -t yourtoken -i blocklist.toml
``` ```
4. **Apply the local blocklist to the server** 4. **Apply the local blocklist to the server**
``` ```
mastodon_blocklist_deploy apply -s yourserver -t yourtoken -i blocklist.toml fediverse_blocklist_deploy apply -s yourserver -t yourtoken -i blocklist.toml
``` ```

View File

@@ -6,8 +6,9 @@ import requests
import os import os
import toml import toml
from mastodon_blocklist_deploy.models import Instance from fediverse_blocklist_deploy.models import Instance
from mastodon_blocklist_deploy.helpers import blocklist_to_markdown, blocklist_to_toml, blocklist_to_csv from fediverse_blocklist_deploy.helpers import blocklist_to_markdown, blocklist_to_toml, blocklist_to_csv, \
blocklist_to_json
def load_blocklist_file(filename: str) -> [Instance]: def load_blocklist_file(filename: str) -> [Instance]:
@@ -52,6 +53,8 @@ def exporter(blocklist, output=None, format: str = "toml", private: bool = False
exported_text = blocklist_to_csv(blocklist, private) exported_text = blocklist_to_csv(blocklist, private)
if format == "markdown": if format == "markdown":
exported_text = blocklist_to_markdown(blocklist, private) exported_text = blocklist_to_markdown(blocklist, private)
if format == "json":
exported_text = blocklist_to_json(blocklist, private)
# Output the text # Output the text
if output is not None: if output is not None:
@@ -61,13 +64,37 @@ def exporter(blocklist, output=None, format: str = "toml", private: bool = False
print(exported_text) print(exported_text)
def merge(input_file, merge_target, format: str = "toml", private: bool = False, overwrite=False):
input_blocklist = load_blocklist_file(input_file)
merge_target_blocklist = load_blocklist_file(merge_target)
for input_instance in input_blocklist:
# If the block is already there with the same parameters we do nothing
if input_instance in merge_target_blocklist:
continue
# Check if there is a domain in the merge target where the input domain is similar
try:
merge_target_instance = [merge_target_instance for merge_target_instance in merge_target if input_instance.domain == merge_target_instance.domain][0]
if not overwrite:
key_input = ""
while key_input not in ("i", "O"):
print(f"Different settings for {input_instance.domain} detected.")
print(f"In the input blocklist the setting is\n{input_instance} whereas it's {merge_target_instance} in the merge target")
key_input = input("Keep input (i) or original (o) [i/O]")
elif key_input == "i":
merge_target_blocklist.append(merge_target_instance)
else:
merge_target_blocklist.append(input_instance)
except KeyError:
pass
def cli(): def cli():
parser = argparse.ArgumentParser(description='Deploy blocklist updates to a mastodon server') parser = argparse.ArgumentParser(description='Deploy blocklist updates to a fediverse server')
parser.add_argument('action', choices=['diff', 'deploy', 'export'], parser.add_argument('action', choices=['diff', 'deploy', 'export', 'merge'],
help="Either use 'diff' to check the difference between local blockĺist and the blocklist on " help="Either use 'diff' to check the difference between local blockĺist and the blocklist on "
"the server, 'deploy' to apply the current local blocklist or 'export' to export the remote " "the server, 'deploy' to apply the current local blocklist or 'export' to export the remote "
"blocklist into a local file.") "blocklist into a local file. merge can be used to merge a blocklist (given by -i) into "
"another (-o)")
parser.add_argument('-s', '--server', help="The address of the server where you want to deploy (e.g. " parser.add_argument('-s', '--server', help="The address of the server where you want to deploy (e.g. "
"mastodon.social)") "mastodon.social)")
parser.add_argument('-t', '--token', help="Authorization token") parser.add_argument('-t', '--token', help="Authorization token")
@@ -76,7 +103,7 @@ def cli():
parser.add_argument('-o', '--output', help="Filename where to export the blocklist") parser.add_argument('-o', '--output', help="Filename where to export the blocklist")
parser.add_argument('-v', '--verbose', action='store_true') parser.add_argument('-v', '--verbose', action='store_true')
parser.add_argument('-n', '--no-delete', action='store_true', help="Do not delete existing blocks") parser.add_argument('-n', '--no-delete', action='store_true', help="Do not delete existing blocks")
parser.add_argument('--format', help="Export format: toml|markdown|csv") parser.add_argument('--format', default="toml", type=str, help="Export format: toml|markdown|csv|json")
parser.add_argument('--private', action='store_true', help="When the flag is set, private comment will also be " parser.add_argument('--private', action='store_true', help="When the flag is set, private comment will also be "
"exported.") "exported.")
args = parser.parse_args() args = parser.parse_args()
@@ -90,8 +117,6 @@ def cli():
else: else:
token = os.getenv('MBD_TOKEN') token = os.getenv('MBD_TOKEN')
"""if there is a remote blocklist provided load this instead of fetching it from a server (for debugging reasons)""" """if there is a remote blocklist provided load this instead of fetching it from a server (for debugging reasons)"""
if args.remote_blocklist: if args.remote_blocklist:
with open(args.remote_blocklist) as f: with open(args.remote_blocklist) as f:
@@ -100,7 +125,7 @@ def cli():
remote_blocklist = load_blocklist_from_instance(server=args.server, token=token) remote_blocklist = load_blocklist_from_instance(server=args.server, token=token)
"""Load local blocklist only when needed""" """Load local blocklist only when needed"""
if args.action in ["diff", "deploy"]: if args.action in ["diff", "deploy", "merge"]:
if args.input_file: if args.input_file:
blocklist_filename = args.input_file blocklist_filename = args.input_file
else: else:
@@ -118,6 +143,9 @@ def cli():
Instance.apply_blocks_from_diff(diffs, args.server, token, args.no_delete) Instance.apply_blocks_from_diff(diffs, args.server, token, args.no_delete)
elif args.action == "export": elif args.action == "export":
exporter(remote_blocklist, args.output, args.format, args.private) exporter(remote_blocklist, args.output, args.format, args.private)
elif args.action == "merge":
merge(args.input_file, args.output, args.format, args.private)
if __name__ == "__main__": if __name__ == "__main__":
cli() cli()

View File

@@ -1,7 +1,8 @@
from mastodon_blocklist_deploy.models import Instance from fediverse_blocklist_deploy.models import Instance
import toml import toml
import io import io
import csv import csv
import json
def blocklist_to_markdown(blocklist: [Instance], private: bool = False): def blocklist_to_markdown(blocklist: [Instance], private: bool = False):
if private: if private:
@@ -29,3 +30,7 @@ def blocklist_to_csv(blocklist: [Instance], private: bool = False):
w.writeheader() w.writeheader()
w.writerows(blocklist_as_dict) w.writerows(blocklist_as_dict)
return csv_string.getvalue() return csv_string.getvalue()
def blocklist_to_json(blocklist: [Instance], private: bool = False):
json_string = json.dumps([b.as_dict(private) for b in blocklist])
return json_string

View File

@@ -11,12 +11,7 @@ class Instance:
self.reject_reports = False self.reject_reports = False
self.id = None self.id = None
"""Remote blocks and local blocks are parsed differently""" self.parse_block(instance_dict)
try:
instance_dict["id"]
self.parse_remote_block(instance_dict)
except KeyError:
self.parse_local_block(instance_dict)
def __str__(self): def __str__(self):
return f"{self.domain}: {self.severity}" return f"{self.domain}: {self.severity}"
@@ -36,37 +31,20 @@ class Instance:
exportable[key] = getattr(self, key) exportable[key] = getattr(self, key)
return exportable return exportable
def parse_remote_block(self, instance_dict): def parse_block(self, instance_dict):
self.domain = instance_dict["domain"] # this specifies possible properties and default values if not found on the remote source. If a default is None
self.id = instance_dict["id"] # the value is required and the parse will fail
self.severity = instance_dict["severity"] properties_and_defaults = [("domain", None), ("severity", "suspend"), ("public_comment", ""),
self.public_comment = instance_dict["public_comment"] ("private_comment", ""), ("obfuscate", False), ("reject_media", True),
self.private_comment = instance_dict["private_comment"] ("reject_reports", True)]
self.obfuscate = instance_dict["obfuscate"] for key, default in properties_and_defaults:
self.reject_media = instance_dict["reject_media"]
self.reject_reports = instance_dict["reject_reports"]
def parse_local_block(self, instance_dict):
try: try:
self.name = instance_dict["name"] setattr(self, key, instance_dict[key])
except KeyError: except KeyError:
pass if default is not None:
self.domain = instance_dict["domain"] setattr(self, key, default)
self.severity = instance_dict["severity"] else:
self.public_comment = instance_dict["public_comment"] raise KeyError(f"The key {key} was not in the instance_dict response.")
self.private_comment = instance_dict["private_comment"]
try:
self.obfuscate = instance_dict["obfuscate"]
except KeyError:
pass
try:
self.reject_media = instance_dict["reject_media"]
except KeyError:
pass
try:
self.reject_reports = instance_dict["reject_reports"]
except KeyError:
pass
def apply(self, server, token, block_id=None): def apply(self, server, token, block_id=None):
"""Applies instance block on the remote server""" """Applies instance block on the remote server"""

View File

@@ -1,12 +1,12 @@
[tool.poetry] [tool.poetry]
name = "mastodon-blocklist-deploy" name = "fediverse-blocklist-deploy"
version = "0.1.0" version = "0.1.0"
description = "A small tool to deploy blocklist updates to a mastodon server using its API." description = "A small tool to deploy blocklist updates to a fediverse server using its API."
authors = ["Georg Krause <mail@georg-krause.net>", "Julian-Samuel Gebühr <julian-samuel@gebuehr.net>"] authors = ["Georg Krause <mail@georg-krause.net>", "Julian-Samuel Gebühr <julian-samuel@gebuehr.net>"]
readme = "README.md" readme = "README.md"
packages = [{include = "mastodon_blocklist_deploy"}] packages = [{include = "fediverse_blocklist_deploy"}]
license = "MIT" license = "MIT"
keywords = ["mastodon", "blocklist", "fediverse"] keywords = ["fediverse", "blocklist", "fediverse"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
@@ -16,7 +16,7 @@ rich = "^13.0.1"
toml = "^0.10.2" toml = "^0.10.2"
[tool.poetry.scripts] [tool.poetry.scripts]
mastodon_blocklist_deploy = 'mastodon_blocklist_deploy.cli:cli' fediverse_blocklist_deploy = 'fediverse_blocklist_deploy.cli:cli'
[build-system] [build-system]