package main import ( "context" "encoding/json" "fmt" "net/http" "os" _ "github.com/joho/godotenv/autoload" "tangled.org/bnewbold.net/atwork-cli/placeatwork" "github.com/bluesky-social/indigo/api/agnostic" 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/identity" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/urfave/cli/v3" ) func main() { app := cli.Command{ Name: "atwork-cli", Usage: "example CLI project for at://work lexicons", Commands: []*cli.Command{ &cli.Command{ Name: "search", Usage: "queries public API for listings", ArgsUsage: "", Flags: []cli.Flag{ &cli.StringFlag{ Name: "host", Value: "https://atwork.place", }, }, Action: runSearch, }, &cli.Command{ Name: "import-bsky-profile", Usage: "creates a new atwork profile using Bluesky profile data", Flags: []cli.Flag{ &cli.StringFlag{ Name: "username", Sources: cli.EnvVars("ATP_USERNAME"), }, &cli.StringFlag{ Name: "password", Sources: cli.EnvVars("ATP_PASSWORD"), }, }, Action: runImportProfile, }, &cli.Command{ Name: "upload-resume", Usage: "uploads resume PDF and adds to atwork profile", ArgsUsage: "", Flags: []cli.Flag{ &cli.StringFlag{ Name: "username", Sources: cli.EnvVars("ATP_USERNAME"), }, &cli.StringFlag{ Name: "password", Sources: cli.EnvVars("ATP_PASSWORD"), }, }, Action: runUploadResume, }, }, } if err := app.Run(context.Background(), os.Args); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } } func runSearch(ctx context.Context, cmd *cli.Command) error { if !cmd.Args().Present() { return fmt.Errorf("requires a search query") } searchQuery := cmd.Args().First() c := atclient.NewAPIClient(cmd.String("host")) resp, err := placeatwork.SearchListings(ctx, c, searchQuery) if err != nil { return err } for _, hit := range resp.Listings { if hit.Value == nil { continue } listing := *hit.Value aturi, err := syntax.ParseATURI(hit.Uri) if err != nil { continue } fmt.Printf("%s: %s\n", aturi.Authority(), listing.Title) if listing.ApplyLink != nil { fmt.Printf("\tApply: %s\n", *listing.ApplyLink) } } return nil } func runImportProfile(ctx context.Context, cmd *cli.Command) error { if !cmd.IsSet("username") || !cmd.IsSet("password") { return fmt.Errorf("authentication required (username and password)") } username, err := syntax.ParseAtIdentifier(cmd.String("username")) if err != nil { return fmt.Errorf("invalid username: %w", err) } dir := identity.DefaultDirectory() c, err := atclient.LoginWithPassword(ctx, dir, *username, cmd.String("password"), "", nil) if err != nil { return err } // fetch existing bsky profile resp, err := agnostic.RepoGetRecord(ctx, c, "", "app.bsky.actor.profile", c.AccountDID.String(), "self") if err != nil { return fmt.Errorf("failed fetching bsky profile record: %s", err) } var bskyProfile appbsky.ActorProfile if err := json.Unmarshal(*resp.Value, &bskyProfile); err != nil { return err } // create new atwork profile record atworkProfile := placeatwork.Profile{ LexiconTypeID: "place.atwork.profile", Avatar: bskyProfile.Avatar, Banner: bskyProfile.Banner, Description: bskyProfile.Description, DisplayName: bskyProfile.DisplayName, } // create profile record (fails if one already exists) if err := c.Post(ctx, syntax.NSID("com.atproto.repo.createRecord"), map[string]any{ "repo": c.AccountDID, "collection": atworkProfile.LexiconTypeID, "rkey": "self", "record": atworkProfile, }, nil); err != nil { return err } return nil } func runUploadResume(ctx context.Context, cmd *cli.Command) error { if !cmd.Args().Present() { return fmt.Errorf("requires resume PDF path as argument") } resumePath := cmd.Args().First() if !cmd.IsSet("username") || !cmd.IsSet("password") { return fmt.Errorf("authentication required (username and password)") } username, err := syntax.ParseAtIdentifier(cmd.String("username")) if err != nil { return fmt.Errorf("invalid username: %w", err) } dir := identity.DefaultDirectory() c, err := atclient.LoginWithPassword(ctx, dir, *username, cmd.String("password"), "", nil) if err != nil { return err } // fetch existing at://work profile getResp, err := agnostic.RepoGetRecord(ctx, c, "", "place.atwork.profile", c.AccountDID.String(), "self") if err != nil { return fmt.Errorf("failed fetching profile record: %s", err) } var atworkProfile placeatwork.Profile if err := json.Unmarshal(*getResp.Value, &atworkProfile); err != nil { return err } // upload resume PDF as blob resumeFile, err := os.Open(resumePath) if err != nil { return err } defer resumeFile.Close() // update record req := atclient.NewAPIRequest(http.MethodPost, syntax.NSID("com.atproto.repo.uploadBlob"), resumeFile) req.Headers.Set("Accept", "application/json") req.Headers.Set("Content-Type", "application/pdf") resp, err := c.Do(ctx, req) if err != nil { return err } defer resp.Body.Close() if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { return fmt.Errorf("blob upload failed") } var blobResp comatproto.RepoUploadBlob_Output if err := json.NewDecoder(resp.Body).Decode(&blobResp); err != nil { return fmt.Errorf("failed decoding JSON response body: %w", err) } // update profile with blob ref atworkProfile.Resume = blobResp.Blob if err := c.Post(ctx, syntax.NSID("com.atproto.repo.putRecord"), map[string]any{ "repo": c.AccountDID, "collection": atworkProfile.LexiconTypeID, "rkey": "self", "record": atworkProfile, }, nil); err != nil { return err } return nil }