lexicon devex tutorial
at main 6.0 kB view raw
1package main 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "os" 9 10 _ "github.com/joho/godotenv/autoload" 11 12 "tangled.org/bnewbold.net/atwork-cli/placeatwork" 13 14 "github.com/bluesky-social/indigo/api/agnostic" 15 comatproto "github.com/bluesky-social/indigo/api/atproto" 16 appbsky "github.com/bluesky-social/indigo/api/bsky" 17 "github.com/bluesky-social/indigo/atproto/atclient" 18 "github.com/bluesky-social/indigo/atproto/identity" 19 "github.com/bluesky-social/indigo/atproto/syntax" 20 "github.com/urfave/cli/v3" 21) 22 23func main() { 24 app := cli.Command{ 25 Name: "atwork-cli", 26 Usage: "example CLI project for at://work lexicons", 27 Commands: []*cli.Command{ 28 &cli.Command{ 29 Name: "search", 30 Usage: "queries public API for listings", 31 ArgsUsage: "<query>", 32 Flags: []cli.Flag{ 33 &cli.StringFlag{ 34 Name: "host", 35 Value: "https://atwork.place", 36 }, 37 }, 38 Action: runSearch, 39 }, 40 &cli.Command{ 41 Name: "import-bsky-profile", 42 Usage: "creates a new atwork profile using Bluesky profile data", 43 Flags: []cli.Flag{ 44 &cli.StringFlag{ 45 Name: "username", 46 Sources: cli.EnvVars("ATP_USERNAME"), 47 }, 48 &cli.StringFlag{ 49 Name: "password", 50 Sources: cli.EnvVars("ATP_PASSWORD"), 51 }, 52 }, 53 Action: runImportProfile, 54 }, 55 &cli.Command{ 56 Name: "upload-resume", 57 Usage: "uploads resume PDF and adds to atwork profile", 58 ArgsUsage: "<pdf-path>", 59 Flags: []cli.Flag{ 60 &cli.StringFlag{ 61 Name: "username", 62 Sources: cli.EnvVars("ATP_USERNAME"), 63 }, 64 &cli.StringFlag{ 65 Name: "password", 66 Sources: cli.EnvVars("ATP_PASSWORD"), 67 }, 68 }, 69 Action: runUploadResume, 70 }, 71 }, 72 } 73 74 if err := app.Run(context.Background(), os.Args); err != nil { 75 fmt.Fprintf(os.Stderr, "error: %v\n", err) 76 os.Exit(1) 77 } 78} 79 80func runSearch(ctx context.Context, cmd *cli.Command) error { 81 if !cmd.Args().Present() { 82 return fmt.Errorf("requires a search query") 83 } 84 searchQuery := cmd.Args().First() 85 86 c := atclient.NewAPIClient(cmd.String("host")) 87 88 resp, err := placeatwork.SearchListings(ctx, c, searchQuery) 89 if err != nil { 90 return err 91 } 92 93 for _, hit := range resp.Listings { 94 if hit.Value == nil { 95 continue 96 } 97 listing := *hit.Value 98 aturi, err := syntax.ParseATURI(hit.Uri) 99 if err != nil { 100 continue 101 } 102 fmt.Printf("%s: %s\n", aturi.Authority(), listing.Title) 103 if listing.ApplyLink != nil { 104 fmt.Printf("\tApply: %s\n", *listing.ApplyLink) 105 } 106 } 107 108 return nil 109} 110 111func runImportProfile(ctx context.Context, cmd *cli.Command) error { 112 113 if !cmd.IsSet("username") || !cmd.IsSet("password") { 114 return fmt.Errorf("authentication required (username and password)") 115 } 116 117 username, err := syntax.ParseAtIdentifier(cmd.String("username")) 118 if err != nil { 119 return fmt.Errorf("invalid username: %w", err) 120 } 121 dir := identity.DefaultDirectory() 122 123 c, err := atclient.LoginWithPassword(ctx, dir, *username, cmd.String("password"), "", nil) 124 if err != nil { 125 return err 126 } 127 128 // fetch existing bsky profile 129 resp, err := agnostic.RepoGetRecord(ctx, c, "", "app.bsky.actor.profile", c.AccountDID.String(), "self") 130 if err != nil { 131 return fmt.Errorf("failed fetching bsky profile record: %s", err) 132 } 133 134 var bskyProfile appbsky.ActorProfile 135 if err := json.Unmarshal(*resp.Value, &bskyProfile); err != nil { 136 return err 137 } 138 139 // create new atwork profile record 140 atworkProfile := placeatwork.Profile{ 141 LexiconTypeID: "place.atwork.profile", 142 Avatar: bskyProfile.Avatar, 143 Banner: bskyProfile.Banner, 144 Description: bskyProfile.Description, 145 DisplayName: bskyProfile.DisplayName, 146 } 147 148 // create profile record (fails if one already exists) 149 if err := c.Post(ctx, syntax.NSID("com.atproto.repo.createRecord"), map[string]any{ 150 "repo": c.AccountDID, 151 "collection": atworkProfile.LexiconTypeID, 152 "rkey": "self", 153 "record": atworkProfile, 154 }, nil); err != nil { 155 return err 156 } 157 158 return nil 159} 160 161func runUploadResume(ctx context.Context, cmd *cli.Command) error { 162 163 if !cmd.Args().Present() { 164 return fmt.Errorf("requires resume PDF path as argument") 165 } 166 resumePath := cmd.Args().First() 167 168 if !cmd.IsSet("username") || !cmd.IsSet("password") { 169 return fmt.Errorf("authentication required (username and password)") 170 } 171 172 username, err := syntax.ParseAtIdentifier(cmd.String("username")) 173 if err != nil { 174 return fmt.Errorf("invalid username: %w", err) 175 } 176 dir := identity.DefaultDirectory() 177 178 c, err := atclient.LoginWithPassword(ctx, dir, *username, cmd.String("password"), "", nil) 179 if err != nil { 180 return err 181 } 182 183 // fetch existing at://work profile 184 getResp, err := agnostic.RepoGetRecord(ctx, c, "", "place.atwork.profile", c.AccountDID.String(), "self") 185 if err != nil { 186 return fmt.Errorf("failed fetching profile record: %s", err) 187 } 188 189 var atworkProfile placeatwork.Profile 190 if err := json.Unmarshal(*getResp.Value, &atworkProfile); err != nil { 191 return err 192 } 193 194 // upload resume PDF as blob 195 resumeFile, err := os.Open(resumePath) 196 if err != nil { 197 return err 198 } 199 defer resumeFile.Close() 200 201 // update record 202 req := atclient.NewAPIRequest(http.MethodPost, syntax.NSID("com.atproto.repo.uploadBlob"), resumeFile) 203 req.Headers.Set("Accept", "application/json") 204 req.Headers.Set("Content-Type", "application/pdf") 205 206 resp, err := c.Do(ctx, req) 207 if err != nil { 208 return err 209 } 210 defer resp.Body.Close() 211 212 if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { 213 return fmt.Errorf("blob upload failed") 214 } 215 216 var blobResp comatproto.RepoUploadBlob_Output 217 if err := json.NewDecoder(resp.Body).Decode(&blobResp); err != nil { 218 return fmt.Errorf("failed decoding JSON response body: %w", err) 219 } 220 221 // update profile with blob ref 222 atworkProfile.Resume = blobResp.Blob 223 if err := c.Post(ctx, syntax.NSID("com.atproto.repo.putRecord"), map[string]any{ 224 "repo": c.AccountDID, 225 "collection": atworkProfile.LexiconTypeID, 226 "rkey": "self", 227 "record": atworkProfile, 228 }, nil); err != nil { 229 return err 230 } 231 232 return nil 233}