diff --git a/base.html b/base.html
index 62231ee..eee2f8d 100644
--- a/base.html
+++ b/base.html
@@ -6,26 +6,27 @@
-
atproto OAuth demo (indigo)
+ ATProto User Intents Demo
- atproto OAuth demo (indigo)
+ ATProto User Intents Demo
{{ if false }}
+
Hello @{{ "handle" }}!
{{ end }}
diff --git a/declaration.go b/declaration.go
new file mode 100644
index 0000000..647a992
--- /dev/null
+++ b/declaration.go
@@ -0,0 +1,30 @@
+package main
+
+type GetDeclarationResp struct {
+ Cid *string `json:"cid,omitempty"`
+ Uri string `json:"uri"`
+ Value Declaration `json:"value"`
+}
+
+type PutDeclarationBody struct {
+ Repo string `json:"repo"`
+ Collection string `json:"collection"`
+ Rkey string `json:"rkey,omitempty"`
+ SwapCommit *string `json:"swapCommit,omitempty"`
+ Validate *bool `json:"validate,omitempty"`
+ Record Declaration `json:"record"`
+}
+
+type DeclarationIntent struct {
+ Value *bool `json:"value,omitempty"`
+ UpdatedAt string `json:"updatedAt"`
+}
+
+type Declaration struct {
+ Type string `json:"$type"`
+ UpdatedAt string `json:"updatedAt,omitempty"`
+ SyntheticContentGeneration *DeclarationIntent `json:"syntheticContentGeneration,omitempty"`
+ PublicAccessArchive *DeclarationIntent `json:"publicAccessArchive,omitempty"`
+ BulkDataset *DeclarationIntent `json:"bulkDataset,omitempty"`
+ ProtocolBridging *DeclarationIntent `json:"protocolBridging,omitempty"`
+}
diff --git a/intents.html b/intents.html
new file mode 100644
index 0000000..833128d
--- /dev/null
+++ b/intents.html
@@ -0,0 +1,34 @@
+{{ define "content" }}
+
+{{ end }}
diff --git a/lexicons/org.user-intents/demo/declaration.json b/lexicons/org.user-intents/demo/declaration.json
index 79cc377..c4333e8 100644
--- a/lexicons/org.user-intents/demo/declaration.json
+++ b/lexicons/org.user-intents/demo/declaration.json
@@ -25,12 +25,12 @@
"ref": "#intent",
"description": "Public access to or replay of account data as part of archiving and preservation efforts"
},
- "syntheticContentGeneration": {
+ "bulkDataset": {
"type": "ref",
"ref": "#intent",
"description": "Inclusion of account data in bulk 'snapshot' datasets which are publicly redistributed, even if only for a fixed time period"
},
- "syntheticContentGeneration": {
+ "protocolBridging": {
"type": "ref",
"ref": "#intent",
"description": "Bridging account data or interactions into distinct social web protocol ecosystems"
diff --git a/main.go b/main.go
index 1689d1d..fb91694 100644
--- a/main.go
+++ b/main.go
@@ -12,7 +12,6 @@ import (
_ "github.com/joho/godotenv/autoload"
"github.com/bluesky-social/indigo/atproto/auth/oauth"
- "github.com/bluesky-social/indigo/atproto/crypto"
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/bluesky-social/indigo/atproto/syntax"
@@ -37,17 +36,6 @@ func main() {
Usage: "public host name for this client (if not localhost dev mode)",
EnvVars: []string{"CLIENT_HOSTNAME"},
},
- &cli.StringFlag{
- Name: "client-secret-key",
- Usage: "confidential client secret key. should be P-256 private key in multibase encoding",
- EnvVars: []string{"CLIENT_SECRET_KEY"},
- },
- &cli.StringFlag{
- Name: "client-secret-key-id",
- Usage: "key id for client-secret-key",
- Value: "primary",
- EnvVars: []string{"CLIENT_SECRET_KEY_ID"},
- },
},
}
h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})
@@ -72,12 +60,14 @@ var tmplHome = template.Must(template.Must(template.New("home.html").Parse(tmplB
var tmplLoginText string
var tmplLogin = template.Must(template.Must(template.New("login.html").Parse(tmplBaseText)).Parse(tmplLoginText))
-//go:embed "post.html"
-var tmplPostText string
-var tmplPost = template.Must(template.Must(template.New("post.html").Parse(tmplBaseText)).Parse(tmplPostText))
+//go:embed "intents.html"
+var tmplIntentsText string
+var tmplIntents = template.Must(template.Must(template.New("intents.html").Parse(tmplBaseText)).Parse(tmplIntentsText))
-func (s *Server) Homepage(w http.ResponseWriter, r *http.Request) {
- tmplHome.Execute(w, nil)
+type WebInfo struct {
+ DID string
+ //Handle string
+ Declaration *Declaration
}
func runServer(cctx *cli.Context) error {
@@ -101,16 +91,6 @@ func runServer(cctx *cli.Context) error {
)
}
- // If a client secret key is provided (as a multibase string), turn this in to a confidential client
- if cctx.String("client-secret-key") != "" && hostname != "" {
- priv, err := crypto.ParsePrivateMultibase(cctx.String("client-secret-key"))
- if err != nil {
- return err
- }
- config.AddClientSecret(priv, cctx.String("client-secret-key-id"))
- slog.Info("configuring confidential OAuth client")
- }
-
oauthClient := oauth.NewClientApp(&config, oauth.NewMemStore())
srv := Server{
@@ -125,10 +105,9 @@ func runServer(cctx *cli.Context) error {
http.HandleFunc("GET /oauth/login", srv.OAuthLogin)
http.HandleFunc("POST /oauth/login", srv.OAuthLogin)
http.HandleFunc("GET /oauth/callback", srv.OAuthCallback)
- http.HandleFunc("GET /oauth/refresh", srv.OAuthRefresh)
http.HandleFunc("GET /oauth/logout", srv.OAuthLogout)
- http.HandleFunc("GET /bsky/post", srv.Post)
- http.HandleFunc("POST /bsky/post", srv.Post)
+ http.HandleFunc("GET /intents", srv.UpdateIntents)
+ http.HandleFunc("POST /intents", srv.UpdateIntents)
slog.Info("starting http server", "bind", bind)
if err := http.ListenAndServe(bind, nil); err != nil {
@@ -160,10 +139,7 @@ func (s *Server) ClientMetadata(w http.ResponseWriter, r *http.Request) {
scope := "atproto transition:generic"
meta := s.OAuth.Config.ClientMetadata(scope)
- if s.OAuth.Config.IsConfidential() {
- meta.JWKSUri = strPtr(fmt.Sprintf("https://%s/oauth/jwks.json", r.Host))
- }
- meta.ClientName = strPtr("indigo atp-oauth-demo")
+ meta.ClientName = strPtr("AI-PREF / Bluesky Demo App")
meta.ClientURI = strPtr(fmt.Sprintf("https://%s", r.Host))
// internal consistency check
@@ -189,6 +165,15 @@ func (s *Server) JWKS(w http.ResponseWriter, r *http.Request) {
}
}
+func (s *Server) Homepage(w http.ResponseWriter, r *http.Request) {
+ did := s.currentSessionDID(r)
+ if did != nil {
+ tmplHome.Execute(w, WebInfo{DID: did.String()})
+ return
+ }
+ tmplHome.Execute(w, nil)
+}
+
func (s *Server) OAuthLogin(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -237,32 +222,7 @@ func (s *Server) OAuthCallback(w http.ResponseWriter, r *http.Request) {
}
slog.Info("login successful", "did", sessData.AccountDID.String())
- http.Redirect(w, r, "/bsky/post", http.StatusFound)
-}
-
-func (s *Server) OAuthRefresh(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
-
- did := s.currentSessionDID(r)
- if did == nil {
- // TODO: suppowed to set a WWW header; and could redirect?
- http.Error(w, "not authenticated", http.StatusUnauthorized)
- return
- }
-
- oauthSess, err := s.OAuth.ResumeSession(ctx, *did)
- if err != nil {
- http.Error(w, "not authenticated", http.StatusUnauthorized)
- return
- }
-
- if err := oauthSess.RefreshTokens(ctx); err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- s.OAuth.Store.SaveSession(ctx, *oauthSess.Data)
- slog.Info("refreshed tokens")
- http.Redirect(w, r, "/", http.StatusFound)
+ http.Redirect(w, r, "/intents", http.StatusFound)
}
func (s *Server) OAuthLogout(w http.ResponseWriter, r *http.Request) {
@@ -280,15 +240,21 @@ func (s *Server) OAuthLogout(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusFound)
}
-func (s *Server) Post(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
-
- slog.Info("in post handler")
-
- if r.Method != "POST" {
- tmplPost.Execute(w, nil)
- return
+func parseTriState(raw string) *bool {
+ switch raw {
+ case "allow":
+ v := true
+ return &v
+ case "disallow":
+ v := false
+ return &v
+ default:
+ return nil
}
+}
+
+func (s *Server) UpdateIntents(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
did := s.currentSessionDID(r)
if did == nil {
@@ -303,28 +269,70 @@ func (s *Server) Post(w http.ResponseWriter, r *http.Request) {
return
}
c := oauthSess.APIClient()
+ info := WebInfo{
+ DID: did.String(),
+ }
+
+ p := map[string]any{
+ "repo": did.String(),
+ "collection": "org.user-intents.demo.declaration",
+ "rkey": "self",
+ }
+ resp := GetDeclarationResp{}
+ err = c.Get(ctx, "com.atproto.repo.getRecord", p, &resp)
+ if err != nil {
+ slog.Info("could not fetch existing declaration", "err", err)
+ }
+
+ if r.Method != "POST" {
+ info.Declaration = &resp.Value
+ tmplIntents.Execute(w, info)
+ return
+ }
if err := r.ParseForm(); err != nil {
http.Error(w, fmt.Errorf("parsing form data: %w", err).Error(), http.StatusBadRequest)
return
}
- text := r.PostFormValue("post_text")
-
- body := map[string]any{
- "repo": c.AccountDID.String(),
- "collection": "app.bsky.feed.post",
- "record": map[string]any{
- "$type": "app.bsky.feed.post",
- "text": text,
- "createdAt": syntax.DatetimeNow(),
+
+ // TODO: have this not clobber current timestamps
+ now := syntax.DatetimeNow().String()
+ decl := Declaration{
+ Type: "org.user-intents.demo.declaration",
+ UpdatedAt: now,
+ SyntheticContentGeneration: &DeclarationIntent{
+ Value: parseTriState(r.PostFormValue("syntheticContentGeneration")),
+ UpdatedAt: now,
+ },
+ PublicAccessArchive: &DeclarationIntent{
+ Value: parseTriState(r.PostFormValue("publicAccessArchive")),
+ UpdatedAt: now,
+ },
+ BulkDataset: &DeclarationIntent{
+ Value: parseTriState(r.PostFormValue("bulkDataset")),
+ UpdatedAt: now,
+ },
+ ProtocolBridging: &DeclarationIntent{
+ Value: parseTriState(r.PostFormValue("protocolBridging")),
+ UpdatedAt: now,
},
}
- slog.Info("attempting post...", "text", text)
- if err := c.Post(ctx, "com.atproto.repo.createRecord", body, nil); err != nil {
- http.Error(w, fmt.Errorf("posting failed: %w", err).Error(), http.StatusBadRequest)
+ //nope := false
+ body := PutDeclarationBody{
+ Repo: did.String(),
+ Collection: "org.user-intents.demo.declaration",
+ Rkey: "self",
+ // XXX: Validate: &nope,
+ Record: decl,
+ }
+
+ slog.Info("updating intents", "did", did, "declaration", decl)
+ if err := c.Post(ctx, "com.atproto.repo.putRecord", body, nil); err != nil {
+ slog.Info("failed to update intents record", "err", err)
+ http.Error(w, fmt.Errorf("update failed: %w", err).Error(), http.StatusBadRequest)
return
}
- http.Redirect(w, r, "/bsky/post", http.StatusFound)
+ http.Redirect(w, r, "/", http.StatusFound)
}
diff --git a/post.html b/post.html
deleted file mode 100644
index 27c15a0..0000000
--- a/post.html
+++ /dev/null
@@ -1,6 +0,0 @@
-{{ define "content" }}
-
-{{ end }}