commit fb17fa26504616fb37d77bee87774a8e289e909a Author: Elara Musayelyan Date: Sat Dec 10 20:08:29 2022 -0800 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..240f63e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/lemmy-reply-bot.toml +/lemmy-reply-bot +/replied.bin \ No newline at end of file diff --git a/config.go b/config.go new file mode 100644 index 0000000..d9c24bb --- /dev/null +++ b/config.go @@ -0,0 +1,90 @@ +package main + +import ( + "net/url" + "os" + + "github.com/pelletier/go-toml/v2" + "go.arsenm.dev/logger/log" + "go.arsenm.dev/pcre" +) + +type Config struct { + Lemmy struct { + InstanceURL string `toml:"instanceURL"` + Account struct { + UserOrEmail string `toml:"userOrEmail"` + Password string `toml:"password"` + } `toml:"account"` + } `toml:"lemmy"` + Replies []Reply `toml:"reply"` +} + +type Reply struct { + Regex string `toml:"regex"` + Msg string `toml:"msg"` +} + +var ( + cfg = Config{} + compiledRegexes = map[string]*pcre.Regexp{} +) + +func loadConfig(path string) error { + fi, err := os.Stat(path) + if err != nil { + return 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() + } + + fl, err := os.Open(path) + if err != nil { + return err + } + + err = toml.NewDecoder(fl).Decode(&cfg) + if err != nil { + return err + } + + err = compileRegexes(cfg.Replies) + if err != nil { + return err + } + + validateConfig() + return nil +} + +func compileRegexes(replies []Reply) error { + for _, reply := range replies { + if _, ok := compiledRegexes[reply.Regex]; ok { + continue + } + + re, err := pcre.Compile(reply.Regex) + if err != nil { + return err + } + compiledRegexes[reply.Regex] = re + } + return nil +} + +func validateConfig() { + _, err := url.Parse(cfg.Lemmy.InstanceURL) + if err != nil { + log.Fatal("Lemmy instance URL is not valid").Err(err).Send() + } + + for i, reply := range cfg.Replies { + re := compiledRegexes[reply.Regex] + + if re.MatchString(reply.Msg) { + log.Fatal("Regular expression matches message. This may create an infinite loop. Refusing to start.").Int("reply-index", i).Send() + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..771a53c --- /dev/null +++ b/go.mod @@ -0,0 +1,30 @@ +module go.arsenm.dev/lemmy-reply-bot + +go 1.19 + +//replace go.arsenm.dev/go-lemmy => /home/arsen/Code/go-lemmy + +require ( + github.com/hashicorp/go-retryablehttp v0.7.1 + github.com/pelletier/go-toml/v2 v2.0.6 + github.com/spf13/pflag v1.0.5 + github.com/vmihailenco/msgpack/v5 v5.3.5 + go.arsenm.dev/go-lemmy v0.0.0-20221210234052-7fc04591ba51 + go.arsenm.dev/logger v0.0.0-20221007032343-cbffce4f4334 + go.arsenm.dev/pcre v0.0.0-20220530205550-74594f6c8b0e +) + +require ( + 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/hashicorp/go-cleanhttp v0.5.1 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect + golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // 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..dec0bba --- /dev/null +++ b/go.sum @@ -0,0 +1,104 @@ +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/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= +github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +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/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.arsenm.dev/go-lemmy v0.0.0-20221210234052-7fc04591ba51 h1:RoQ4KR1kmm0jOosJpPjXUnAQShMMsjHsS+QnvKy8D5c= +go.arsenm.dev/go-lemmy v0.0.0-20221210234052-7fc04591ba51/go.mod h1:bDUHw1QFZtjygM5DdsvVnBY1xP3YmLiAzlBvZOdfe8A= +go.arsenm.dev/logger v0.0.0-20221007032343-cbffce4f4334 h1:S98LJOBmj1pAKSw94spJk6+n8ERlBNTxi4lt5B67nQo= +go.arsenm.dev/logger v0.0.0-20221007032343-cbffce4f4334/go.mod h1:RV2qydKDdoyaRkhAq8JEGvojR8eJ6bjq5WnSIlH7gYw= +go.arsenm.dev/pcre v0.0.0-20220530205550-74594f6c8b0e h1:4XwLmFDvAKt7ZvS3E3hD2R++0wr75fBUEvXkK9dLXzk= +go.arsenm.dev/pcre v0.0.0-20220530205550-74594f6c8b0e/go.mod h1:c/E0D60A6rRLoDLh6mLUdFV9gxyth+CnXnqGHos2CAQ= +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/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-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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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/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-20201124115921-2c860bdd6e78/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= +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/lemmy-reply-bot.example.toml b/lemmy-reply-bot.example.toml new file mode 100644 index 0000000..66adc3e --- /dev/null +++ b/lemmy-reply-bot.example.toml @@ -0,0 +1,12 @@ +[lemmy] +instanceURL = "https://lemmy.ml" + +[lemmy.account] +userOrEmail = "user@example.com" +password = "ExamplePassword123" + +# Replies to any message starting with "!!BOT_TEST" with everything +# after "!!BOT_TEST" +[[reply]] +regex = "!!BOT_TEST (.*)" +msg = "$1" \ No newline at end of file diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..e27d7d5 --- /dev/null +++ b/logger.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + "os" + + "go.arsenm.dev/logger" + "go.arsenm.dev/logger/log" +) + +func init() { + l := logger.NewPretty(os.Stderr) + + if os.Getenv("LEMMY_REPLY_BOT_DEBUG") == "1" { + l.Level = logger.LogLevelDebug + } + + log.Logger = l +} + +type retryableLogger struct{} + +func (retryableLogger) Error(msg string, v ...any) { + msgs := splitMsgs(v) + log.Error(msg). + Str("method", msgs["method"].(string)). + Stringer("url", msgs["url"].(fmt.Stringer)). + Send() +} + +func (retryableLogger) Info(msg string, v ...any) { + msgs := splitMsgs(v) + log.Info(msg). + Str("method", msgs["method"].(string)). + Stringer("url", msgs["url"].(fmt.Stringer)). + Send() +} + +func (retryableLogger) Debug(msg string, v ...any) { + msgs := splitMsgs(v) + log.Debug(msg). + Str("method", msgs["method"].(string)). + Stringer("url", msgs["url"].(fmt.Stringer)). + Send() +} + +func (retryableLogger) Warn(msg string, v ...any) { + msgs := splitMsgs(v) + log.Warn(msg). + Str("method", msgs["method"].(string)). + Stringer("url", msgs["url"].(fmt.Stringer)). + Send() +} + +func splitMsgs(v []any) map[string]any { + out := map[string]any{} + + for i, val := range v { + if (i+1)%2 == 0 { + continue + } + + out[val.(string)] = v[i+1] + } + + return out +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..3ff039b --- /dev/null +++ b/main.go @@ -0,0 +1,186 @@ +package main + +import ( + "context" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + "time" + + "github.com/hashicorp/go-retryablehttp" + "github.com/spf13/pflag" + "github.com/vmihailenco/msgpack/v5" + "go.arsenm.dev/go-lemmy" + "go.arsenm.dev/go-lemmy/types" + "go.arsenm.dev/logger/log" +) + +func main() { + configPath := pflag.StringP("config", "c", "./lemmy-reply-bot.toml", "Path to the config file") + dryRun := pflag.BoolP("dry-run", "D", false, "Don't actually send comments, just check for matches") + pflag.Parse() + + ctx := context.Background() + ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + err := loadConfig(*configPath) + if err != nil { + log.Fatal("Error loading config file").Err(err).Send() + } + + rhc := retryablehttp.NewClient() + rhc.Logger = retryableLogger{} + + c, err := lemmy.NewWithClient(cfg.Lemmy.InstanceURL, rhc.StandardClient()) + if err != nil { + log.Fatal("Error creating new Lemmy API client").Err(err).Send() + } + + err = c.Login(ctx, types.Login{ + UsernameOrEmail: cfg.Lemmy.Account.UserOrEmail, + Password: cfg.Lemmy.Account.Password, + }) + if err != nil { + log.Fatal("Error logging in to Lemmy instance").Err(err).Send() + } + + log.Info("Successfully logged in to Lemmy instance").Send() + + replyCh := make(chan replyJob, 200) + + if !*dryRun { + go commentReplyWorker(ctx, c, replyCh) + } + + commentWorker(ctx, c, replyCh) +} + +func commentWorker(ctx context.Context, c *lemmy.Client, replyCh chan<- replyJob) { + repliedIDs := map[int]struct{}{} + + repliedStore, err := os.Open("replied.bin") + if err == nil { + err = msgpack.NewDecoder(repliedStore).Decode(&repliedIDs) + if err != nil { + log.Warn("Error decoding reply store").Err(err).Send() + } + repliedStore.Close() + } + + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + cr, err := c.Comments(ctx, types.GetComments{ + Sort: types.NewOptional(types.CommentSortNew), + Limit: types.NewOptional(200), + }) + if err != nil { + log.Warn("Error while trying to get comments").Err(err).Send() + continue + } + + for _, commentView := range cr.Comments { + if _, ok := repliedIDs[commentView.Comment.ID]; ok { + continue + } + + for i, reply := range cfg.Replies { + re := compiledRegexes[reply.Regex] + if !re.MatchString(commentView.Comment.Content) { + continue + } + + log.Info("Matched comment body"). + Int("reply-index", i). + Int("comment-id", commentView.Comment.ID). + Send() + + job := replyJob{ + CommentID: commentView.Comment.ID, + PostID: commentView.Comment.PostID, + } + + matches := re.FindStringSubmatch(commentView.Comment.Content) + job.Content = expandStr(reply.Msg, func(s string) string { + i, err := strconv.Atoi(s) + if err != nil { + return "" + } + + if len(matches) > i+1 { + return "" + } + + return matches[i] + }) + + replyCh <- job + + repliedIDs[commentView.Comment.ID] = struct{}{} + } + } + case <-ctx.Done(): + repliedStore, err := os.Create("replied.bin") + if err != nil { + log.Warn("Error creating reply store file").Err(err).Send() + return + } + + err = msgpack.NewEncoder(repliedStore).Encode(repliedIDs) + if err != nil { + log.Warn("Error encoding replies to reply store").Err(err).Send() + } + + repliedStore.Close() + return + } + } +} + +type replyJob struct { + Content string + CommentID int + PostID int +} + +func commentReplyWorker(ctx context.Context, c *lemmy.Client, ch <-chan replyJob) { + for { + select { + case reply := <-ch: + cr, err := c.CreateComment(ctx, types.CreateComment{ + PostID: reply.PostID, + ParentID: types.NewOptional(reply.CommentID), + Content: reply.Content, + }) + if err != nil { + log.Warn("Error while trying to create new comment").Err(err).Send() + } + + log.Info("Created new comment"). + Int("post-id", reply.PostID). + Int("parent-id", reply.CommentID). + Int("comment-id", cr.CommentView.Comment.ID). + Send() + + // Make sure requests don't happen too quickly + time.Sleep(1 * time.Second) + case <-ctx.Done(): + return + } + } +} + +func expandStr(s string, mapping func(string) string) string { + strings.ReplaceAll(s, "$$", "${_escaped_dollar_symbol}") + return os.Expand(s, func(s string) string { + if s == "_escaped_dollar_symbol" { + return "$" + } + return mapping(s) + }) +}