diff --git a/.github/workflows/publish-index.yml b/.github/workflows/publish-index.yml new file mode 100644 index 0000000..4e29cad --- /dev/null +++ b/.github/workflows/publish-index.yml @@ -0,0 +1,67 @@ +--- + +name: Deploy Rules Index + +on: + push: + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build-and-publish: + container: + image: luzifer/archlinux + + defaults: + run: + shell: bash + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + runs-on: ubuntu-latest + + steps: + - name: Install required packages + run: | + pacman -Syy --noconfirm \ + git \ + python \ + python-pip \ + make \ + tar \ + yamllint \ + yq + + - name: Checkout + uses: actions/checkout@v3 + + - name: Lint rules files + run: make rules_lint + + - name: Build rules index + run: make index + + - name: Setup Pages + if: github.ref_name == 'main' + uses: actions/configure-pages@v2 + + - name: Upload artifact + if: github.ref_name == 'main' + uses: actions/upload-pages-artifact@v1 + + - name: Deploy to GitHub Pages + id: deployment + if: github.ref_name == 'main' + uses: actions/deploy-pages@v1 + +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3876d12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +_site +.venv diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a458134 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +OUTDIR :=_site +export RULE_BASE := https://github.com/Luzifer/twitch-bot-rules/raw/main/ + +default: + +rules_lint: + bash ci/lint.sh + +index: .venv + rm -rf $(OUTDIR) + mkdir $(OUTDIR) + ./.venv/bin/python ci/indexer.py \ + $(OUTDIR)/index.html \ + rules + +.venv: + python -m venv .venv + ./.venv/bin/pip install -r ci/requirements.yml diff --git a/ci/indexer.py b/ci/indexer.py new file mode 100644 index 0000000..c38da17 --- /dev/null +++ b/ci/indexer.py @@ -0,0 +1,68 @@ +import jinja2 +import os +import re +import sys +import yaml + +FIELD_REGEX = r"^# @([^ ]+)\s+(.*)$" + + +def get_doc_field(content, field, default=None): + matches = re.finditer(FIELD_REGEX, content, re.MULTILINE) + + fields = {} + + for matchNum, match in enumerate(matches, start=1): + fields[match.groups()[0]] = match.groups()[1] + + return fields[field] if field in fields else default + + +def get_rules_index(rules_dir): + rules = [] + + for file in os.listdir(rules_dir): + fullpath = '/'.join([rules_dir, file]) + with open(fullpath, 'r') as rulefile: + content = rulefile.read() + + rule = yaml.load(content, Loader=yaml.SafeLoader) + + rules.append({ + "actions": [x["type"] for x in rule["actions"]], + "author": get_doc_field(content, "author"), + "min_bot_ver": get_doc_field(content, "minBotVersion", "v3.x"), + "description": rule["description"], + "file": fullpath, + "shortened_id": rule["uuid"].split('-')[0], + "version": get_doc_field(content, "version", "v0"), + }) + + return sorted(rules, key=lambda x: x["description"]) + + +def main(args): + outfile = args[1] + rules_dir = args[2] + + rules = get_rules_index(rules_dir) + + render_index(outfile, rules) + + return 0 + + +def render_index(outfile, rules): + with open('index.tpl.html', 'r') as template_source: + env = jinja2.Environment() + tpl = env.from_string(template_source.read()) + + with open(outfile, 'w') as output: + output.write(tpl.render( + rule_base=os.environ['RULE_BASE'] if 'RULE_BASE' in os.environ else '/', + rules=rules, + )) + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/ci/lint.sh b/ci/lint.sh new file mode 100644 index 0000000..3039928 --- /dev/null +++ b/ci/lint.sh @@ -0,0 +1,44 @@ +set -euo pipefail + +exit_code=0 + +function error() { + log E "$@" + exit_code=1 +} + +function info() { + log I "$@" +} + +function log() { + local level=$1 + shift + echo "[$(date +%H:%M:%S)][$level] $@" >&2 +} + +required_tags=( + author + minBotVersion + version +) + +for rule_file in rules/*.yml; do + + info "Linting rules file ${rule_file}" + + info "+++ Checking with YAMLlint..." + yamllint -c ci/yamllint.yml ${rule_file} + + info "+++ Checking required tags..." + for tag in "${required_tags[@]}"; do + grep -Eq "^# @${tag} .+$" ${rule_file} || error "Missing required tag: ${tag}" + done + + info "+++ Checking subscription URL..." + exp_url="${RULE_BASE}${rule_file}" + sub_url="$(yq -r '.subscribe_from' ${rule_file})" + [[ $sub_url == $exp_url ]] || error "Wrong subscription URL: expected ${exp_url}" +done + +exit $exit_code diff --git a/ci/requirements.yml b/ci/requirements.yml new file mode 100644 index 0000000..9bdd4e1 --- /dev/null +++ b/ci/requirements.yml @@ -0,0 +1,3 @@ +Jinja2==3.1.2 +MarkupSafe==2.1.1 +PyYAML==6.0 diff --git a/ci/yamllint.yml b/ci/yamllint.yml new file mode 100644 index 0000000..34876f3 --- /dev/null +++ b/ci/yamllint.yml @@ -0,0 +1,51 @@ +--- +# See documentation for configuration / rules here: +# https://yamllint.readthedocs.io/en/stable/configuration.html +# +# - Put this file in ~/.config/yamllint/config to apply the rules +# - Install yamllint using `pip install yamllint` +# - Execute the check using `yamllint []` + +extends: default + +rules: + braces: + level: warning + min-spaces-inside: 1 + max-spaces-inside: 1 + min-spaces-inside-empty: 0 + max-spaces-inside-empty: 1 + brackets: + level: warning + colons: + level: warning + commas: + level: warning + document-end: + level: warning + present: true + document-start: + level: warning + present: true + empty-lines: + level: warning + hyphens: + level: warning + indentation: + level: warning + spaces: 2 + indent-sequences: true + check-multi-line-strings: true + key-duplicates: + level: error + line-length: + level: warning + allow-non-breakable-inline-mappings: true + new-line-at-end-of-file: + level: warning + trailing-spaces: + level: warning + +... + +# vim: set ft=yaml: diff --git a/index.tpl.html b/index.tpl.html new file mode 100644 index 0000000..4a95e3c --- /dev/null +++ b/index.tpl.html @@ -0,0 +1,60 @@ + + + Twitch-Bot Rules Archive + + + + + +
+
+
+ + + + + + + + {% for rule in rules %} + + + + + + {% endfor %} +
RuleDescriptionReq. Bot
Version
+ + {{ rule.shortened_id }}… + +

