[mirror] a bluesky bot to post golang projects
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: move to urfave/cli/v3

split code from main into "cmd" package, add Config struct to move
settings around. get rid off package/main level vars mostly.

+136 -130
+35 -118
cmd/bot/main.go
··· 3 3 4 4 import ( 5 5 "context" 6 - "errors" 7 6 "fmt" 7 + "net/mail" 8 8 "os" 9 - "time" 10 9 11 10 "log/slog" 12 11 13 12 "github.com/minio/minio-go/v7" 14 13 "github.com/minio/minio-go/v7/pkg/credentials" 15 - bk "github.com/tailscale/go-bluesky" 16 - "github.com/till/golangoss-bluesky/internal/bluesky" 17 - "github.com/till/golangoss-bluesky/internal/content" 14 + "github.com/till/golangoss-bluesky/internal/cmd" 18 15 "github.com/till/golangoss-bluesky/internal/utils" 19 - "github.com/urfave/cli/v2" 16 + "github.com/urfave/cli/v3" 20 17 ) 21 18 22 19 var ( 23 20 blueskyHandle = "till+bluesky-golang@lagged.biz" 24 - blueskyAppKey = "" 25 21 26 22 // for cache 27 - awsEndpoint = "" 28 - awsAccessKeyID = "" 29 - awsSecretKey = "" 30 - cacheBucket = "golangoss-cache-bucket" 31 - 32 - // for github crawling 33 - githubToken = "" 34 - 35 - checkInterval time.Duration = 15 * time.Minute 36 - // How long to wait before retrying after a connection failure 37 - reconnectDelay time.Duration = 2 * time.Minute 23 + cacheBucket = "golangoss-cache-bucket" 38 24 ) 39 25 40 26 func init() { ··· 43 29 }))) 44 30 } 45 31 46 - // connectBluesky establishes a connection to Bluesky and logs in 47 - func connectBluesky(ctx context.Context) (*bk.Client, error) { 48 - client, err := bk.Dial(ctx, bk.ServerBskySocial) 49 - if err != nil { 50 - return nil, fmt.Errorf("failed to open connection: %v", err) 51 - } 52 - 53 - if err := client.Login(ctx, blueskyHandle, blueskyAppKey); err != nil { 54 - client.Close() 55 - switch { 56 - case errors.Is(err, bk.ErrMasterCredentials): 57 - return nil, fmt.Errorf("you're not allowed to use your full-access credentials, please create an appkey") 58 - case errors.Is(err, bk.ErrLoginUnauthorized): 59 - return nil, fmt.Errorf("username of application password seems incorrect, please double check") 60 - default: 61 - return nil, fmt.Errorf("login failed: %v", err) 62 - } 63 - } 64 - 65 - return client, nil 66 - } 67 - 68 - // runWithReconnect attempts to run the bot with automatic reconnection on failure 69 - func runWithReconnect(ctx context.Context, mc *minio.Client) error { 70 - for { 71 - client, err := connectBluesky(ctx) 72 - if err != nil { 73 - slog.Error("failed to connect to Bluesky", "error", err) 74 - slog.Info("retrying connection", "delay", reconnectDelay) 75 - time.Sleep(reconnectDelay) 76 - continue 77 - } 78 - 79 - c := bluesky.Client{ 80 - Client: client, 81 - } 82 - 83 - cacheClient := content.NewCacheClientS3(ctx, mc, cacheBucket) 84 - 85 - // Initialize and start the cleanup handler 86 - cleanup := content.NewS3Cleanup(mc, cacheBucket) 87 - cleanup.Start(ctx) 88 - defer cleanup.Stop() 89 - 90 - if err := content.Start(githubToken, cacheClient); err != nil { 91 - slog.Error("failed to start service", "error", err) 92 - client.Close() 93 - time.Sleep(reconnectDelay) 94 - continue 95 - } 96 - 97 - // Run the main loop 98 - for { 99 - slog.DebugContext(ctx, "checking...") 100 - if err := content.Do(ctx, c); err != nil { 101 - if !errors.Is(err, content.ErrCouldNotContent) { 102 - slog.Error("error during content check", "error", err) 103 - client.Close() 104 - time.Sleep(reconnectDelay) 105 - break 106 - } 107 - slog.DebugContext(ctx, "backing off...") 108 - } 109 - 110 - time.Sleep(checkInterval) 111 - } 112 - } 113 - } 114 - 115 32 func main() { 116 - bot := cli.App{ 33 + bot := cli.Command{ 117 34 Name: "golangoss-bluesky", 118 35 Description: "A little bot to post interesting Github projects to Bluesky", 119 36 HideVersion: true, 120 - Authors: []*cli.Author{ 121 - { 122 - Name: "Till Klampeckel", 123 - }, 37 + Authors: []any{ 38 + mail.Address{Name: "Till Klampeckel"}, 124 39 }, 125 40 Flags: []cli.Flag{ 126 41 &cli.StringFlag{ 127 - Name: "bluesky-app-key", 128 - EnvVars: []string{"BLUESKY_APP_KEY"}, 129 - Required: true, 130 - Destination: &blueskyAppKey, 42 + Name: "bluesky-app-key", 43 + Sources: cli.EnvVars("BLUESKY_APP_KEY"), 44 + Required: true, 131 45 }, 132 46 &cli.StringFlag{ 133 - Name: "aws-endpoint", 134 - EnvVars: []string{"AWS_ENDPOINT"}, 135 - Required: true, 136 - Destination: &awsEndpoint, 47 + Name: "aws-endpoint", 48 + Sources: cli.EnvVars("AWS_ENDPOINT"), 49 + Required: true, 137 50 }, 138 51 &cli.StringFlag{ 139 - Name: "aws-access-key-id", 140 - EnvVars: []string{"AWS_ACCESS_KEY_ID"}, 141 - Required: true, 142 - Destination: &awsAccessKeyID, 52 + Name: "aws-access-key-id", 53 + Sources: cli.EnvVars("AWS_ACCESS_KEY_ID"), 54 + Required: true, 143 55 }, 144 56 &cli.StringFlag{ 145 - Name: "aws-secret-key", 146 - EnvVars: []string{"AWS_SECRET_KEY"}, 147 - Required: true, 148 - Destination: &awsSecretKey, 57 + Name: "aws-secret-key", 58 + Sources: cli.EnvVars("AWS_SECRET_KEY"), 59 + Required: true, 149 60 }, 150 61 &cli.StringFlag{ 151 - Name: "github-token", 152 - EnvVars: []string{"GITHUB_TOKEN"}, 153 - Required: true, 154 - Destination: &githubToken, 62 + Name: "github-token", 63 + Sources: cli.EnvVars("GITHUB_TOKEN"), 64 + Required: true, 155 65 }, 156 66 }, 157 67 158 - Action: func(cCtx *cli.Context) error { 68 + Action: func(ctx context.Context, c *cli.Command) error { 159 69 // Initialize S3 client 160 - mc, err := minio.New(awsEndpoint, &minio.Options{ 161 - Creds: credentials.NewStaticV4(awsAccessKeyID, awsSecretKey, ""), 70 + mc, err := minio.New(c.String("aws-endpoint"), &minio.Options{ 71 + Creds: credentials.NewStaticV4(c.String("aws-access-key-id"), c.String("aws-secret-key"), ""), 162 72 Secure: true, 163 73 }) 164 74 if err != nil { ··· 166 76 } 167 77 168 78 // Ensure the bucket exists 169 - if err := mc.MakeBucket(cCtx.Context, cacheBucket, minio.MakeBucketOptions{}); err != nil { 79 + if err := mc.MakeBucket(ctx, cacheBucket, minio.MakeBucketOptions{}); err != nil { 170 80 return fmt.Errorf("failed to create bucket: %v", err) 171 81 } 172 82 173 - return runWithReconnect(cCtx.Context, mc) 83 + config := cmd.Config{ 84 + Handle: blueskyHandle, 85 + AppKey: c.String("bluesky-app-key"), 86 + CacheBucket: cacheBucket, 87 + GitHubToken: c.String("github-token"), 88 + } 89 + 90 + return cmd.RunWithReconnect(ctx, mc, config) 174 91 }, 175 92 } 176 93 177 - if err := bot.Run(os.Args); err != nil { 94 + if err := bot.Run(context.Background(), os.Args); err != nil { 178 95 utils.LogError(err) 179 96 os.Exit(1) 180 97 }
+1 -4
go.mod
··· 6 6 github.com/go-redis/redis/v8 v8.11.5 7 7 github.com/stretchr/testify v1.11.1 8 8 github.com/tailscale/go-bluesky v0.0.0-20241115170709-693553a07285 9 + github.com/urfave/cli/v3 v3.8.0 9 10 ) 10 11 11 12 require ( 12 13 github.com/carlmjohnson/versioninfo v0.22.5 // indirect 13 14 github.com/cespare/xxhash/v2 v2.3.0 // indirect 14 - github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect 15 15 github.com/davecgh/go-spew v1.1.1 // indirect 16 16 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 17 17 github.com/dustin/go-humanize v1.0.1 // indirect ··· 29 29 github.com/philhofer/fwd v1.2.0 // indirect 30 30 github.com/pmezard/go-difflib v1.0.0 // indirect 31 31 github.com/rs/xid v1.6.0 // indirect 32 - github.com/russross/blackfriday/v2 v2.1.0 // indirect 33 32 github.com/tinylib/msgp v1.6.1 // indirect 34 - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 35 33 github.com/zeebo/xxh3 v1.1.0 // indirect 36 34 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect 37 35 go.opentelemetry.io/otel v1.21.0 // indirect ··· 85 83 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 86 84 github.com/spaolacci/murmur3 v1.1.0 // indirect 87 85 github.com/ureeves/jwt-go-secp256k1 v0.2.0 // indirect 88 - github.com/urfave/cli/v2 v2.27.7 89 86 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 90 87 go.uber.org/atomic v1.11.0 // indirect 91 88 go.uber.org/multierr v1.11.0 // indirect
+2 -8
go.sum
··· 21 21 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 22 22 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 23 23 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 24 - github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= 25 - github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 26 24 github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 25 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 26 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= ··· 183 181 github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= 184 182 github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 185 183 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 186 - github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 187 - github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 188 184 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 189 185 github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 190 186 github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= ··· 206 202 github.com/ureeves/jwt-go-secp256k1 v0.2.0 h1:A2D2F5E8a+WxZdkO9YviVxA9aUo3IqSvA/4zQztOZ5A= 207 203 github.com/ureeves/jwt-go-secp256k1 v0.2.0/go.mod h1:7WMTEkrUxSM5PEesinVfdsiq5vu7kUJvLZUXBmL1svM= 208 204 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 209 - github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= 210 - github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= 205 + github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI= 206 + github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= 211 207 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 212 208 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 213 209 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 214 210 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 215 - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 216 - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 217 211 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 218 212 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 219 213 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+8
internal/cmd/config.go
··· 1 + package cmd 2 + 3 + type Config struct { 4 + Handle string 5 + AppKey string 6 + CacheBucket string 7 + GitHubToken string 8 + }
+90
internal/cmd/run.go
··· 1 + package cmd 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "fmt" 7 + "log/slog" 8 + "time" 9 + 10 + bk "github.com/tailscale/go-bluesky" 11 + 12 + "github.com/minio/minio-go/v7" 13 + "github.com/till/golangoss-bluesky/internal/bluesky" 14 + "github.com/till/golangoss-bluesky/internal/content" 15 + ) 16 + 17 + const ( 18 + checkInterval time.Duration = 15 * time.Minute 19 + // How long to wait before retrying after a connection failure 20 + reconnectDelay time.Duration = 2 * time.Minute 21 + ) 22 + 23 + // connectBluesky establishes a connection to Bluesky and logs in 24 + func connectBluesky(ctx context.Context, handle, appKey string) (*bk.Client, error) { 25 + client, err := bk.Dial(ctx, bk.ServerBskySocial) 26 + if err != nil { 27 + return nil, fmt.Errorf("failed to open connection: %v", err) 28 + } 29 + 30 + if err := client.Login(ctx, handle, appKey); err != nil { 31 + client.Close() 32 + switch { 33 + case errors.Is(err, bk.ErrMasterCredentials): 34 + return nil, fmt.Errorf("you're not allowed to use your full-access credentials, please create an appkey") 35 + case errors.Is(err, bk.ErrLoginUnauthorized): 36 + return nil, fmt.Errorf("username of application password seems incorrect, please double check") 37 + default: 38 + return nil, fmt.Errorf("login failed: %v", err) 39 + } 40 + } 41 + 42 + return client, nil 43 + } 44 + 45 + // RunWithReconnect attempts to run the bot with automatic reconnection on failure 46 + func RunWithReconnect(ctx context.Context, mc *minio.Client, cfg Config) error { 47 + for { 48 + client, err := connectBluesky(ctx, cfg.Handle, cfg.AppKey) 49 + if err != nil { 50 + slog.Error("failed to connect to Bluesky", "error", err) 51 + slog.Info("retrying connection", "delay", reconnectDelay) 52 + time.Sleep(reconnectDelay) 53 + continue 54 + } 55 + 56 + c := bluesky.Client{ 57 + Client: client, 58 + } 59 + 60 + cacheClient := content.NewCacheClientS3(ctx, mc, cfg.CacheBucket) 61 + 62 + // Initialize and start the cleanup handler 63 + cleanup := content.NewS3Cleanup(mc, cfg.CacheBucket) 64 + cleanup.Start(ctx) 65 + defer cleanup.Stop() 66 + 67 + if err := content.Start(cfg.GitHubToken, cacheClient); err != nil { 68 + slog.Error("failed to start service", "error", err) 69 + client.Close() 70 + time.Sleep(reconnectDelay) 71 + continue 72 + } 73 + 74 + // Run the main loop 75 + for { 76 + slog.DebugContext(ctx, "checking...") 77 + if err := content.Do(ctx, c); err != nil { 78 + if !errors.Is(err, content.ErrCouldNotContent) { 79 + slog.Error("error during content check", "error", err) 80 + client.Close() 81 + time.Sleep(reconnectDelay) 82 + break 83 + } 84 + slog.DebugContext(ctx, "backing off...") 85 + } 86 + 87 + time.Sleep(checkInterval) 88 + } 89 + } 90 + }