this repo has no description

progress

Changed files
+160 -93
lexicons
org.user-intents
+6 -5
base.html
··· 6 6 <meta name="referrer" content="origin-when-cross-origin"> 7 7 <meta name="viewport" content="width=device-width, initial-scale=1"> 8 8 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.purple.min.css"> 9 - <title>atproto OAuth demo (indigo)</title> 9 + <title>ATProto User Intents Demo</title> 10 10 </head> 11 11 <body> 12 12 <header> 13 13 <hgroup> 14 - <h1>atproto OAuth demo (indigo)</h1> 14 + <h1>ATProto User Intents Demo</h1> 15 15 {{ if false }} 16 + <!-- XXX --> 16 17 <p>Hello <span style="font-family: monospace;">@{{ "handle" }}</span>!</p> 17 18 {{ end }} 18 19 </hgroup> 19 20 <nav> 20 21 <ul> 21 22 {{ if false }} 22 - <li><a href="/bsky/post">Create Post</a> 23 - <li><a href="/oauth/refresh">Refresh Token</a> 23 + <!-- XXX --> 24 + <li><a href="/intents">Configure Intents</a> 24 25 <li><a href="/oauth/logout">Logout</a> 25 26 {{ else }} 26 27 <li><a href="/oauth/login">Login</a> 27 28 {{ end }} 28 - <li><a href="https://github.com/bluesky-social/indigo/tree/main/atproto/auth/oauth">Code</a> 29 + <li><a href="https://tangled.sh/@bnewbold.net/user-intents">Code</a> 29 30 </ul> 30 31 </nav> 31 32 </header>
+30
declaration.go
··· 1 + package main 2 + 3 + type GetDeclarationResp struct { 4 + Cid *string `json:"cid,omitempty"` 5 + Uri string `json:"uri"` 6 + Value Declaration `json:"value"` 7 + } 8 + 9 + type PutDeclarationBody struct { 10 + Repo string `json:"repo"` 11 + Collection string `json:"collection"` 12 + Rkey string `json:"rkey,omitempty"` 13 + SwapCommit *string `json:"swapCommit,omitempty"` 14 + Validate *bool `json:"validate,omitempty"` 15 + Record Declaration `json:"record"` 16 + } 17 + 18 + type DeclarationIntent struct { 19 + Value *bool `json:"value,omitempty"` 20 + UpdatedAt string `json:"updatedAt"` 21 + } 22 + 23 + type Declaration struct { 24 + Type string `json:"$type"` 25 + UpdatedAt string `json:"updatedAt,omitempty"` 26 + SyntheticContentGeneration *DeclarationIntent `json:"syntheticContentGeneration,omitempty"` 27 + PublicAccessArchive *DeclarationIntent `json:"publicAccessArchive,omitempty"` 28 + BulkDataset *DeclarationIntent `json:"bulkDataset,omitempty"` 29 + ProtocolBridging *DeclarationIntent `json:"protocolBridging,omitempty"` 30 + }
+34
intents.html
··· 1 + {{ define "content" }} 2 + <form method="post"> 3 + 4 + <label for="syntheticContentGeneration-select">Synthetic Concent Generation:</label> 5 + <select name="syntheticContentGeneration" id="syntheticContentGeneration-select"> 6 + <option value="undefined">Undefined</option> 7 + <option value="allow">Allow</option> 8 + <option value="disallow">Disallow</option> 9 + </select> 10 + 11 + <label for="publicAccessArchive-select">Public Access Archiving:</label> 12 + <select name="publicAccessArchive" id="publicAccessArchive-select"> 13 + <option value="undefined">Undefined</option> 14 + <option value="allow">Allow</option> 15 + <option value="disallow">Disallow</option> 16 + </select> 17 + 18 + <label for="bulkDataset-select">Inclusion in Bulk Datasets:</label> 19 + <select name="bulkDataset" id="bulkDataset-select"> 20 + <option value="undefined">Undefined</option> 21 + <option value="allow">Allow</option> 22 + <option value="disallow">Disallow</option> 23 + </select> 24 + 25 + <label for="protocolBridging-select">Protocol Bridging:</label> 26 + <select name="protocolBridging" id="protocolBridging-select"> 27 + <option value="undefined">Undefined</option> 28 + <option value="allow">Allow</option> 29 + <option value="disallow">Disallow</option> 30 + </select> 31 + 32 + <input type="submit" value="Update Intents"> 33 + </form> 34 + {{ end }}
+2 -2
lexicons/org.user-intents/demo/declaration.json
··· 25 25 "ref": "#intent", 26 26 "description": "Public access to or replay of account data as part of archiving and preservation efforts" 27 27 }, 28 - "syntheticContentGeneration": { 28 + "bulkDataset": { 29 29 "type": "ref", 30 30 "ref": "#intent", 31 31 "description": "Inclusion of account data in bulk 'snapshot' datasets which are publicly redistributed, even if only for a fixed time period" 32 32 }, 33 - "syntheticContentGeneration": { 33 + "protocolBridging": { 34 34 "type": "ref", 35 35 "ref": "#intent", 36 36 "description": "Bridging account data or interactions into distinct social web protocol ecosystems"
+88 -80
main.go
··· 12 12 _ "github.com/joho/godotenv/autoload" 13 13 14 14 "github.com/bluesky-social/indigo/atproto/auth/oauth" 15 - "github.com/bluesky-social/indigo/atproto/crypto" 16 15 "github.com/bluesky-social/indigo/atproto/identity" 17 16 "github.com/bluesky-social/indigo/atproto/syntax" 18 17 ··· 37 36 Usage: "public host name for this client (if not localhost dev mode)", 38 37 EnvVars: []string{"CLIENT_HOSTNAME"}, 39 38 }, 40 - &cli.StringFlag{ 41 - Name: "client-secret-key", 42 - Usage: "confidential client secret key. should be P-256 private key in multibase encoding", 43 - EnvVars: []string{"CLIENT_SECRET_KEY"}, 44 - }, 45 - &cli.StringFlag{ 46 - Name: "client-secret-key-id", 47 - Usage: "key id for client-secret-key", 48 - Value: "primary", 49 - EnvVars: []string{"CLIENT_SECRET_KEY_ID"}, 50 - }, 51 39 }, 52 40 } 53 41 h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}) ··· 72 60 var tmplLoginText string 73 61 var tmplLogin = template.Must(template.Must(template.New("login.html").Parse(tmplBaseText)).Parse(tmplLoginText)) 74 62 75 - //go:embed "post.html" 76 - var tmplPostText string 77 - var tmplPost = template.Must(template.Must(template.New("post.html").Parse(tmplBaseText)).Parse(tmplPostText)) 63 + //go:embed "intents.html" 64 + var tmplIntentsText string 65 + var tmplIntents = template.Must(template.Must(template.New("intents.html").Parse(tmplBaseText)).Parse(tmplIntentsText)) 78 66 79 - func (s *Server) Homepage(w http.ResponseWriter, r *http.Request) { 80 - tmplHome.Execute(w, nil) 67 + type WebInfo struct { 68 + DID string 69 + //Handle string 70 + Declaration *Declaration 81 71 } 82 72 83 73 func runServer(cctx *cli.Context) error { ··· 101 91 ) 102 92 } 103 93 104 - // If a client secret key is provided (as a multibase string), turn this in to a confidential client 105 - if cctx.String("client-secret-key") != "" && hostname != "" { 106 - priv, err := crypto.ParsePrivateMultibase(cctx.String("client-secret-key")) 107 - if err != nil { 108 - return err 109 - } 110 - config.AddClientSecret(priv, cctx.String("client-secret-key-id")) 111 - slog.Info("configuring confidential OAuth client") 112 - } 113 - 114 94 oauthClient := oauth.NewClientApp(&config, oauth.NewMemStore()) 115 95 116 96 srv := Server{ ··· 125 105 http.HandleFunc("GET /oauth/login", srv.OAuthLogin) 126 106 http.HandleFunc("POST /oauth/login", srv.OAuthLogin) 127 107 http.HandleFunc("GET /oauth/callback", srv.OAuthCallback) 128 - http.HandleFunc("GET /oauth/refresh", srv.OAuthRefresh) 129 108 http.HandleFunc("GET /oauth/logout", srv.OAuthLogout) 130 - http.HandleFunc("GET /bsky/post", srv.Post) 131 - http.HandleFunc("POST /bsky/post", srv.Post) 109 + http.HandleFunc("GET /intents", srv.UpdateIntents) 110 + http.HandleFunc("POST /intents", srv.UpdateIntents) 132 111 133 112 slog.Info("starting http server", "bind", bind) 134 113 if err := http.ListenAndServe(bind, nil); err != nil { ··· 160 139 161 140 scope := "atproto transition:generic" 162 141 meta := s.OAuth.Config.ClientMetadata(scope) 163 - if s.OAuth.Config.IsConfidential() { 164 - meta.JWKSUri = strPtr(fmt.Sprintf("https://%s/oauth/jwks.json", r.Host)) 165 - } 166 - meta.ClientName = strPtr("indigo atp-oauth-demo") 142 + meta.ClientName = strPtr("AI-PREF / Bluesky Demo App") 167 143 meta.ClientURI = strPtr(fmt.Sprintf("https://%s", r.Host)) 168 144 169 145 // internal consistency check ··· 187 163 http.Error(w, err.Error(), http.StatusInternalServerError) 188 164 return 189 165 } 166 + } 167 + 168 + func (s *Server) Homepage(w http.ResponseWriter, r *http.Request) { 169 + did := s.currentSessionDID(r) 170 + if did != nil { 171 + tmplHome.Execute(w, WebInfo{DID: did.String()}) 172 + return 173 + } 174 + tmplHome.Execute(w, nil) 190 175 } 191 176 192 177 func (s *Server) OAuthLogin(w http.ResponseWriter, r *http.Request) { ··· 237 222 } 238 223 239 224 slog.Info("login successful", "did", sessData.AccountDID.String()) 240 - http.Redirect(w, r, "/bsky/post", http.StatusFound) 241 - } 242 - 243 - func (s *Server) OAuthRefresh(w http.ResponseWriter, r *http.Request) { 244 - ctx := r.Context() 245 - 246 - did := s.currentSessionDID(r) 247 - if did == nil { 248 - // TODO: suppowed to set a WWW header; and could redirect? 249 - http.Error(w, "not authenticated", http.StatusUnauthorized) 250 - return 251 - } 252 - 253 - oauthSess, err := s.OAuth.ResumeSession(ctx, *did) 254 - if err != nil { 255 - http.Error(w, "not authenticated", http.StatusUnauthorized) 256 - return 257 - } 258 - 259 - if err := oauthSess.RefreshTokens(ctx); err != nil { 260 - http.Error(w, err.Error(), http.StatusBadRequest) 261 - return 262 - } 263 - s.OAuth.Store.SaveSession(ctx, *oauthSess.Data) 264 - slog.Info("refreshed tokens") 265 - http.Redirect(w, r, "/", http.StatusFound) 225 + http.Redirect(w, r, "/intents", http.StatusFound) 266 226 } 267 227 268 228 func (s *Server) OAuthLogout(w http.ResponseWriter, r *http.Request) { ··· 280 240 http.Redirect(w, r, "/", http.StatusFound) 281 241 } 282 242 283 - func (s *Server) Post(w http.ResponseWriter, r *http.Request) { 284 - ctx := r.Context() 285 - 286 - slog.Info("in post handler") 243 + func parseTriState(raw string) *bool { 244 + switch raw { 245 + case "allow": 246 + v := true 247 + return &v 248 + case "disallow": 249 + v := false 250 + return &v 251 + default: 252 + return nil 253 + } 254 + } 287 255 288 - if r.Method != "POST" { 289 - tmplPost.Execute(w, nil) 290 - return 291 - } 256 + func (s *Server) UpdateIntents(w http.ResponseWriter, r *http.Request) { 257 + ctx := r.Context() 292 258 293 259 did := s.currentSessionDID(r) 294 260 if did == nil { ··· 303 269 return 304 270 } 305 271 c := oauthSess.APIClient() 272 + info := WebInfo{ 273 + DID: did.String(), 274 + } 275 + 276 + p := map[string]any{ 277 + "repo": did.String(), 278 + "collection": "org.user-intents.demo.declaration", 279 + "rkey": "self", 280 + } 281 + resp := GetDeclarationResp{} 282 + err = c.Get(ctx, "com.atproto.repo.getRecord", p, &resp) 283 + if err != nil { 284 + slog.Info("could not fetch existing declaration", "err", err) 285 + } 286 + 287 + if r.Method != "POST" { 288 + info.Declaration = &resp.Value 289 + tmplIntents.Execute(w, info) 290 + return 291 + } 306 292 307 293 if err := r.ParseForm(); err != nil { 308 294 http.Error(w, fmt.Errorf("parsing form data: %w", err).Error(), http.StatusBadRequest) 309 295 return 310 296 } 311 - text := r.PostFormValue("post_text") 312 297 313 - body := map[string]any{ 314 - "repo": c.AccountDID.String(), 315 - "collection": "app.bsky.feed.post", 316 - "record": map[string]any{ 317 - "$type": "app.bsky.feed.post", 318 - "text": text, 319 - "createdAt": syntax.DatetimeNow(), 298 + // TODO: have this not clobber current timestamps 299 + now := syntax.DatetimeNow().String() 300 + decl := Declaration{ 301 + Type: "org.user-intents.demo.declaration", 302 + UpdatedAt: now, 303 + SyntheticContentGeneration: &DeclarationIntent{ 304 + Value: parseTriState(r.PostFormValue("syntheticContentGeneration")), 305 + UpdatedAt: now, 306 + }, 307 + PublicAccessArchive: &DeclarationIntent{ 308 + Value: parseTriState(r.PostFormValue("publicAccessArchive")), 309 + UpdatedAt: now, 310 + }, 311 + BulkDataset: &DeclarationIntent{ 312 + Value: parseTriState(r.PostFormValue("bulkDataset")), 313 + UpdatedAt: now, 314 + }, 315 + ProtocolBridging: &DeclarationIntent{ 316 + Value: parseTriState(r.PostFormValue("protocolBridging")), 317 + UpdatedAt: now, 320 318 }, 321 319 } 322 320 323 - slog.Info("attempting post...", "text", text) 324 - if err := c.Post(ctx, "com.atproto.repo.createRecord", body, nil); err != nil { 325 - http.Error(w, fmt.Errorf("posting failed: %w", err).Error(), http.StatusBadRequest) 321 + //nope := false 322 + body := PutDeclarationBody{ 323 + Repo: did.String(), 324 + Collection: "org.user-intents.demo.declaration", 325 + Rkey: "self", 326 + // XXX: Validate: &nope, 327 + Record: decl, 328 + } 329 + 330 + slog.Info("updating intents", "did", did, "declaration", decl) 331 + if err := c.Post(ctx, "com.atproto.repo.putRecord", body, nil); err != nil { 332 + slog.Info("failed to update intents record", "err", err) 333 + http.Error(w, fmt.Errorf("update failed: %w", err).Error(), http.StatusBadRequest) 326 334 return 327 335 } 328 336 329 - http.Redirect(w, r, "/bsky/post", http.StatusFound) 337 + http.Redirect(w, r, "/", http.StatusFound) 330 338 }
-6
post.html
··· 1 - {{ define "content" }} 2 - <form method="post"> 3 - <textarea name="post_text" placeholder="What's up?" id="post_text" required></textarea> 4 - <input type="submit" value="Poast!"> 5 - </form> 6 - {{ end }}