+ by {{ rule.author }}
+ ver {{ rule.version}} +

+
+

{{ rule.description }}

+

+ {% for action in rule.actions %} + {{ action }} + {% endfor %} +

+
{{ rule.min_bot_ver }}
+ +
+
+
+ + + + + diff --git a/rules/a136bb59-02f9-4a2a-88a2-be719655a7e2_wanna-become-famous.yml b/rules/a136bb59-02f9-4a2a-88a2-be719655a7e2_wanna-become-famous.yml new file mode 100644 index 0000000..5e3458e --- /dev/null +++ b/rules/a136bb59-02f9-4a2a-88a2-be719655a7e2_wanna-become-famous.yml @@ -0,0 +1,21 @@ +--- + +# @author Luzifer +# @minBotVersion v3.0 +# @version v1 + +uuid: a136bb59-02f9-4a2a-88a2-be719655a7e2 +subscribe_from: https://github.com/Luzifer/twitch-bot-rules/raw/main/rules/a136bb59-02f9-4a2a-88a2-be719655a7e2_wanna-become-famous.yml +description: 'Spam: "Wanna become famous? Buy followers, primes and viewers"' + +actions: + - type: delete + - type: ban + attributes: + reason: Chat-Spam "{{ group 1 }}" + +match_message: (?i)(.*(?:Buy|Best)?(?:\s+(?:followers|primes|viewers)\s*(?:,|and|)){2,}\s+on.*) + +disable_on_template: '{{ not (botHasBadge "moderator") }}' + +...