Initial Commit

This commit is contained in:
Elara 2024-01-09 17:36:20 -08:00
commit 68b25e0faf
7 changed files with 738 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/lemmy-reply-bot

109
config.go Normal file
View File

@ -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
}

51
go.mod Normal file
View File

@ -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
)

170
go.sum Normal file
View File

@ -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=

65
internal/db/db.go Normal file
View File

@ -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)
}
}

View File

@ -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
'''

288
main.go Normal file
View File

@ -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
}