diff --git a/api/socialscrumble/betacollectionitem.go b/api/socialscrumble/betacollectionitem.go index 8ee69e2..1d8268a 100644 --- a/api/socialscrumble/betacollectionitem.go +++ b/api/socialscrumble/betacollectionitem.go @@ -13,11 +13,11 @@ import ( // Individual item submitted to a scene. type BetaCollectionItem struct { - LexiconTypeID string `json:"$type" cborgen:"$type,const=social.scrumble.beta.collection.item"` - CreatedAt string `json:"createdAt" cborgen:"createdAt"` - Item BetaCollectionItem_Item `json:"item" cborgen:"item"` - Scene string `json:"scene" cborgen:"scene"` - Tags []string `json:"tags,omitempty" cborgen:"tags,omitempty"` + LexiconTypeID string `json:"$type" cborgen:"$type,const=social.scrumble.beta.collection.item"` + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + Item *BetaCollectionItem_Item `json:"item" cborgen:"item"` + Scene string `json:"scene" cborgen:"scene"` + Tags []string `json:"tags,omitempty" cborgen:"tags,omitempty"` } type BetaCollectionItem_Item struct { diff --git a/info.go b/info.go index 8aa3195..3d52087 100644 --- a/info.go +++ b/info.go @@ -1,9 +1,13 @@ package main import ( + "context" "net/http" + "tangled.org/bnewbold.net/scrumble/api/socialscrumble" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/labstack/echo/v4" ) type SessionInfo struct { @@ -12,9 +16,9 @@ type SessionInfo struct { } type SceneInfo struct { -} - -type UserInfo struct { + DID syntax.DID + Handle syntax.Handle + Declaration *socialscrumble.BetaSceneDeclaration } // returns the session for the current request, or nil @@ -38,3 +42,27 @@ func (srv *Server) GetCurrentSession(r *http.Request) *SessionInfo { SessionID: sessionID, } } + +func (srv *Server) GetSceneInfo(ctx context.Context, param string) (*SceneInfo, error) { + + sceneDID, err := syntax.ParseDID(param) + if err != nil { + return nil, echo.NewHTTPError(http.StatusBadRequest, "Broken URL: Invalid Scene Name") + } + + decl, err := srv.store.GetSceneDeclaration(ctx, sceneDID) + if err != nil { + return nil, echo.NewHTTPError(http.StatusNotFound, "Unknown Scene (Declaration)") + } + + ident, err := srv.dir.LookupDID(ctx, sceneDID) + if err != nil { + return nil, echo.NewHTTPError(http.StatusNotFound, "Unknown Scene (Identity)") + } + + return &SceneInfo{ + DID: sceneDID, + Handle: ident.Handle, + Declaration: decl, + }, nil +} diff --git a/main.go b/main.go index 1c1c706..e21fff3 100644 --- a/main.go +++ b/main.go @@ -263,9 +263,11 @@ func runDev(ctx context.Context, cmd *cli.Command) error { } // insert example data - if err := srv.store.InsertExampleData(ctx); err != nil { + mockDir, err := srv.store.InsertExampleData(ctx) + if err != nil { return err } + srv.dir = mockDir srvErr := make(chan error, 1) go func() { diff --git a/server.go b/server.go index 088c282..fca1515 100644 --- a/server.go +++ b/server.go @@ -189,8 +189,7 @@ func (srv *Server) startWithListener(listen net.Listener) error { // "global" views e.GET("/s/:scene", srv.WebSceneView) - //e.GET("/s/:scene/item/:account/:rkey", srv.WebItemView) - //e.GET("/s/:scene/user/:account", srv.WebUserView) + e.GET("/s/:scene/item/:account/:rkey", srv.WebItemView) // In order to support booting on random ports in tests, we need to tell // the Echo instance it's already got a port, and then use its StartServer diff --git a/store/db/query.sql.go b/store/db/query.sql.go index 25c46d5..408624b 100644 --- a/store/db/query.sql.go +++ b/store/db/query.sql.go @@ -101,6 +101,7 @@ func (q *Queries) GetOAuthAuthRequest(ctx context.Context, state string) (OauthA } const getOAuthSession = `-- name: GetOAuthSession :one + SELECT account_did, session_id, data_json, created_at, updated_at FROM oauth_session WHERE account_did = $1 AND session_id = $2 @@ -112,6 +113,7 @@ type GetOAuthSessionParams struct { SessionID string } +// ### OAuth Queries ### func (q *Queries) GetOAuthSession(ctx context.Context, arg GetOAuthSessionParams) (OauthSession, error) { row := q.db.QueryRow(ctx, getOAuthSession, arg.AccountDid, arg.SessionID) var i OauthSession @@ -125,6 +127,33 @@ func (q *Queries) GetOAuthSession(ctx context.Context, arg GetOAuthSessionParams return i, err } +const getRecord = `-- name: GetRecord :one +SELECT collection, account_did, rkey, version, data_json FROM record WHERE + account_did = $1 AND + collection = $2 AND + rkey = $3 +LIMIT 1 +` + +type GetRecordParams struct { + AccountDid string + Collection string + Rkey string +} + +func (q *Queries) GetRecord(ctx context.Context, arg GetRecordParams) (Record, error) { + row := q.db.QueryRow(ctx, getRecord, arg.AccountDid, arg.Collection, arg.Rkey) + var i Record + err := row.Scan( + &i.Collection, + &i.AccountDid, + &i.Rkey, + &i.Version, + &i.DataJson, + ) + return i, err +} + const insertOAuthAuthRequest = `-- name: InsertOAuthAuthRequest :exec INSERT INTO oauth_auth_request (state, data_json) VALUES ($1, $2) diff --git a/store/example_data.go b/store/example_data.go index 23b6648..d496da4 100644 --- a/store/example_data.go +++ b/store/example_data.go @@ -2,28 +2,151 @@ package store import ( "context" + "encoding/json" + "tangled.org/bnewbold.net/scrumble/api/socialscrumble" + + "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" ) -/* +type exampleAccount struct { + DID syntax.DID + Handle syntax.Handle + Status string +} + +type exampleScene struct { + DID syntax.DID + Handle syntax.Handle + DisplayName string + Description string + Members []syntax.DID +} + +type exampleItem struct { + SceneDID syntax.DID + AccountDID syntax.DID + RKey syntax.RecordKey + Title string + Text string +} -accounts: -- voltaire.admin.example / did:example:voltaire -- micromegas.user.example / did:example:micromegas -- saturnian.user.example / did:example:micromegas +func (st *Store) InsertExampleData(ctx context.Context) (*identity.MockDirectory, error) { -scenes: -- jupiter.solar.example -- earth.solar.example + dir := identity.NewMockDirectory() -*/ + voltaire := exampleAccount{ + DID: syntax.DID("did:example:voltaire"), + Handle: syntax.Handle("voltaire.cosmos.social"), + Status: "active", + } + micromegas := exampleAccount{ + DID: syntax.DID("did:example:micromegas"), + Handle: syntax.Handle("micromegas.cosmo.social"), + Status: "active", + } + saturnian := exampleAccount{ + DID: syntax.DID("did:example:saturnian"), + Handle: syntax.Handle("saturnian.cosmos.social"), + Status: "active", + } + accounts := []exampleAccount{ + voltaire, + micromegas, + saturnian, + } -func (st *Store) InsertExampleData(ctx context.Context) error { + jupiter := exampleScene{ + DID: syntax.DID("did:example:jupiter"), + Handle: syntax.Handle("jupiter.solar.town"), + DisplayName: "Jupiter", + Description: "gas giant with too many moons", + Members: []syntax.DID{micromegas.DID, saturnian.DID}, + } + earth := exampleScene{ + DID: syntax.DID("did:example:earth"), + Handle: syntax.Handle("earth.solar.town"), + DisplayName: "Earth", + Description: "third rock from the sun", + Members: []syntax.DID{micromegas.DID, saturnian.DID}, + } + scenes := []exampleScene{ + jupiter, + earth, + } + + items := []exampleItem{ + exampleItem{ + SceneDID: earth.DID, + AccountDID: micromegas.DID, + RKey: "3mlpgpstll522", + Title: "First Post!", + Text: "Hello, Earth!", + }, + exampleItem{ + SceneDID: earth.DID, + AccountDID: saturnian.DID, + RKey: "3mlpgrt2pmv22", + Title: "What Now?", + Text: "blah", + }, + } + + for _, acc := range accounts { + if err := st.UpsertIdentity(ctx, acc.DID, acc.Handle, acc.Status); err != nil { + return nil, err + } + dir.Insert(identity.Identity{ + DID: acc.DID, + Handle: acc.Handle, + }) + } + + for _, scene := range scenes { + decl := socialscrumble.BetaSceneDeclaration{ + LexiconTypeID: "social.scrumble.beta.scene.declaration", + DisplayName: new(scene.DisplayName), + Description: new(scene.Description), + } + data, err := json.Marshal(decl) + if err != nil { + return nil, err + } + version := "XXX" + if err := st.UpsertRecord(ctx, scene.DID, syntax.NSID("social.scrumble.beta.scene.declaration"), syntax.RecordKey("self"), version, data); err != nil { + return nil, err + } + dir.Insert(identity.Identity{ + DID: scene.DID, + Handle: scene.Handle, + }) + } - if err := st.UpsertIdentity(ctx, syntax.DID("did:example:voltaire"), syntax.Handle("voltaire.admin.com"), "active"); err != nil { - return err + for _, item := range items { + // XXX: item record + rec := socialscrumble.BetaCollectionItem{ + LexiconTypeID: "social.scrumble.beta.collection.item", + Scene: item.SceneDID.String(), + Item: &socialscrumble.BetaCollectionItem_Item{ + BetaCollectionItem_TextItem: &socialscrumble.BetaCollectionItem_TextItem{ + Title: new(item.Title), + Text: item.Text, + }, + }, + } + data, err := json.Marshal(rec) + if err != nil { + return nil, err + } + version := "XXX" + if err := st.UpsertRecord(ctx, item.AccountDID, syntax.NSID("social.scrumble.beta.collection.item"), item.RKey, version, data); err != nil { + return nil, err + } + if err := st.UpsertItem(ctx, item.SceneDID, item.AccountDID, item.RKey, version); err != nil { + return nil, err + } } - return nil + return dir, nil } diff --git a/store/schemas/000001_init.up.sql b/store/schemas/000001_init.up.sql index 37be5c7..b8c3eda 100644 --- a/store/schemas/000001_init.up.sql +++ b/store/schemas/000001_init.up.sql @@ -28,6 +28,7 @@ CREATE TABLE membership ( scene_did TEXT NOT NULL, account_did TEXT NOT NULL, + -- TODO: should these be NOT NULL? member_rkey TEXT, join_rkey TEXT, role TEXT, @@ -41,7 +42,7 @@ CREATE TABLE item ( rkey TEXT NOT NULL, version TEXT NOT NULL, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(), PRIMARY KEY (scene_did, account_did, rkey) ); diff --git a/store/schemas/query.sql b/store/schemas/query.sql index 71ae0f1..2963025 100644 --- a/store/schemas/query.sql +++ b/store/schemas/query.sql @@ -32,6 +32,15 @@ ON CONFLICT (scene_did, account_did, rkey) DO UPDATE SET -- name: DeleteItem :exec DELETE FROM item WHERE scene_did = $1 AND account_did = $2 AND rkey = $3; +-- name: GetRecord :one +SELECT * FROM record WHERE + account_did = $1 AND + collection = $2 AND + rkey = $3 +LIMIT 1; + +-- ### OAuth Queries ### + -- name: GetOAuthSession :one SELECT * FROM oauth_session WHERE account_did = $1 AND diff --git a/store/store.go b/store/store.go index 01cf398..657b1aa 100644 --- a/store/store.go +++ b/store/store.go @@ -11,6 +11,7 @@ import ( _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" "github.com/jackc/pgx/v5" + "tangled.org/bnewbold.net/scrumble/api/socialscrumble" ) type Store struct { @@ -94,3 +95,37 @@ func (st *Store) DeleteItem(ctx context.Context, accountDID syntax.DID, rkey syn Rkey: rkey.String(), }) } + +func (st *Store) GetSceneDeclaration(ctx context.Context, did syntax.DID) (*socialscrumble.BetaSceneDeclaration, error) { + q := db.New(st.conn) + row, err := q.GetRecord(ctx, db.GetRecordParams{ + AccountDid: did.String(), + Collection: "social.scrumble.beta.scene.declaration", + Rkey: "self", + }) + if err != nil { + return nil, err + } + var decl socialscrumble.BetaSceneDeclaration + if err := json.Unmarshal(row.DataJson, &decl); err != nil { + return nil, err + } + return &decl, nil +} + +func (st *Store) GetItem(ctx context.Context, did syntax.DID, rkey syntax.RecordKey) (*socialscrumble.BetaCollectionItem, error) { + q := db.New(st.conn) + row, err := q.GetRecord(ctx, db.GetRecordParams{ + AccountDid: did.String(), + Collection: "social.scrumble.beta.collection.item", + Rkey: rkey.String(), + }) + if err != nil { + return nil, err + } + var record socialscrumble.BetaCollectionItem + if err := json.Unmarshal(row.DataJson, &record); err != nil { + return nil, err + } + return &record, nil +} diff --git a/templates/item.html b/templates/item.html new file mode 100644 index 0000000..4931c2c --- /dev/null +++ b/templates/item.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block main_content %} +

item

+ + + +

scene

+ + + +{% endblock %} diff --git a/templates/scene.html b/templates/scene.html new file mode 100644 index 0000000..76f964f --- /dev/null +++ b/templates/scene.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block main_content %} +

