package main import ( "bytes" "context" "fmt" "image" "image/draw" "image/jpeg" "image/png" "io" "log" "os" "os/signal" "path/filepath" "syscall" comatproto "github.com/bluesky-social/indigo/api/atproto" appbsky "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/atproto/atclient" "github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util" "github.com/coder/websocket" "github.com/joho/godotenv" xdraw "golang.org/x/image/draw" ) // PumpkinHead context for increasing the pumpkin head/ type PumpkinHead struct { bodyImg image.Image headImg image.Image callCount int scaleStep float64 } func (p *PumpkinHead) Reset() { p.callCount = 0 } func (p *PumpkinHead) GetCallCount() int { return p.callCount } // NewPumpkinHead commence pumpkinhead func NewPumpkinHead(bodyPath, headPath string) (*PumpkinHead, error) { // Load body image bodyFile, err := os.Open(bodyPath) if err != nil { return nil, fmt.Errorf("failed to open body image: %w", err) } defer bodyFile.Close() bodyImg, err := jpeg.Decode(bodyFile) if err != nil { return nil, fmt.Errorf("failed to decode body image: %w", err) } // Load head image headFile, err := os.Open(headPath) if err != nil { return nil, fmt.Errorf("failed to open head image: %w", err) } defer headFile.Close() headImg, err := png.Decode(headFile) if err != nil { return nil, fmt.Errorf("failed to decode head image: %w", err) } return &PumpkinHead{ bodyImg: bodyImg, headImg: headImg, callCount: 0, scaleStep: 0.01, // How much my big head grows each call }, nil } // GrowHead Increases the pumpkin head by values in PumpkinHead.scaleStep func (p *PumpkinHead) GrowHead() (image.Image, error) { p.callCount++ // Calculate new scale (starts at 1.0, grows by scaleStep each time) scale := 1.0 + (float64(p.callCount) * p.scaleStep) fmt.Printf("Call #%d: Growing head to %.1fx original size\n", p.callCount, scale) bodyBounds := p.bodyImg.Bounds() headBounds := p.headImg.Bounds() newHeadWidth := int(float64(headBounds.Dx()) * scale) newHeadHeight := int(float64(headBounds.Dy()) * scale) scaledHead := image.NewRGBA(image.Rect(0, 0, newHeadWidth, newHeadHeight)) // Scale the head using bilinear interpolation xdraw.BiLinear.Scale(scaledHead, scaledHead.Bounds(), p.headImg, headBounds, draw.Over, nil) output := image.NewRGBA(bodyBounds) // Drawing the base layer image draw.Draw(output, bodyBounds, p.bodyImg, image.Point{0, 0}, draw.Src) // Calculate position to center the head on top of the body // Place head in upper portion of the image headX := (bodyBounds.Dx() - newHeadWidth) / 2 headY := bodyBounds.Dy()/8 - newHeadHeight/2 + 350 // the 350 is to line up with my head headPos := image.Rect(headX, headY, headX+newHeadWidth, headY+newHeadHeight) // Draw scaled head on top draw.Draw(output, headPos, scaledHead, image.Point{0, 0}, draw.Over) return output, nil } // CompressImage compresses the image to JPEG format with the given quality (1-100) func CompressImage(img image.Image, quality int) (*bytes.Buffer, error) { buf := new(bytes.Buffer) err := jpeg.Encode(buf, img, &jpeg.Options{Quality: quality}) if err != nil { return nil, fmt.Errorf("failed to compress image: %w", err) } return buf, nil } // saveToFiles saves the image to a file. Mostly used for testing func saveToFiles(pumpkin *PumpkinHead, outputDir string, newPic image.Image) { // Create output directory if it doesn't exist if err := os.MkdirAll(outputDir, 0755); err != nil { writeErrorAndExit(err) } filename := filepath.Join(outputDir, fmt.Sprintf("bighead_%03d.jpg", pumpkin.callCount)) outFile, err := os.Create(filename) if err != nil { writeErrorAndExit(err) } defer outFile.Close() compressed, err := CompressImage(newPic, 80) if err != nil { writeErrorAndExit(err) } if _, err := outFile.Write(compressed.Bytes()); err != nil { writeErrorAndExit(err) } } // AuthSession stores the auth session data. ty goat type AuthSession struct { DID syntax.DID `json:"did"` Password string `json:"password"` AccessToken string `json:"access_token"` RefreshToken string `json:"session_token"` PDS string `json:"pds"` } // AtProtoStuff stores the atproto stuff. or more correctly the context of auth and a authenticated client type AtProtoStuff struct { authSession AuthSession Client *atclient.APIClient } // Login logs into their PDS and sets AtProtoStuff func Login(ctx context.Context, host, did, password string) (*AtProtoStuff, error) { client, err := atclient.LoginWithPasswordHost(ctx, host, did, password, "", nil) if err != nil { return nil, err } passAuth, ok := client.Auth.(*atclient.PasswordAuth) if !ok { return nil, fmt.Errorf("expected password auth") } sess := AuthSession{ DID: passAuth.Session.AccountDID, PDS: passAuth.Session.Host, Password: password, AccessToken: passAuth.Session.AccessToken, RefreshToken: passAuth.Session.RefreshToken, } return &AtProtoStuff{ authSession: sess, Client: client, }, nil } // loadAuthClient ty again goat func (a *AtProtoStuff) loadAuthClient(ctx context.Context) (*atclient.APIClient, error) { // first try to resume session client := atclient.ResumePasswordSession(atclient.PasswordSessionData{ AccessToken: a.authSession.AccessToken, RefreshToken: a.authSession.RefreshToken, AccountDID: a.authSession.DID, Host: a.authSession.PDS, }, a.authRefreshCallback) // check that auth is working _, err := comatproto.ServerGetSession(ctx, client) if nil == err { return client, nil } return atclient.LoginWithPasswordHost(ctx, a.authSession.PDS, a.authSession.DID.String(), a.authSession.Password, "", nil) } // authRefreshCallback is called when the auth session expires and refreshes the auth session func (a *AtProtoStuff) authRefreshCallback(ctx context.Context, data atclient.PasswordSessionData) { fmt.Println("auth refresh callback") a.authSession.DID = data.AccountDID a.authSession.AccessToken = data.AccessToken a.authSession.RefreshToken = data.RefreshToken a.authSession.PDS = data.Host } func (a *AtProtoStuff) updatePumpkinHead(ctx context.Context, image io.Reader) (err error) { client, err := a.loadAuthClient(ctx) if err != nil { return err } profileNsid := "app.bsky.actor.profile" selfKey := "self" currentProfileResp, err := comatproto.RepoGetRecord( ctx, client, "", profileNsid, client.AccountDID.String(), selfKey) if err != nil { return err } profile := currentProfileResp.Value.Val.(*appbsky.ActorProfile) blobUploadResp, err := comatproto.RepoUploadBlob(ctx, a.Client, image) if err != nil { return err } profile.Avatar = blobUploadResp.Blob _, err = comatproto.RepoPutRecord(ctx, a.Client, &comatproto.RepoPutRecord_Input{ Collection: profileNsid, Repo: a.Client.AccountDID.String(), Rkey: selfKey, SwapRecord: currentProfileResp.Cid, Record: &lexutil.LexiconTypeDecoder{ Val: profile, }, }) if err != nil { return err } return nil } func writeErrorAndExit(err error) { _, err = fmt.Fprintf(os.Stderr, "Error: %v\n", err) if err != nil { panic(err) } os.Exit(1) } func egoWatcher(ctx context.Context, conn *websocket.Conn, atProtoStuff *AtProtoStuff, pumpkin *PumpkinHead) { for { _, _, err := conn.Read(ctx) if err != nil { if websocket.CloseStatus(err) == websocket.StatusNormalClosure { log.Println("Connection closed normally") return } log.Println("Read error:", err) return } fmt.Println("Someone liked one of your posts. Inflating ego") newHeadWhoDis, err := pumpkin.GrowHead() if err != nil { writeErrorAndExit(err) } compressed, err := CompressImage(newHeadWhoDis, 80) if err != nil { log.Printf("Failed to compress image: %v", err) continue } if err := atProtoStuff.updatePumpkinHead(ctx, compressed); err != nil { log.Printf("Failed to update Bluesky profile: %v", err) } } } func main() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() err := godotenv.Load() if err != nil { writeErrorAndExit(err) } usersDid := os.Getenv("DID") pdsHost := os.Getenv("PDS_HOST") appPassword := os.Getenv("APP_PASSWORD") fmt.Fprintf(os.Stderr, "Logging with DID %s to %s\n", usersDid, pdsHost) var atProtoStuff *AtProtoStuff atProtoStuff, err = Login(ctx, pdsHost, usersDid, appPassword) if err != nil { writeErrorAndExit(err) } //Resets back to a baseline pumpkinhead ogPumpkinHead, err := os.Open("./base_images/base_big_head.jpg") if err != nil { writeErrorAndExit(err) } err = atProtoStuff.updatePumpkinHead(ctx, ogPumpkinHead) if err != nil { writeErrorAndExit(err) } //fmt.Println(app_password) spaceDustUrl := fmt.Sprintf("wss://spacedust.microcosm.blue/subscribe?wantedSubjectDids=%s&wantedSources=app.bsky.feed.like:subject.uri", usersDid) pumpkin, err := NewPumpkinHead("base_images/pumpkin_body_cropped.jpg", "base_images/better_pumpkin_head.png") if err != nil { writeErrorAndExit(err) } fmt.Println("Pumpkin Head Grower!") fmt.Println("==================") conn, _, err := websocket.Dial(ctx, spaceDustUrl, nil) if err != nil { log.Fatal(err) } defer conn.Close(websocket.StatusNormalClosure, "done") fmt.Println("Connected to spacedust!") // Watching for likes, like a hawk go egoWatcher(ctx, conn, atProtoStuff, pumpkin) // Wait for stop signal <-ctx.Done() fmt.Println("\nStopping... Check the 'bighead' directory for results.") }