diff --git a/README.md b/README.md index 7cd3ea4..9755110 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Lemmy Reply Bot -This project is a simple bot that replies to comments on Lemmy. Every 10 seconds, it fetches the 200 newest comments from your configured Lemmy instance, and sees if they match any regex configured in the config file. If it finds one that does, it replies with the message corresponding to that regex. +This project is a simple bot that replies to comments on Lemmy. It uses Lemmy's WebSocket to get notified of any new comments, and sees if they match any regex configured in the config file. If it finds one that does, it replies with the message corresponding to that regex. ### Configuration diff --git a/go.mod b/go.mod index 771a53c..a45737b 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,10 @@ 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/go-lemmy v0.0.0-20221213021222-a4a015a4cc2a go.arsenm.dev/logger v0.0.0-20221007032343-cbffce4f4334 go.arsenm.dev/pcre v0.0.0-20220530205550-74594f6c8b0e ) @@ -18,8 +17,10 @@ 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/gorilla/websocket v1.4.2 // indirect + github.com/jpillora/backoff v1.0.0 // indirect github.com/mattn/go-isatty v0.0.14 // indirect + github.com/recws-org/recws v1.4.0 // 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 diff --git a/go.sum b/go.sum index dec0bba..9fb55e8 100644 --- a/go.sum +++ b/go.sum @@ -11,12 +11,10 @@ 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/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 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= @@ -25,6 +23,8 @@ github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvI 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/recws-org/recws v1.4.0 h1:y9LLddtAicjejikNZXiaY9DQjIwcAQ82acd1XU6n0lU= +github.com/recws-org/recws v1.4.0/go.mod h1:7+NQkTmBdU98VSzkzq9/P7+X0xExioUVBx9OeRKQIkk= 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= @@ -32,7 +32,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An 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= @@ -46,8 +45,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV 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/go-lemmy v0.0.0-20221213021222-a4a015a4cc2a h1:iBeRt2sU8Y3pwKPKTFnkIcTwFTvifyCLUtjjsj7CaWM= +go.arsenm.dev/go-lemmy v0.0.0-20221213021222-a4a015a4cc2a/go.mod h1:/XgX+cQqs9a2VJWwZ67wArjK2CVXWKdEEpHdAFbFRNQ= 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= diff --git a/main.go b/main.go index ab6a2dd..3bd7217 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,6 @@ import ( "syscall" "time" - "github.com/hashicorp/go-retryablehttp" "github.com/spf13/pflag" "github.com/vmihailenco/msgpack/v5" "go.arsenm.dev/go-lemmy" @@ -31,10 +30,7 @@ func main() { log.Fatal("Error loading config file").Err(err).Send() } - rhc := retryablehttp.NewClient() - rhc.Logger = retryableLogger{} - - c, err := lemmy.NewWithClient(cfg.Lemmy.InstanceURL, rhc.StandardClient()) + c, err := lemmy.NewWebSocket(cfg.Lemmy.InstanceURL) if err != nil { log.Fatal("Error creating new Lemmy API client").Err(err).Send() } @@ -49,6 +45,18 @@ func main() { log.Info("Successfully logged in to Lemmy instance").Send() + err = c.Request(types.UserOpUserJoin, nil) + if err != nil { + log.Fatal("Error joining WebSocket user context").Err(err).Send() + } + + err = c.Request(types.UserOpCommunityJoin, types.CommunityJoin{ + CommunityID: 0, + }) + if err != nil { + log.Fatal("Error joining WebSocket community context").Err(err).Send() + } + replyCh := make(chan replyJob, 200) if !*dryRun { @@ -58,7 +66,7 @@ func main() { commentWorker(ctx, c, replyCh) } -func commentWorker(ctx context.Context, c *lemmy.Client, replyCh chan<- replyJob) { +func commentWorker(ctx context.Context, c *lemmy.WSClient, replyCh chan<- replyJob) { repliedIDs := map[int]struct{}{} repliedStore, err := os.Open("replied.bin") @@ -74,38 +82,38 @@ func commentWorker(ctx context.Context, c *lemmy.Client, replyCh chan<- replyJob 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 - } + case res := <-c.Responses(): + // Check which operation has been sent from the server + switch res.Op { + case types.UserOpCreateComment, types.UserOpEditComment: + var cr types.CommentResponse + err = lemmy.DecodeResponse(res.Data, &cr) + if err != nil { + log.Warn("Error while trying to decode comment").Err(err).Send() + continue + } - for _, commentView := range cr.Comments { - if _, ok := repliedIDs[commentView.Comment.ID]; ok { + if _, ok := repliedIDs[cr.CommentView.Comment.ID]; ok { continue } for i, reply := range cfg.Replies { re := compiledRegexes[reply.Regex] - if !re.MatchString(commentView.Comment.Content) { + if !re.MatchString(cr.CommentView.Comment.Content) { continue } log.Info("Matched comment body"). Int("reply-index", i). - Int("comment-id", commentView.Comment.ID). + Int("comment-id", cr.CommentView.Comment.ID). Send() job := replyJob{ - CommentID: commentView.Comment.ID, - PostID: commentView.Comment.PostID, + CommentID: cr.CommentView.Comment.ID, + PostID: cr.CommentView.Comment.PostID, } - matches := re.FindStringSubmatch(commentView.Comment.Content) + matches := re.FindStringSubmatch(cr.CommentView.Comment.Content) job.Content = expandStr(reply.Msg, func(s string) string { i, err := strconv.Atoi(s) if err != nil { @@ -124,7 +132,7 @@ func commentWorker(ctx context.Context, c *lemmy.Client, replyCh chan<- replyJob replyCh <- job - repliedIDs[commentView.Comment.ID] = struct{}{} + repliedIDs[cr.CommentView.Comment.ID] = struct{}{} } } case <-ctx.Done(): @@ -151,11 +159,11 @@ type replyJob struct { PostID int } -func commentReplyWorker(ctx context.Context, c *lemmy.Client, ch <-chan replyJob) { +func commentReplyWorker(ctx context.Context, c *lemmy.WSClient, ch <-chan replyJob) { for { select { case reply := <-ch: - cr, err := c.CreateComment(ctx, types.CreateComment{ + err := c.Request(types.UserOpCreateComment, types.CreateComment{ PostID: reply.PostID, ParentID: types.NewOptional(reply.CommentID), Content: reply.Content, @@ -167,11 +175,7 @@ func commentReplyWorker(ctx context.Context, c *lemmy.Client, ch <-chan replyJob 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 }