Skip to content

Commit

Permalink
Initial commit: basic bot and policy list parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
tulir committed Aug 31, 2024
0 parents commit 8905903
Show file tree
Hide file tree
Showing 20 changed files with 1,194 additions and 0 deletions.
18 changes: 18 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
root = true

[*]
indent_style = tab
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

[*.{yaml,yml,sql}]
indent_style = space

[{.gitlab-ci.yml,.github/workflows/*.yml}]
indent_size = 2
36 changes: 36 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Go

on: [push, pull_request]

jobs:
lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go-version: ["1.22", "1.23"]
name: Lint ${{ matrix.go-version == '1.23' && '(latest)' || '(old)' }}

steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: true

- name: Install libolm
run: sudo apt-get install libolm-dev libolm3

- name: Install dependencies
run: |
go install golang.org/x/tools/cmd/goimports@latest
go install honnef.co/go/tools/cmd/staticcheck@latest
export PATH="$HOME/go/bin:$PATH"
- name: Install pre-commit
run: pip install pre-commit

- name: Lint
run: pre-commit run -a
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.idea

*.yaml
!.pre-commit-config.yaml
!example-config.yaml

*.session
*.json
*.db
*.log
*.log.gz
*.bak

/meowlnir
/start
3 changes: 3 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include:
- project: 'mautrix/ci'
file: '/go.yml'
26 changes: 26 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
exclude_types: [markdown]
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files

- repo: https://github.com/tekwizely/pre-commit-golang
rev: v1.0.0-rc.1
hooks:
- id: go-imports-repo
args:
- "-local"
- "go.mau.fi/meowlnir"
- "-w"
- id: go-vet-repo-mod
- id: go-staticcheck-repo-mod

- repo: https://github.com/beeper/pre-commit-go
rev: v0.3.1
hooks:
- id: zerolog-ban-msgf
- id: zerolog-use-stringer
10 changes: 10 additions & 0 deletions Dockerfile.ci
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM alpine:3.20

RUN apk add --no-cache ca-certificates jq curl

ARG EXECUTABLE=./meowlnir
COPY $EXECUTABLE /usr/bin/meowlnir
VOLUME /data
WORKDIR /data

CMD ["/usr/bin/meowlnir"]
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Meowlnir
Work in progress: A Matrix moderation bot.

## Documentation
It's not ready yet.

## Discussion
Matrix room: [#meowlnir:maunium.net](https://matrix.to/#/#meowlnir:maunium.net)
4 changes: 4 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
MAUTRIX_VERSION=$(cat go.mod | grep 'maunium.net/go/mautrix ' | awk '{ print $2 }')
GO_LDFLAGS="-X main.Tag=$(git describe --exact-match --tags 2>/dev/null) -X main.Commit=$(git rev-parse HEAD) -X 'main.BuildTime=`date -Iseconds`' -X 'maunium.net/go/mautrix.GoModVersion=$MAUTRIX_VERSION'"
go build -ldflags="-s -w $GO_LDFLAGS" "$@"
119 changes: 119 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package main

import (
_ "embed"
"fmt"
"os"

up "go.mau.fi/util/configupgrade"
"go.mau.fi/util/dbutil"
"go.mau.fi/util/random"
"go.mau.fi/zeroconfig"
"gopkg.in/yaml.v3"
"maunium.net/go/mautrix/id"
)

//go:embed example-config.yaml
var ExampleConfig string

type HomeserverConfig struct {
Address string `yaml:"address"`
Domain string `yaml:"domain"`
}

type BotConfig struct {
Username string `yaml:"username"`
Displayname string `yaml:"displayname"`
AvatarURL id.ContentURI `yaml:"avatar_url"`
}

type AppserviceConfig struct {
ID string `yaml:"id"`
ASToken string `yaml:"as_token"`
HSToken string `yaml:"hs_token"`
Bot BotConfig `yaml:"bot"`
PickleKey string `yaml:"pickle_key"`
}

type ServerConfig struct {
Address string `yaml:"address"`
Hostname string `yaml:"hostname"`
Port uint16 `yaml:"port"`
}

type Config struct {
Homeserver HomeserverConfig `yaml:"homeserver"`
Appservice AppserviceConfig `yaml:"appservice"`
Server ServerConfig `yaml:"server"`
Database dbutil.Config `yaml:"database"`
SynapseDB dbutil.Config `yaml:"synapse_db"`
Logging zeroconfig.Config `yaml:"logging"`
}

func generateOrCopy(helper up.Helper, path ...string) {
if secret, ok := helper.Get(up.Str, path...); !ok || secret == "generate" {
helper.Set(up.Str, random.String(64), path...)
} else {
helper.Copy(up.Str, path...)
}
}

func upgradeConfig(helper up.Helper) {
helper.Copy(up.Str, "homeserver", "address")
helper.Copy(up.Str, "homeserver", "domain")

helper.Copy(up.Str, "appservice", "id")
generateOrCopy(helper, "appservice", "as_token")
generateOrCopy(helper, "appservice", "hs_token")
helper.Copy(up.Str, "appservice", "bot", "username")
helper.Copy(up.Str|up.Null, "appservice", "bot", "displayname")
helper.Copy(up.Str|up.Null, "appservice", "bot", "avatar_url")
generateOrCopy(helper, "appservice", "pickle_key")

helper.Copy(up.Str, "server", "address")
helper.Copy(up.Str, "server", "hostname")
helper.Copy(up.Int, "server", "port")

helper.Copy(up.Str, "database", "type")
helper.Copy(up.Str, "database", "uri")
helper.Copy(up.Int, "database", "max_open_conns")
helper.Copy(up.Int, "database", "max_idle_conns")
helper.Copy(up.Str|up.Null, "database", "max_conn_idle_time")
helper.Copy(up.Str|up.Null, "database", "max_conn_lifetime")

helper.Copy(up.Str, "synapse_db", "type")
helper.Copy(up.Str, "synapse_db", "uri")
helper.Copy(up.Int, "synapse_db", "max_open_conns")
helper.Copy(up.Int, "synapse_db", "max_idle_conns")
helper.Copy(up.Str|up.Null, "synapse_db", "max_conn_idle_time")
helper.Copy(up.Str|up.Null, "synapse_db", "max_conn_lifetime")

helper.Copy(up.Map, "logging")
}

var SpacedBlocks = [][]string{
{"appservice"},
{"server"},
{"database"},
{"synapse_db"},
{"logging"},
}

func loadConfig(path string, noSave bool) *Config {
configData, _, err := up.Do(path, !noSave, &up.StructUpgrader{
SimpleUpgrader: upgradeConfig,
Blocks: SpacedBlocks,
Base: ExampleConfig,
})
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to upgrade config:", err)
os.Exit(10)
}
var config Config
err = yaml.Unmarshal(configData, &config)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to parse config:", err)
os.Exit(10)
}
return &config
}
82 changes: 82 additions & 0 deletions eventhandling.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package main

import (
"context"
"fmt"
"strings"
"time"

"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"

"go.mau.fi/meowlnir/policylist"
)

func (m *Meowlnir) AddEventHandlers() {
m.EventProcessor.On(event.StatePolicyUser, m.UpdatePolicyList)
m.EventProcessor.On(event.StatePolicyRoom, m.UpdatePolicyList)
m.EventProcessor.On(event.StatePolicyServer, m.UpdatePolicyList)
m.EventProcessor.On(event.StateLegacyPolicyUser, m.UpdatePolicyList)
m.EventProcessor.On(event.StateLegacyPolicyRoom, m.UpdatePolicyList)
m.EventProcessor.On(event.StateLegacyPolicyServer, m.UpdatePolicyList)
m.EventProcessor.On(event.StateUnstablePolicyUser, m.UpdatePolicyList)
m.EventProcessor.On(event.StateUnstablePolicyRoom, m.UpdatePolicyList)
m.EventProcessor.On(event.StateUnstablePolicyServer, m.UpdatePolicyList)
m.EventProcessor.On(event.StateMember, m.HandleMember)
m.EventProcessor.On(event.EventMessage, m.HandleCommand)
}

func (m *Meowlnir) UpdatePolicyList(ctx context.Context, evt *event.Event) {
added, removed := m.PolicyStore.Update(evt)
fmt.Println(added, removed)
}

const tempAdmin = "@tulir:maunium.net"

func (m *Meowlnir) HandleMember(ctx context.Context, evt *event.Event) {
content, ok := evt.Content.Parsed.(*event.MemberEventContent)
if !ok {
return
}
if evt.Sender == tempAdmin && evt.GetStateKey() == m.Client.UserID.String() && content.Membership == event.MembershipInvite {
m.Client.JoinRoomByID(ctx, evt.RoomID)
m.Client.State(ctx, evt.RoomID)
}
}

func (m *Meowlnir) LoadBanList(ctx context.Context, roomID id.RoomID) (*policylist.Room, error) {
state, err := m.Client.State(ctx, roomID)
if err != nil {
return nil, fmt.Errorf("failed to get room state: %w", err)
}
m.PolicyStore.Add(roomID, state)
return nil, nil
}

func (m *Meowlnir) HandleCommand(ctx context.Context, evt *event.Event) {
if evt.Sender != tempAdmin {
return
}
m.Client.State(ctx, evt.RoomID)
fields := strings.Fields(evt.Content.AsMessage().Body)
cmd := fields[0]
args := fields[1:]
switch strings.ToLower(cmd) {
case "!join":
m.Client.JoinRoomByID(ctx, id.RoomID(args[0]))
case "!load":
_, err := m.LoadBanList(ctx, id.RoomID(args[0]))
if err != nil {
m.Client.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Failed to load ban list: %v", err))
} else {
m.Client.SendNotice(ctx, evt.RoomID, "Ban list loaded")
}
case "!match":
match := m.PolicyStore.MatchUser(nil, id.UserID(args[0]))
if match != nil {
m.Client.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Matched: %s set recommendation %s for %s at %s: %s", match.RawEvent.Sender, match.Recommendation, match.Entity, time.UnixMilli(match.RawEvent.Timestamp), match.Reason))
} else {
m.Client.SendNotice(ctx, evt.RoomID, "No match")
}
}
}
Loading

0 comments on commit 8905903

Please sign in to comment.