view scene

+ + + +{% endblock %} diff --git a/web.go b/web.go index 352963a..fbc1d36 100644 --- a/web.go +++ b/web.go @@ -4,6 +4,7 @@ import ( "net/http" "strings" + "github.com/bluesky-social/indigo/atproto/syntax" "github.com/flosch/pongo2/v7" "github.com/labstack/echo/v4" ) @@ -32,23 +33,59 @@ func (srv *Server) WebAbout(c echo.Context) error { } func (srv *Server) WebSceneView(c echo.Context) error { + ctx := c.Request().Context() info := pongo2.Context{} sess := srv.GetCurrentSession(c.Request()) if sess != nil { info["sess"] = sess } - return c.Render(http.StatusOK, "about.html", info) + sceneInfo, err := srv.GetSceneInfo(ctx, c.Param("scene")) + if err != nil { + return err + } + info["scene"] = sceneInfo + srv.logger.Info("viewing scene", "scene", sceneInfo) + + // XXX: top items + + return c.Render(http.StatusOK, "scene.html", info) } func (srv *Server) WebItemView(c echo.Context) error { + ctx := c.Request().Context() info := pongo2.Context{} sess := srv.GetCurrentSession(c.Request()) if sess != nil { info["sess"] = sess } - //info["scene"] := - //info["item"] := - return c.Render(http.StatusOK, "about.html", info) + sceneInfo, err := srv.GetSceneInfo(ctx, c.Param("scene")) + if err != nil { + return err + } + info["scene"] = sceneInfo + + accountDID, err := syntax.ParseDID(c.Param("account")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Broken URL: Invalid Account Name") + } + tid, err := syntax.ParseTID(c.Param("rkey")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Broken URL: Invalid Item ID") + } + rkey := syntax.RecordKey(tid) + + info["account_did"] = accountDID + info["rkey"] = rkey + + record, err := srv.store.GetItem(ctx, accountDID, rkey) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound, "Item Not Found") + } + info["record"] = record + + srv.logger.Info("viewing item", "scene", sceneInfo, "record", record) + + return c.Render(http.StatusOK, "item.html", info) } func (srv *Server) WebLogin(c echo.Context) error {