From 68b25e0fafa38aa42a4f5da501b2561e21b551b1 Mon Sep 17 00:00:00 2001 From: Elara6331 Date: Tue, 9 Jan 2024 17:36:20 -0800 Subject: [PATCH] Initial Commit --- .gitignore | 1 + config.go | 109 +++++++++++++ go.mod | 51 +++++++ go.sum | 170 +++++++++++++++++++++ internal/db/db.go | 65 ++++++++ lemmy-reply-bot.example.toml | 54 +++++++ main.go | 288 +++++++++++++++++++++++++++++++++++ 7 files changed, 738 insertions(+) create mode 100644 .gitignore create mode 100644 config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/db/db.go create mode 100644 lemmy-reply-bot.example.toml create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cbfa375 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/lemmy-reply-bot \ No newline at end of file diff --git a/config.go b/config.go new file mode 100644 index 0000000..cccc154 --- /dev/null +++ b/config.go @@ -0,0 +1,109 @@ +package main + +import ( + "os" + "time" + + "github.com/pelletier/go-toml/v2" + "go.elara.ws/logger/log" + "go.elara.ws/pcre" + "go.elara.ws/salix" +) + +type Config struct { + File *ConfigFile + PollInterval time.Duration + Regexes map[string]*pcre.Regexp + Tmpls *salix.Namespace +} + +type ConfigFile struct { + Lemmy Lemmy `toml:"lemmy"` + Replies []Reply `toml:"reply"` +} + +type Lemmy struct { + InstanceURL string `toml:"instance_url"` + PollInterval string `toml:"poll_interval"` + Account LemmyAccount `toml:"account"` +} + +type LemmyAccount struct { + UserOrEmail string `toml:"user_or_email"` + Password string `toml:"password"` +} + +type Reply struct { + Regex string `toml:"regex"` + Template string `toml:"template"` +} + +func loadConfig(path string) (Config, error) { + fl, err := os.Open(path) + if err != nil { + return Config{}, err + } + + fi, err := fl.Stat() + if err != nil { + return Config{}, err + } + + if fi.Mode().Perm() != 0o600 { + log.Fatal("Your config file's permissions are insecure. Please use chmod to set them to 600. Refusing to start.").Send() + } + + cfgFile := &ConfigFile{Lemmy: Lemmy{PollInterval: "10s"}} + err = toml.NewDecoder(fl).Decode(cfgFile) + if err != nil { + return Config{}, err + } + + out := Config{File: cfgFile} + + out.Regexes, out.Tmpls, err = compileReplies(cfgFile.Replies) + if err != nil { + return Config{}, err + } + + out.PollInterval, err = time.ParseDuration(cfgFile.Lemmy.PollInterval) + if err != nil { + return Config{}, err + } + + return out, nil +} + +func compileReplies(replies []Reply) (map[string]*pcre.Regexp, *salix.Namespace, error) { + regexes := map[string]*pcre.Regexp{} + ns := salix.New().WithVarMap(map[string]any{ + "regexReplace": regexReplace, + }) + + for _, reply := range replies { + if _, ok := regexes[reply.Regex]; ok { + continue + } + + re, err := pcre.Compile(reply.Regex) + if err != nil { + return nil, nil, err + } + regexes[reply.Regex] = re + + _, err = ns.ParseString(reply.Regex, reply.Template) + if err != nil { + return nil, nil, err + } + } + + return regexes, ns, nil +} + +func regexReplace(str, pattern, new string) (string, error) { + re, err := pcre.Compile(pattern) + if err != nil { + return "", err + } + return re.ReplaceAllString(str, new), nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..20184cb --- /dev/null +++ b/go.mod @@ -0,0 +1,51 @@ +module go.elara.ws/lemmy-reply-bot + +go 1.21.5 + +require ( + github.com/chaisql/chai v0.16.0 + github.com/pelletier/go-toml/v2 v2.0.5 + github.com/spf13/pflag v1.0.5 + go.elara.ws/go-lemmy v0.19.0 + go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae + go.elara.ws/pcre v0.0.0-20230805032557-4ce849193f64 + go.elara.ws/salix v0.0.0-20240103024736-25037db86a10 +) + +require ( + github.com/DataDog/zstd v1.5.5 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cockroachdb/errors v1.11.1 // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/cockroachdb/pebble v1.0.0 // indirect + github.com/cockroachdb/redact v1.1.5 // indirect + github.com/getsentry/sentry-go v0.25.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-module/carbon/v2 v2.2.14 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gookit/color v1.5.1 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.17.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + modernc.org/libc v1.16.8 // indirect + modernc.org/mathutil v1.4.1 // indirect + modernc.org/memory v1.1.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..de1ddae --- /dev/null +++ b/go.sum @@ -0,0 +1,170 @@ +github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= +github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chaisql/chai v0.16.0 h1:UVvVOcf9H/OfSNRAzH9j1TuJnetUGGqV6gaAXZ8mrjQ= +github.com/chaisql/chai v0.16.0/go.mod h1:DYGursaN0/64vw3puP+ICq/sYr+TfdbKo9jmRax6J3Q= +github.com/cockroachdb/datadriven v1.0.3-0.20230801171734-e384cf455877 h1:1MLK4YpFtIEo3ZtMA5C795Wtv5VuUnrXX7mQG+aHg6o= +github.com/cockroachdb/datadriven v1.0.3-0.20230801171734-e384cf455877/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= +github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.0.0 h1:WZWlV/s78glZbY2ylUITDOWSVBD3cLjcWPLRPFbHNYg= +github.com/cockroachdb/pebble v1.0.0/go.mod h1:bynZ3gvVyhlvjLI7PT6dmZ7g76xzJ7HpxfjgkzCGz6s= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI= +github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-module/carbon/v2 v2.2.14 h1:mT2hpNoCQVnkboZ6iyRf7WCbXtZTRXFBvXXWMp0PaMc= +github.com/golang-module/carbon/v2 v2.2.14/go.mod h1:XDALX7KgqmHk95xyLeaqX9/LJGbfLATyruTziq68SZ8= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gookit/color v1.5.1 h1:Vjg2VEcdHpwq+oY63s/ksHrgJYCTo0bwWvmmYWdE9fQ= +github.com/gookit/color v1.5.1/go.mod h1:wZFzea4X8qN6vHOSP2apMb4/+w/orMznEzYsIHPaqKM= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.elara.ws/go-lemmy v0.19.0 h1:FdPfiA+8yOa2IhrLdBp8jdYbnY6H55bfwnBbiGr0OHg= +go.elara.ws/go-lemmy v0.19.0/go.mod h1:aZbF/4c1VA7qPXsP4Pth0ERu3HGZFPPl8bTY1ltBrcQ= +go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae h1:d+gJUhEWSrOjrrfgeydYWEr8TTnx0DLvcVhghaOsFeE= +go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae/go.mod h1:qng49owViqsW5Aey93lwBXONw20oGbJIoLVscB16mPM= +go.elara.ws/pcre v0.0.0-20230805032557-4ce849193f64 h1:QixGnJE1jP08Hs1G3rS7tZGd8DeBRtz9RBpk08WlGh4= +go.elara.ws/pcre v0.0.0-20230805032557-4ce849193f64/go.mod h1:EF48C6VnP4wBayzFGk6lXqbiLucH7EfiaYOgiiCe5k4= +go.elara.ws/salix v0.0.0-20240103024736-25037db86a10 h1:JOFqDTLiWO+WL/+BofQAd63pv1SfEawsGVbqnSVFf6E= +go.elara.ws/salix v0.0.0-20240103024736-25037db86a10/go.mod h1:niWia13iw7qDrS1C1mlqv5hxO1sunt8CcOQAB5yVlNU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.8 h1:Ux98PaOMvolgoFX/YwusFOHBnanXdGRmWgI8ciI2z4o= +modernc.org/libc v1.16.8/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000..de98e76 --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,65 @@ +package db + +import ( + "time" + + "github.com/chaisql/chai" +) + +var db *chai.DB + +// Init opens the database and applies migrations +func Init(path string) error { + g, err := chai.Open(path) + if err != nil { + return err + } + db = g + return db.Exec(` + CREATE TABLE IF NOT EXISTS replied_items ( + id INT NOT NULL PRIMARY KEY, + reply_id INT NOT NULL, + item_type TEXT NOT NULL CHECK ( item_type IN ['post', 'comment'] ), + updated TIMESTAMP NOT NULL, + UNIQUE(id, item_type) + ) + `) +} + +func Close() error { + return db.Close() +} + +type ItemType string + +const ( + Post ItemType = "post" + Comment ItemType = "comment" +) + +type Item struct { + ID int64 `chai:"id"` + ReplyID int64 `chai:"reply_id"` + ItemType ItemType `chai:"item_type"` + Updated time.Time `chai:"updated"` +} + +func AddItem(i Item) error { + return db.Exec(`INSERT INTO replied_items VALUES ?`, &i) +} + +func SetUpdatedTime(id int64, itemType ItemType, updated time.Time) error { + return db.Exec(`UPDATE replied_items SET updated = ? WHERE id = ? AND item_type = ?`, updated, id, itemType) +} + +func GetItem(id int64, itemType ItemType) (*Item, error) { + row, err := db.QueryRow(`SELECT * FROM replied_items WHERE id = ? AND item_type = ?`, id, itemType) + if chai.IsNotFoundError(err) { + return nil, nil + } else if err != nil { + return nil, err + } else { + out := &Item{} + return out, row.StructScan(out) + } +} diff --git a/lemmy-reply-bot.example.toml b/lemmy-reply-bot.example.toml new file mode 100644 index 0000000..1e8d949 --- /dev/null +++ b/lemmy-reply-bot.example.toml @@ -0,0 +1,54 @@ +[lemmy] +instance_url = "https://lemmy.ml" +poll_interval = "10s" + +[lemmy.account] +user_or_email = "user@example.com" +password = "ExamplePassword123" + +# Replies to any message starting with "!!BOT_TEST", with some information +# about what it's replying to +# +# Example: !!BOT_TEST Hello :3 +[[reply]] +regex = "!!BOT_TEST (.+)" +template = ''' +ID: #(id) \ +Type: #(type) \ +Content: #(matches[0][1]) +''' + + +# Returns archive links for URLs preceded with "!archive" +# +# Example: !archive https://gitea.elara.ws/Elara6331/lemmy-reply-bot +[[reply]] +regex = '!archive (https?)://([.\w\d]+\.[\w\d]{2,4}[\w\d?&=%/.-]*)' +msg = ''' +Here are the archive links you requested: + +#for(i, match in matches): +#if(len(matches) > 1):Link #(i+1):#!if +- [archive.vn](https://archive.vn/#(match[1])://#(match[2])) +- [archive.org](https://web.archive.org/web/#(match[1])://#(match[2])) +- [ghostarchive.org](https://ghostarchive.org/search?term=#(match[1])://#(match[2])) + +#!for +''' + +# Returns invidious links for YouTube URLs +# +# Example: https://www.youtube.com/watch?v=2vPhySbRETM +[[reply]] +regex = 'https?://(?:(?:www|m)\.)?youtu(?:\.be/|be\.com/(?:watch\?v=|shorts/))([\w\d-]{11})[&?]?([\w\d?&=%/-]*)' +msg = ''' +#(len(matches) == 1 ? "A YouTube link was" : "YouTube links were") detected in your #(type). Here are links to the same #(len(matches) == 1 ? "video" : "videos") on Invidious, which is a YouTube frontend that protects your privacy: + +#for(i, match in matches): +#if(len(matches) > 1):Link #(i+1):#!if +- [yewtu.be](https://yewtu.be/watch?v=$(match[1])&#(match[2])) +- [invidious.weblibre.org](https://invidious.weblibre.org/watch?v=#(match[1])&#(match[2])) +- [inv.vern.cc](https://inv.vern.cc/watch?v=#(match[1])&#(match[2])) + +#!for +''' diff --git a/main.go b/main.go new file mode 100644 index 0000000..6046732 --- /dev/null +++ b/main.go @@ -0,0 +1,288 @@ +package main + +import ( + "context" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/spf13/pflag" + "go.elara.ws/go-lemmy" + "go.elara.ws/lemmy-reply-bot/internal/db" + "go.elara.ws/logger" + "go.elara.ws/logger/log" + "go.elara.ws/salix" +) + +func init() { + log.Logger = logger.NewPretty(os.Stderr) +} + +func main() { + cfgPath := pflag.StringP("config-path", "c", "/etc/lemmy-reply-bot/config.toml", "Path to the config file") + dbPath := pflag.StringP("db-path", "d", "/etc/lemmy-reply-bot/replies", "Path to the ChaiSQL database") + pflag.Parse() + + ctx := context.Background() + ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) + defer cancel() + + err := db.Init(*dbPath) + if err != nil { + log.Fatal("Error initializing database").Err(err).Send() + } + + cfg, err := loadConfig(*cfgPath) + if err != nil { + log.Fatal("Error loading config").Err(err).Send() + } + + c, err := lemmy.New(cfg.File.Lemmy.InstanceURL) + if err != nil { + log.Fatal("Error creating new lemmy client").Err(err).Send() + } + + err = c.ClientLogin(ctx, lemmy.Login{ + UsernameOrEmail: cfg.File.Lemmy.Account.UserOrEmail, + Password: cfg.File.Lemmy.Account.Password, + }) + if err != nil { + log.Fatal("Error logging into lemmy").Err(err).Send() + } + + log.Info("Successfully logged in!").Send() + + go poll(ctx, cfg, c) + + <-ctx.Done() + _ = db.Close() +} + +func poll(ctx context.Context, cfg Config, c *lemmy.Client) { + for { + select { + case <-time.After(cfg.PollInterval): + // Get 20 of the newest comments from Lemmy + comments, err := c.Comments(ctx, lemmy.GetComments{ + Type: lemmy.NewOptional(lemmy.ListingTypeLocal), + Sort: lemmy.NewOptional(lemmy.CommentSortTypeNew), + Limit: lemmy.NewOptional[int64](20), + }) + if err != nil { + log.Warn("Error getting comments").Err(err).Send() + continue + } + + handleComments(ctx, comments.Comments, cfg, c) + + // Get 20 of the newest comments from Lemmy + posts, err := c.Posts(ctx, lemmy.GetPosts{ + Type: lemmy.NewOptional(lemmy.ListingTypeLocal), + Sort: lemmy.NewOptional(lemmy.SortTypeNew), + Limit: lemmy.NewOptional[int64](20), + }) + if err != nil { + log.Warn("Error getting posts").Err(err).Send() + continue + } + + handlePosts(ctx, posts.Posts, cfg, c) + case <-ctx.Done(): + return + } + } +} + +func handleComments(ctx context.Context, comments []lemmy.CommentView, cfg Config, c *lemmy.Client) { + for _, comment := range comments { + if !comment.Community.Local { + continue + } + + item, err := db.GetItem(comment.Comment.ID, db.Comment) + if err != nil { + log.Warn("Error getting comment from db").Err(err).Send() + continue + } + + edit := false + if item == nil { + // If the item is nil, it doesn't exist, which means we need to + // create a new reply, so we don't set edit to true in this case. + } else if item.Updated.Equal(comment.Comment.Updated) { + // If the item exists but hasn't been edited since we've last seen it, + // we can skip it since we've already replied to it. + continue + } else if item.Updated.Before(comment.Comment.Updated) { + // If the item exists and has been edited since we've last seen it, + // we need to edit it, so we set edit to true. + edit = true + } + + for i, reply := range cfg.File.Replies { + re := cfg.Regexes[reply.Regex] + if !re.MatchString(comment.Comment.Content) { + continue + } + + log.Info("Matched comment body"). + Int("reply-index", i). + Int64("comment-id", comment.Comment.ID). + Send() + + matches := re.FindAllStringSubmatch(comment.Comment.Content, -1) + content, err := executeTmpl(cfg.Tmpls, reply.Regex, map[string]any{ + "id": comment.Comment.ID, + "type": db.Comment, + "matches": matches, + }) + if err != nil { + log.Warn("Error executing template").Int("index", i).Err(err).Send() + continue + } + + if edit { + _, err = c.EditComment(ctx, lemmy.EditComment{ + CommentID: item.ReplyID, + Content: lemmy.NewOptional(content), + }) + if err != nil { + log.Warn("Error editing comment").Int64("id", item.ReplyID).Err(err).Send() + continue + } + + log.Info("Edited comment").Int64("parent-id", item.ID).Int64("reply-id", item.ReplyID).Send() + + err = db.SetUpdatedTime(comment.Comment.ID, db.Comment, comment.Comment.Updated) + if err != nil { + log.Warn("Error setting new updated time").Int64("id", item.ReplyID).Err(err).Send() + continue + } + } else { + cr, err := c.CreateComment(ctx, lemmy.CreateComment{ + PostID: comment.Comment.PostID, + Content: content, + ParentID: lemmy.NewOptional(comment.Comment.ID), + }) + if err != nil { + log.Warn("Error creating reply").Int64("comment-id", comment.Comment.ID).Err(err).Send() + continue + } + + log.Info("Created comment").Int64("parent-id", comment.Comment.ID).Int64("reply-id", cr.CommentView.Comment.ID).Send() + + err = db.AddItem(db.Item{ + ID: comment.Comment.ID, + ReplyID: cr.CommentView.Comment.ID, + ItemType: db.Comment, + Updated: comment.Comment.Updated, + }) + if err != nil { + log.Warn("Error adding reply to database").Int64("id", item.ReplyID).Err(err).Send() + continue + } + } + } + } +} + +func handlePosts(ctx context.Context, posts []lemmy.PostView, cfg Config, c *lemmy.Client) { + for _, post := range posts { + if !post.Community.Local { + continue + } + + item, err := db.GetItem(post.Post.ID, db.Post) + if err != nil { + log.Warn("Error getting comment from db").Err(err).Send() + continue + } + + edit := false + if item == nil { + // If the item is nil, it doesn't exist, which means we need to + // reply to it, so we don't set edit to true in this case. + } else if item.Updated.Equal(post.Post.Updated) { + // If the item exists but hasn't been edited since we've last seen it, + // we can skip it since we've already replied to it. + continue + } else if item.Updated.Before(post.Post.Updated) { + // If the item exists and has been edited since we've last seen it, + // we need to edit it, so we set edit to true. + edit = true + } + + for i, reply := range cfg.File.Replies { + re := cfg.Regexes[reply.Regex] + content := post.Post.URL.ValueOrZero() + "\n\n" + post.Post.Body.ValueOrZero() + if !re.MatchString(content) { + continue + } + + log.Info("Matched post body"). + Int("reply-index", i). + Int64("post-id", post.Post.ID). + Send() + + matches := re.FindAllStringSubmatch(content, -1) + content, err := executeTmpl(cfg.Tmpls, reply.Regex, map[string]any{ + "id": post.Post.ID, + "type": db.Post, + "matches": matches, + }) + if err != nil { + log.Warn("Error executing template").Int("index", i).Err(err).Send() + continue + } + + if edit { + _, err = c.EditComment(ctx, lemmy.EditComment{ + CommentID: item.ReplyID, + Content: lemmy.NewOptional(content), + }) + if err != nil { + log.Warn("Error editing post").Int64("id", item.ReplyID).Err(err).Send() + continue + } + + log.Info("Edited comment").Int64("post-id", item.ID).Int64("reply-id", item.ReplyID).Send() + + err = db.SetUpdatedTime(post.Post.ID, db.Post, post.Post.Updated) + if err != nil { + log.Warn("Error setting new updated time").Int64("id", item.ReplyID).Err(err).Send() + continue + } + } else { + cr, err := c.CreateComment(ctx, lemmy.CreateComment{ + PostID: post.Post.ID, + Content: content, + }) + if err != nil { + log.Warn("Error creating reply").Int64("post-id", post.Post.ID).Err(err).Send() + continue + } + + log.Info("Created comment").Int64("post-id", post.Post.ID).Int64("reply-id", cr.CommentView.Comment.ID).Send() + + err = db.AddItem(db.Item{ + ID: post.Post.ID, + ReplyID: cr.CommentView.Comment.ID, + ItemType: db.Post, + Updated: post.Post.Updated, + }) + if err != nil { + log.Warn("Error adding reply to database").Int64("id", item.ReplyID).Err(err).Send() + continue + } + } + } + } +} + +func executeTmpl(ns *salix.Namespace, name string, vars map[string]any) (string, error) { + sb := &strings.Builder{} + err := ns.ExecuteTemplate(sb, name, vars) + return sb.String(), err +}