From dfb852f62cf48351b67b93f8c124992cfde3ba4b Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Fri, 10 Apr 2026 09:36:21 +0100 Subject: [PATCH] hacking on UI Signed-off-by: oppiliappan --- api/tangled/cbor_gen.go | 769 +++++++++--------- api/tangled/graphvouch.go | 2 +- appview/db/db.go | 1 + appview/db/vouch.go | 144 +--- appview/ingester.go | 16 +- appview/models/vouch.go | 104 ++- appview/oauth/scopes.go | 37 +- appview/pages/pages.go | 15 +- .../templates/user/fragments/profileCard.html | 28 +- .../pages/templates/user/fragments/vouch.html | 197 +++++ appview/state/profile.go | 17 +- appview/state/router.go | 1 - appview/state/state.go | 19 +- appview/state/vouch.go | 157 ++-- input.css | 9 + lexicons/graph/vouch.json | 5 +- 16 files changed, 890 insertions(+), 631 deletions(-) create mode 100644 appview/pages/templates/user/fragments/vouch.html diff --git a/api/tangled/cbor_gen.go b/api/tangled/cbor_gen.go index 23837d71..3e565f70 100644 --- a/api/tangled/cbor_gen.go +++ b/api/tangled/cbor_gen.go @@ -2460,6 +2460,228 @@ func (t *GraphFollow) UnmarshalCBOR(r io.Reader) (err error) { return nil } +func (t *GraphVouch) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 4 + + if t.Reason == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Kind (string) (string) + if len("kind") > 1000000 { + return xerrors.Errorf("Value in field \"kind\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("kind"))); err != nil { + return err + } + if _, err := cw.WriteString(string("kind")); err != nil { + return err + } + + if len(t.Kind) > 1000000 { + return xerrors.Errorf("Value in field t.Kind was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Kind))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Kind)); err != nil { + return err + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.graph.vouch"))); err != nil { + return err + } + if _, err := cw.WriteString(string("sh.tangled.graph.vouch")); err != nil { + return err + } + + // t.Reason (string) (string) + if t.Reason != nil { + + if len("reason") > 1000000 { + return xerrors.Errorf("Value in field \"reason\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("reason"))); err != nil { + return err + } + if _, err := cw.WriteString(string("reason")); err != nil { + return err + } + + if t.Reason == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Reason) > 1000000 { + return xerrors.Errorf("Value in field t.Reason was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Reason))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Reason)); err != nil { + return err + } + } + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + return nil +} + +func (t *GraphVouch) UnmarshalCBOR(r io.Reader) (err error) { + *t = GraphVouch{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("GraphVouch: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 9) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Kind (string) (string) + case "kind": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Kind = string(sval) + } + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Reason (string) (string) + case "reason": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Reason = (*string)(&sval) + } + } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} func (t *Knot) MarshalCBOR(w io.Writer) error { if t == nil { _, err := w.Write(cbg.CborNull) @@ -9520,37 +9742,173 @@ func (t *RepoPullComment) UnmarshalCBOR(r io.Reader) (err error) { return nil } -func (t *RepoPull_Source) MarshalCBOR(w io.Writer) error { +func (t *RepoPull_Round) MarshalCBOR(w io.Writer) error { if t == nil { _, err := w.Write(cbg.CborNull) return err } cw := cbg.NewCborWriter(w) - fieldCount := 3 - if t.Repo == nil { - fieldCount-- + if _, err := cw.Write([]byte{162}); err != nil { + return err } - if t.RepoDid == nil { - fieldCount-- + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") } - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { return err } - // t.Repo (string) (string) - if t.Repo != nil { + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } - if len("repo") > 1000000 { - return xerrors.Errorf("Value in field \"repo\" was too long") - } + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { - return err - } + // t.PatchBlob (util.LexBlob) (struct) + if len("patchBlob") > 1000000 { + return xerrors.Errorf("Value in field \"patchBlob\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patchBlob"))); err != nil { + return err + } + if _, err := cw.WriteString(string("patchBlob")); err != nil { + return err + } + + if err := t.PatchBlob.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *RepoPull_Round) UnmarshalCBOR(r io.Reader) (err error) { + *t = RepoPull_Round{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RepoPull_Round: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 9) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + // t.PatchBlob (util.LexBlob) (struct) + case "patchBlob": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.PatchBlob = new(util.LexBlob) + if err := t.PatchBlob.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.PatchBlob pointer: %w", err) + } + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RepoPull_Source) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 3 + + if t.Repo == nil { + fieldCount-- + } + + if t.RepoDid == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Repo (string) (string) + if t.Repo != nil { + + if len("repo") > 1000000 { + return xerrors.Errorf("Value in field \"repo\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { + return err + } if _, err := cw.WriteString(string("repo")); err != nil { return err } @@ -9735,142 +10093,6 @@ func (t *RepoPull_Source) UnmarshalCBOR(r io.Reader) (err error) { return nil } -func (t *RepoPull_Round) MarshalCBOR(w io.Writer) error { - if t == nil { - _, err := w.Write(cbg.CborNull) - return err - } - - cw := cbg.NewCborWriter(w) - - if _, err := cw.Write([]byte{162}); err != nil { - return err - } - - // t.CreatedAt (string) (string) - if len("createdAt") > 1000000 { - return xerrors.Errorf("Value in field \"createdAt\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { - return err - } - if _, err := cw.WriteString(string("createdAt")); err != nil { - return err - } - - if len(t.CreatedAt) > 1000000 { - return xerrors.Errorf("Value in field t.CreatedAt was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { - return err - } - if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { - return err - } - - // t.PatchBlob (util.LexBlob) (struct) - if len("patchBlob") > 1000000 { - return xerrors.Errorf("Value in field \"patchBlob\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patchBlob"))); err != nil { - return err - } - if _, err := cw.WriteString(string("patchBlob")); err != nil { - return err - } - - if err := t.PatchBlob.MarshalCBOR(cw); err != nil { - return err - } - return nil -} - -func (t *RepoPull_Round) UnmarshalCBOR(r io.Reader) (err error) { - *t = RepoPull_Round{} - - cr := cbg.NewCborReader(r) - - maj, extra, err := cr.ReadHeader() - if err != nil { - return err - } - defer func() { - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - }() - - if maj != cbg.MajMap { - return fmt.Errorf("cbor input should be of type map") - } - - if extra > cbg.MaxLength { - return fmt.Errorf("RepoPull_Round: map struct too large (%d)", extra) - } - - n := extra - - nameBuf := make([]byte, 9) - for i := uint64(0); i < n; i++ { - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) - if err != nil { - return err - } - - if !ok { - // Field doesn't exist on this type, so ignore it - if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { - return err - } - continue - } - - switch string(nameBuf[:nameLen]) { - // t.CreatedAt (string) (string) - case "createdAt": - - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { - return err - } - - t.CreatedAt = string(sval) - } - // t.PatchBlob (util.LexBlob) (struct) - case "patchBlob": - - { - - b, err := cr.ReadByte() - if err != nil { - return err - } - if b != cbg.CborNull[0] { - if err := cr.UnreadByte(); err != nil { - return err - } - t.PatchBlob = new(util.LexBlob) - if err := t.PatchBlob.UnmarshalCBOR(cr); err != nil { - return xerrors.Errorf("unmarshaling t.PatchBlob pointer: %w", err) - } - } - - } - - default: - // Field doesn't exist on this type, so ignore it - if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { - return err - } - } - } - - return nil -} func (t *RepoPullStatus) MarshalCBOR(w io.Writer) error { if t == nil { _, err := w.Write(cbg.CborNull) @@ -10810,248 +11032,3 @@ func (t *String) UnmarshalCBOR(r io.Reader) (err error) { return nil } -func (t *GraphVouch) MarshalCBOR(w io.Writer) error { - if t == nil { - _, err := w.Write(cbg.CborNull) - return err - } - - cw := cbg.NewCborWriter(w) - fieldCount := 4 - - if t.Kind == nil { - fieldCount-- - } - - if t.Reason == nil { - fieldCount-- - } - - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { - return err - } - - // t.Kind (string) (string) - if t.Kind != nil { - - if len("kind") > 1000000 { - return xerrors.Errorf("Value in field \"kind\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("kind"))); err != nil { - return err - } - if _, err := cw.WriteString(string("kind")); err != nil { - return err - } - - if t.Kind == nil { - if _, err := cw.Write(cbg.CborNull); err != nil { - return err - } - } else { - if len(*t.Kind) > 1000000 { - return xerrors.Errorf("Value in field t.Kind was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Kind))); err != nil { - return err - } - if _, err := cw.WriteString(string(*t.Kind)); err != nil { - return err - } - } - } - - // t.LexiconTypeID (string) (string) - if len("$type") > 1000000 { - return xerrors.Errorf("Value in field \"$type\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { - return err - } - if _, err := cw.WriteString(string("$type")); err != nil { - return err - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.graph.vouch"))); err != nil { - return err - } - if _, err := cw.WriteString(string("sh.tangled.graph.vouch")); err != nil { - return err - } - - // t.Reason (string) (string) - if t.Reason != nil { - - if len("reason") > 1000000 { - return xerrors.Errorf("Value in field \"reason\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("reason"))); err != nil { - return err - } - if _, err := cw.WriteString(string("reason")); err != nil { - return err - } - - if t.Reason == nil { - if _, err := cw.Write(cbg.CborNull); err != nil { - return err - } - } else { - if len(*t.Reason) > 1000000 { - return xerrors.Errorf("Value in field t.Reason was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Reason))); err != nil { - return err - } - if _, err := cw.WriteString(string(*t.Reason)); err != nil { - return err - } - } - } - - // t.CreatedAt (string) (string) - if len("createdAt") > 1000000 { - return xerrors.Errorf("Value in field \"createdAt\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { - return err - } - if _, err := cw.WriteString(string("createdAt")); err != nil { - return err - } - - if len(t.CreatedAt) > 1000000 { - return xerrors.Errorf("Value in field t.CreatedAt was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { - return err - } - if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { - return err - } - return nil -} - -func (t *GraphVouch) UnmarshalCBOR(r io.Reader) (err error) { - *t = GraphVouch{} - - cr := cbg.NewCborReader(r) - - maj, extra, err := cr.ReadHeader() - if err != nil { - return err - } - defer func() { - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - }() - - if maj != cbg.MajMap { - return fmt.Errorf("cbor input should be of type map") - } - - if extra > cbg.MaxLength { - return fmt.Errorf("GraphVouch: map struct too large (%d)", extra) - } - - n := extra - - nameBuf := make([]byte, 9) - for i := uint64(0); i < n; i++ { - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) - if err != nil { - return err - } - - if !ok { - // Field doesn't exist on this type, so ignore it - if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { - return err - } - continue - } - - switch string(nameBuf[:nameLen]) { - // t.Kind (string) (string) - case "kind": - - { - b, err := cr.ReadByte() - if err != nil { - return err - } - if b != cbg.CborNull[0] { - if err := cr.UnreadByte(); err != nil { - return err - } - - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { - return err - } - - t.Kind = (*string)(&sval) - } - } - // t.LexiconTypeID (string) (string) - case "$type": - - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { - return err - } - - t.LexiconTypeID = string(sval) - } - // t.Reason (string) (string) - case "reason": - - { - b, err := cr.ReadByte() - if err != nil { - return err - } - if b != cbg.CborNull[0] { - if err := cr.UnreadByte(); err != nil { - return err - } - - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { - return err - } - - t.Reason = (*string)(&sval) - } - } - // t.CreatedAt (string) (string) - case "createdAt": - - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { - return err - } - - t.CreatedAt = string(sval) - } - - default: - // Field doesn't exist on this type, so ignore it - if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { - return err - } - } - } - - return nil -} diff --git a/api/tangled/graphvouch.go b/api/tangled/graphvouch.go index 0569e32e..22d1d4c0 100644 --- a/api/tangled/graphvouch.go +++ b/api/tangled/graphvouch.go @@ -20,7 +20,7 @@ type GraphVouch struct { LexiconTypeID string `json:"$type,const=sh.tangled.graph.vouch" cborgen:"$type,const=sh.tangled.graph.vouch"` CreatedAt string `json:"createdAt" cborgen:"createdAt"` // kind: Whether this user is being vouched for or denounced - Kind *string `json:"kind,omitempty" cborgen:"kind,omitempty"` + Kind string `json:"kind" cborgen:"kind"` // reason: The reason for this vouch/denouncement Reason *string `json:"reason,omitempty" cborgen:"reason,omitempty"` } diff --git a/appview/db/db.go b/appview/db/db.go index 92fbfb69..82ba49cb 100644 --- a/appview/db/db.go +++ b/appview/db/db.go @@ -95,6 +95,7 @@ func Make(ctx context.Context, dbPath string) (*DB, error) { create table if not exists vouches ( did text not null, subject_did text not null, + cid text not null, kind text not null default 'vouch', reason text, created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), diff --git a/appview/db/vouch.go b/appview/db/vouch.go index 25d22af1..37ede8f2 100644 --- a/appview/db/vouch.go +++ b/appview/db/vouch.go @@ -7,13 +7,14 @@ import ( "strings" "time" + "github.com/ipfs/go-cid" "tangled.org/core/appview/models" "tangled.org/core/orm" ) func AddVouch(e Execer, vouch *models.Vouch) error { - query := `insert or replace into vouches (did, subject_did, kind, reason) values (?, ?, ?, ?)` - _, err := e.Exec(query, vouch.Did, vouch.SubjectDid, vouch.Kind, vouch.Reason) + query := `insert or replace into vouches (did, subject_did, cid, kind, reason) values (?, ?, ?, ?, ?)` + _, err := e.Exec(query, vouch.Did, vouch.SubjectDid, vouch.Cid.String(), vouch.Kind, vouch.Reason) return err } @@ -49,7 +50,7 @@ func GetVouches(e Execer, limit int, filters ...orm.Filter) ([]models.Vouch, err } query := fmt.Sprintf( - `select did, subject_did, kind, reason, created_at + `select did, subject_did, cid, kind, reason, created_at from vouches %s order by created_at desc @@ -65,10 +66,12 @@ func GetVouches(e Execer, limit int, filters ...orm.Filter) ([]models.Vouch, err for rows.Next() { var vouch models.Vouch var createdAt string + var cidStr string var reason sql.NullString err := rows.Scan( &vouch.Did, &vouch.SubjectDid, + &cidStr, &vouch.Kind, &reason, &createdAt, @@ -76,6 +79,13 @@ func GetVouches(e Execer, limit int, filters ...orm.Filter) ([]models.Vouch, err if err != nil { return nil, err } + + vouch.Cid, err = cid.Parse(cidStr) + if err != nil { + log.Println("unable to parse CID:", err) + continue + } + createdAtTime, err := time.Parse(time.RFC3339, createdAt) if err != nil { log.Println("unable to determine created at time") @@ -101,23 +111,6 @@ func DeleteVouchByRkey(e Execer, did, rkey string) error { return err } -func GetVouchStats(e Execer, did string) (models.VouchStats, error) { - var vouches, denounces int64 - err := e.QueryRow( - `SELECT - COUNT(CASE WHEN kind = 'vouch' THEN 1 END) AS vouches, - COUNT(CASE WHEN kind = 'denounce' THEN 1 END) AS denounces - FROM vouches - WHERE subject_did = ?`, did).Scan(&vouches, &denounces) - if err != nil { - return models.VouchStats{}, err - } - return models.VouchStats{ - Vouches: vouches, - Denounces: denounces, - }, nil -} - func GetVouchStatsBatch(e Execer, dids []string) (map[string]models.VouchStats, error) { if len(dids) == 0 { return nil, nil @@ -181,16 +174,12 @@ func GetVouchesReceived(e Execer, did string) ([]models.Vouch, error) { return GetVouches(e, 0, orm.FilterEq("subject_did", did)) } -// GetNetworkVouchesForSubject returns vouches for subjectDid from people that viewerDid follows or vouches for func GetNetworkVouchesForSubject(e Execer, viewerDid, subjectDid string, limit int) ([]models.Vouch, error) { query := ` - select distinct v.did, v.subject_did, v.kind, v.reason, v.created_at + select distinct v.did, v.subject_did, v.cid, v.kind, v.reason, v.created_at from vouches v where v.subject_did = ? - and v.kind = 'vouch' and v.did in ( - select subject_did from follows where user_did = ? - union select subject_did from vouches where did = ? and kind = 'vouch' ) order by v.created_at desc @@ -212,10 +201,12 @@ func GetNetworkVouchesForSubject(e Execer, viewerDid, subjectDid string, limit i for rows.Next() { var vouch models.Vouch var createdAt string + var cidStr string var reason sql.NullString err := rows.Scan( &vouch.Did, &vouch.SubjectDid, + &cidStr, &vouch.Kind, &reason, &createdAt, @@ -223,6 +214,13 @@ func GetNetworkVouchesForSubject(e Execer, viewerDid, subjectDid string, limit i if err != nil { return nil, err } + + vouch.Cid, err = cid.Parse(cidStr) + if err != nil { + log.Println("unable to parse CID:", err) + continue + } + createdAtTime, err := time.Parse(time.RFC3339, createdAt) if err != nil { log.Println("unable to determine created at time") @@ -238,93 +236,27 @@ func GetNetworkVouchesForSubject(e Execer, viewerDid, subjectDid string, limit i return vouches, nil } -// GetNetworkDenouncesForSubject returns denounces for subjectDid from people that viewerDid follows or vouches for -func GetNetworkDenouncesForSubject(e Execer, viewerDid, subjectDid string, limit int) ([]models.Vouch, error) { - query := ` - select distinct v.did, v.subject_did, v.kind, v.reason, v.created_at - from vouches v - where v.subject_did = ? - and v.kind = 'denounce' - and v.did in ( - select subject_did from follows where user_did = ? - union - select subject_did from vouches where did = ? and kind = 'vouch' - ) - order by v.created_at desc - ` - args := []any{subjectDid, viewerDid, viewerDid} - - if limit > 0 { - query += " limit ?" - args = append(args, limit) +func GetVouchRelationship(e Execer, viewerDid, subjectDid string) (*models.VouchRelationship, error) { + relationship := &models.VouchRelationship{ + ViewerDid: viewerDid, + SubjectDid: subjectDid, + NetworkVouches: []models.Vouch{}, } - rows, err := e.Query(query, args...) - if err != nil { + // Get direct vouch from viewer to subject (if exists) + directVouch, err := GetVouch(e, viewerDid, subjectDid) + if err == nil { + relationship.NetworkVouches = append(relationship.NetworkVouches, *directVouch) + } else if err != sql.ErrNoRows { return nil, err } - defer rows.Close() - var vouches []models.Vouch - for rows.Next() { - var vouch models.Vouch - var createdAt string - var reason sql.NullString - err := rows.Scan( - &vouch.Did, - &vouch.SubjectDid, - &vouch.Kind, - &reason, - &createdAt, - ) - if err != nil { - return nil, err - } - createdAtTime, err := time.Parse(time.RFC3339, createdAt) - if err != nil { - log.Println("unable to determine created at time") - vouch.CreatedAt = time.Now() - } else { - vouch.CreatedAt = createdAtTime - } - if reason.Valid { - vouch.Reason = &reason.String - } - vouches = append(vouches, vouch) + networkVouches, err := GetNetworkVouchesForSubject(e, viewerDid, subjectDid, 0) + if err != nil { + return nil, err } - return vouches, nil -} -// CountNetworkVouchesForSubject returns count of vouches for subjectDid from viewerDid's network -func CountNetworkVouchesForSubject(e Execer, viewerDid, subjectDid string) (int64, error) { - var count int64 - err := e.QueryRow(` - select count(distinct v.did) - from vouches v - where v.subject_did = ? - and v.kind = 'vouch' - and v.did in ( - select subject_did from follows where user_did = ? - union - select subject_did from vouches where did = ? and kind = 'vouch' - ) - `, subjectDid, viewerDid, viewerDid).Scan(&count) - return count, err -} + relationship.NetworkVouches = append(relationship.NetworkVouches, networkVouches...) -// CountNetworkDenouncesForSubject returns count of denounces for subjectDid from viewerDid's network -func CountNetworkDenouncesForSubject(e Execer, viewerDid, subjectDid string) (int64, error) { - var count int64 - err := e.QueryRow(` - select count(distinct v.did) - from vouches v - where v.subject_did = ? - and v.kind = 'denounce' - and v.did in ( - select subject_did from follows where user_did = ? - union - select subject_did from vouches where did = ? and kind = 'vouch' - ) - `, subjectDid, viewerDid, viewerDid).Scan(&count) - return count, err + return relationship, nil } diff --git a/appview/ingester.go b/appview/ingester.go index 8a2c96e9..b84ea176 100644 --- a/appview/ingester.go +++ b/appview/ingester.go @@ -211,6 +211,7 @@ func (i *Ingester) ingestVouch(ctx context.Context, e *jmodels.Event) error { l := i.Logger.With("handler", "ingestVouch") l = l.With("nsid", e.Commit.Collection) + l.Info("ingesting vouch") switch e.Commit.Operation { case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: @@ -245,19 +246,22 @@ func (i *Ingester) ingestVouch(ctx context.Context, e *jmodels.Event) error { return err } - kind := "vouch" - if record.Kind != nil { - kind = *record.Kind - } - - if kind != "vouch" && kind != "denounce" { + kind, err := models.ParseVouchKind(record.Kind) + if err != nil { l.Error("invalid kind", "kind", kind) return fmt.Errorf("invalid kind: %s", kind) } + recordCid, err := cid.Parse(e.Commit.CID) + if err != nil { + l.Error("invalid cid", "err", err, "cid", e.Commit.CID) + return fmt.Errorf("invalid cid: %w", err) + } + err = db.AddVouch(i.Db, &models.Vouch{ Did: did, SubjectDid: subjectDID, + Cid: recordCid, Kind: kind, Reason: record.Reason, }) diff --git a/appview/models/vouch.go b/appview/models/vouch.go index 1549217a..2d30aed7 100644 --- a/appview/models/vouch.go +++ b/appview/models/vouch.go @@ -1,16 +1,116 @@ package models -import "time" +import ( + "fmt" + "time" + + "github.com/ipfs/go-cid" +) + +type VouchKind string + +const ( + VouchKindVouch VouchKind = "vouch" + VouchKindDenounce VouchKind = "denounce" +) + +func ParseVouchKind(v string) (VouchKind, error) { + switch v { + case "vouch": + return VouchKindVouch, nil + case "denounce": + return VouchKindVouch, nil + default: + return VouchKindVouch, fmt.Errorf("invalid vouch kind: %s", v) + } +} type Vouch struct { Did string SubjectDid string - Kind string + Cid cid.Cid + Kind VouchKind Reason *string CreatedAt time.Time } +func (v Vouch) IsVouch() bool { + return v.Kind == VouchKindVouch +} + +func (v Vouch) IsDenounce() bool { + return v.Kind == VouchKindDenounce +} + type VouchStats struct { Vouches int64 Denounces int64 } + +type VouchRelationship struct { + ViewerDid string + SubjectDid string + + NetworkVouches []Vouch +} + +func (vr *VouchRelationship) IsDirectVouch() bool { + for _, v := range vr.NetworkVouches { + if v.Did == vr.ViewerDid && v.SubjectDid == vr.SubjectDid && v.Kind == VouchKindVouch { + return true + } + } + return false +} + +func (vr *VouchRelationship) IsDirectDenounce() bool { + for _, v := range vr.NetworkVouches { + if v.Did == vr.ViewerDid && v.SubjectDid == vr.SubjectDid && v.Kind == VouchKindDenounce { + return true + } + } + return false +} + +func (vr *VouchRelationship) IndirectVouches() []Vouch { + var indirectVouches []Vouch + for _, v := range vr.NetworkVouches { + if v.Did != vr.ViewerDid { + indirectVouches = append(indirectVouches, v) + } + } + return indirectVouches +} + +func (vr *VouchRelationship) IsEmpty() bool { + return len(vr.NetworkVouches) == 0 +} + +func (vr *VouchRelationship) GetDirectVouch() *Vouch { + for _, v := range vr.NetworkVouches { + if v.Did == vr.ViewerDid && v.SubjectDid == vr.SubjectDid { + return &v + } + } + return nil +} + +func (vr *VouchRelationship) VouchStrength() int { + count := 0 + for _, v := range vr.NetworkVouches { + if v.Did != vr.ViewerDid && v.Kind == VouchKindVouch { + count++ + } + } + return count +} + +func (vr *VouchRelationship) DenounceStrength() int { + count := 0 + for _, v := range vr.NetworkVouches { + if v.Did != vr.ViewerDid && v.Kind == VouchKindDenounce { + count++ + } + } + return count +} diff --git a/appview/oauth/scopes.go b/appview/oauth/scopes.go index 351e05d9..83397f86 100644 --- a/appview/oauth/scopes.go +++ b/appview/oauth/scopes.go @@ -3,39 +3,40 @@ package oauth var TangledScopes = []string{ "atproto", + "repo:sh.tangled.actor.profile", + "repo:sh.tangled.feed.reaction", + "repo:sh.tangled.feed.star", + "repo:sh.tangled.graph.follow", + "repo:sh.tangled.graph.vouch", + "repo:sh.tangled.knot", + "repo:sh.tangled.knot.member", + "repo:sh.tangled.label.definition", + "repo:sh.tangled.label.op", "repo:sh.tangled.publicKey", "repo:sh.tangled.repo", - "repo:sh.tangled.repo.pull", - "repo:sh.tangled.repo.pull.comment", "repo:sh.tangled.repo.artifact", + "repo:sh.tangled.repo.collaborator", "repo:sh.tangled.repo.issue", "repo:sh.tangled.repo.issue.comment", - "repo:sh.tangled.repo.collaborator", - "repo:sh.tangled.knot", - "repo:sh.tangled.knot.member", + "repo:sh.tangled.repo.pull", + "repo:sh.tangled.repo.pull.comment", "repo:sh.tangled.spindle", "repo:sh.tangled.spindle.member", - "repo:sh.tangled.graph.follow", - "repo:sh.tangled.feed.star", - "repo:sh.tangled.feed.reaction", - "repo:sh.tangled.label.definition", - "repo:sh.tangled.label.op", "repo:sh.tangled.string", - "repo:sh.tangled.actor.profile", "blob:*/*", + "rpc:sh.tangled.pipeline.cancelPipeline?aud=*", + "rpc:sh.tangled.repo.addSecret?aud=*", "rpc:sh.tangled.repo.create?aud=*", "rpc:sh.tangled.repo.delete?aud=*", - "rpc:sh.tangled.repo.merge?aud=*", - "rpc:sh.tangled.repo.hiddenRef?aud=*", "rpc:sh.tangled.repo.deleteBranch?aud=*", - "rpc:sh.tangled.repo.setDefaultBranch?aud=*", - "rpc:sh.tangled.repo.forkSync?aud=*", "rpc:sh.tangled.repo.forkStatus?aud=*", + "rpc:sh.tangled.repo.forkSync?aud=*", + "rpc:sh.tangled.repo.hiddenRef?aud=*", + "rpc:sh.tangled.repo.listSecrets?aud=*", + "rpc:sh.tangled.repo.merge?aud=*", "rpc:sh.tangled.repo.mergeCheck?aud=*", - "rpc:sh.tangled.pipeline.cancelPipeline?aud=*", - "rpc:sh.tangled.repo.addSecret?aud=*", "rpc:sh.tangled.repo.removeSecret?aud=*", - "rpc:sh.tangled.repo.listSecrets?aud=*", + "rpc:sh.tangled.repo.setDefaultBranch?aud=*", } diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 8b1f6adc..39c7ce37 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -604,13 +604,14 @@ func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { } type ProfileCard struct { - UserDid string - HasProfile bool - FollowStatus models.FollowStatus - Punchcard *models.Punchcard - Profile *models.Profile - Stats ProfileStats - Active string + UserDid string + HasProfile bool + FollowStatus models.FollowStatus + VouchRelationship *models.VouchRelationship + Punchcard *models.Punchcard + Profile *models.Profile + Stats ProfileStats + Active string } type ProfileStats struct { diff --git a/appview/pages/templates/user/fragments/profileCard.html b/appview/pages/templates/user/fragments/profileCard.html index c9b5e147..a19314df 100644 --- a/appview/pages/templates/user/fragments/profileCard.html +++ b/appview/pages/templates/user/fragments/profileCard.html @@ -87,7 +87,7 @@ {{ end }} -
+
{{ if ne .FollowStatus.String "IsSelf" }} {{ template "user/fragments/follow" . }} {{ else }} @@ -108,6 +108,10 @@
+ {{ if ne .FollowStatus.String "IsSelf" }} + {{ template "user/fragments/vouch" . }} + {{ end }} +
@@ -115,15 +119,15 @@ {{ end }} {{ define "followerFollowing" }} -{{ $root := index . 0 }} -{{ $userIdent := index . 1 }} -{{ with $root }} - + {{ $root := index . 0 }} + {{ $userIdent := index . 1 }} + {{ with $root }} + + {{ end }} {{ end }} -{{ end }} \ No newline at end of file diff --git a/appview/pages/templates/user/fragments/vouch.html b/appview/pages/templates/user/fragments/vouch.html new file mode 100644 index 00000000..cbac8d7c --- /dev/null +++ b/appview/pages/templates/user/fragments/vouch.html @@ -0,0 +1,197 @@ +{{ define "user/fragments/vouch" }} +{{ $userIdent := resolve .UserDid }} +{{ $isVouched := false }} +{{ $isDenounced := false }} +{{ $isUndecided := false }} +{{ if .VouchRelationship }} + {{ if .VouchRelationship.IsDirectVouch }} + {{ $isVouched = true }} + {{ else if .VouchRelationship.IsDirectDenounce }} + {{ $isDenounced = true }} + {{ else }} + {{ $isUndecided = true }} + {{ end }} +{{ else }} + {{ $isUndecided = true }} +{{ end }} +
+ + +
+ +
+ +

Vouch for {{ $userIdent }}

+ {{ with .VouchRelationship }} + {{ with .GetDirectVouch }} +

+ You {{if $isVouched}}vouched{{else}}denounced{{end}} {{ $userIdent }} {{ relTimeFmt .CreatedAt }}. + You can change your descision below. +

+ {{ end }} + {{ end }} + + {{ if .VouchRelationship }} + {{ if .VouchRelationship.IndirectVouches }} +

Vouches from your network

+ {{ template "networkVouches" . }} + {{ end }} + {{ end }} + + + + {{ $labelClass := "grid grid-cols-[auto_1fr_auto] items-center gap-2 rounded p-2 py- ring-1 ring-gray-200 dark:ring-gray-700 cursor-pointer" }} + +
+ + + + + + + + + +
+ +
+ + +
+
+ +
+
+
+{{ end }} + +{{ define "networkVouches" }} + {{ $style := "flex flex-col border divide-y rounded" }} + {{ $indirectVouches := .VouchRelationship.IndirectVouches }} + + {{ $bg := "bg-green-200 dark:bg-green-700" }} + {{ $fg := "text-green-800 dark:text-green-200" }} + {{ $border := "border-green-400 dark:border-green-600" }} +
+ {{ range .VouchRelationship.IndirectVouches }} + {{ if .IsVouch }} + {{ template "networkVouchItem" . }} + {{ end }} + {{ end }} +
+ + {{ $bg = "bg-red-200 dark:bg-red-700" }} + {{ $fg = "text-red-800 dark:text-red-200" }} + {{ $border = "border-red-400 dark:border-red-600" }} +
+ {{ range .VouchRelationship.IndirectVouches }} + {{ if .IsDenounce }} + {{ template "networkVouchItem" . }} + {{ end }} + {{ end }} +
+{{ end }} + +{{ define "networkVouchItem" }} +
+
+ {{ template "user/fragments/picLink" (list .Did "size-8") }} +
+
+
+ {{ resolve .Did }} + + {{ template "repo/fragments/shortTime" .CreatedAt }} +
+
+ {{ .Reason | description }} +
+
+
+{{ end }} diff --git a/appview/state/profile.go b/appview/state/profile.go index 44b15a93..e6dedb81 100644 --- a/appview/state/profile.go +++ b/appview/state/profile.go @@ -91,8 +91,11 @@ func (s *State) profile(r *http.Request) (*pages.ProfileCard, error) { loggedInUser := s.oauth.GetMultiAccountUser(r) followStatus := models.IsNotFollowing + var vouchRelationship *models.VouchRelationship + if loggedInUser != nil { followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did) + vouchRelationship, err = db.GetVouchRelationship(s.db, loggedInUser.Did, did) } showPunchcard := s.shouldShowPunchcard(did, loggedInUser.Did) @@ -113,10 +116,11 @@ func (s *State) profile(r *http.Request) (*pages.ProfileCard, error) { } return &pages.ProfileCard{ - UserDid: did, - HasProfile: hasProfile, - Profile: profile, - FollowStatus: followStatus, + UserDid: did, + HasProfile: hasProfile, + Profile: profile, + FollowStatus: followStatus, + VouchRelationship: vouchRelationship, Stats: pages.ProfileStats{ RepoCount: repoCount, StringCount: stringCount, @@ -174,13 +178,16 @@ func (s *State) profileOverview(w http.ResponseWriter, r *http.Request) { l.Error("failed to create timeline", "err", err) } - s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ + err = s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ LoggedInUser: s.oauth.GetMultiAccountUser(r), Card: profile, Repos: pinnedRepos, CollaboratingRepos: pinnedCollaboratingRepos, ProfileTimeline: timeline, }) + if err != nil { + l.Error("failed to render template", "err", err) + } } func (s *State) shouldShowPunchcard(targetDid, requesterDid string) bool { diff --git a/appview/state/router.go b/appview/state/router.go index 63a6bff8..ae604dd1 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -180,7 +180,6 @@ func (s *State) StandardRouter(mw *middleware.Middleware) http.Handler { r.With(middleware.AuthMiddleware(s.oauth)).Route("/vouch", func(r chi.Router) { r.Post("/", s.Vouch) - r.Delete("/", s.Vouch) }) r.With(middleware.AuthMiddleware(s.oauth)).Route("/star", func(r chi.Router) { diff --git a/appview/state/state.go b/appview/state/state.go index 6899e5ae..dc74fbc5 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -115,21 +115,22 @@ func Make(ctx context.Context, config *config.Config) (*State, error) { config.Jetstream.Endpoint, "appview", []string{ - tangled.GraphFollowNSID, + tangled.ActorProfileNSID, tangled.FeedStarNSID, + tangled.GraphFollowNSID, + tangled.GraphVouchNSID, + tangled.KnotMemberNSID, + tangled.KnotNSID, + tangled.LabelDefinitionNSID, + tangled.LabelOpNSID, tangled.PublicKeyNSID, tangled.RepoArtifactNSID, - tangled.ActorProfileNSID, - tangled.KnotMemberNSID, + tangled.RepoIssueCommentNSID, + tangled.RepoIssueNSID, + tangled.RepoPullNSID, tangled.SpindleMemberNSID, tangled.SpindleNSID, - tangled.KnotNSID, tangled.StringNSID, - tangled.RepoPullNSID, - tangled.RepoIssueNSID, - tangled.RepoIssueCommentNSID, - tangled.LabelDefinitionNSID, - tangled.LabelOpNSID, }, nil, tlog.SubLogger(logger, "jetstream"), diff --git a/appview/state/vouch.go b/appview/state/vouch.go index a008c9e1..ebeb3dd3 100644 --- a/appview/state/vouch.go +++ b/appview/state/vouch.go @@ -1,112 +1,67 @@ package state import ( - "encoding/json" "net/http" "time" comatproto "github.com/bluesky-social/indigo/api/atproto" lexutil "github.com/bluesky-social/indigo/lex/util" + "github.com/ipfs/go-cid" "tangled.org/core/api/tangled" "tangled.org/core/appview/db" "tangled.org/core/appview/models" + "tangled.org/core/log" ) func (s *State) Vouch(w http.ResponseWriter, r *http.Request) { - l := s.logger.With("handler", "Vouch") + l := log.SubLogger(s.logger, "vouch") + l = s.logger.With("handler", "Vouch") currentUser := s.oauth.GetMultiAccountUser(r) - subject := r.URL.Query().Get("subject") + var subject string + + subject = r.FormValue("subject") + if subject == "" { l.Warn("invalid form: missing subject") - http.Error(w, "missing subject", http.StatusBadRequest) + s.pages.Notice(w, "error", "Missing subject user.") return } - kind := r.URL.Query().Get("kind") - if kind == "" { - kind = "vouch" - } - if kind != "vouch" && kind != "denounce" { - l.Warn("invalid kind", "kind", kind) - http.Error(w, "invalid kind", http.StatusBadRequest) - return - } - - reason := r.URL.Query().Get("reason") - subjectIdent, err := s.idResolver.ResolveIdent(r.Context(), subject) if err != nil { l.Error("failed to vouch, invalid did", "subject", subject, "err", err) - http.Error(w, "invalid subject", http.StatusBadRequest) + s.pages.Notice(w, "error", "Could not find that user.") return } if currentUser.Active.Did == subjectIdent.DID.String() { l.Warn("cant vouch or denounce yourself") - http.Error(w, "cannot vouch yourself", http.StatusBadRequest) + s.pages.Notice(w, "error", "You cannot vouch for yourself.") return } client, err := s.oauth.AuthorizedClient(r) if err != nil { l.Error("failed to authorize client", "err", err) - http.Error(w, "unauthorized", http.StatusUnauthorized) + s.pages.Notice(w, "error", "Authentication required.") return } - switch r.Method { - case http.MethodPost: - createdAt := time.Now().Format(time.RFC3339) - subjectDid := subjectIdent.DID.String() - - var reasonPtr *string - if reason != "" { - reasonPtr = &reason - } - - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ - Collection: tangled.GraphVouchNSID, - Repo: currentUser.Active.Did, - Rkey: subjectDid, - Record: &lexutil.LexiconTypeDecoder{ - Val: &tangled.GraphVouch{ - Kind: &kind, - Reason: reasonPtr, - CreatedAt: createdAt, - }}, - }) - if err != nil { - l.Error("failed to create atproto record", "err", err) - http.Error(w, "failed to create vouch", http.StatusInternalServerError) - return - } - - l.Info("created atproto record", "uri", resp.Uri, "kind", kind) - - vouch := &models.Vouch{ - Did: currentUser.Active.Did, - SubjectDid: subjectDid, - Kind: kind, - Reason: reasonPtr, - } - - err = db.AddVouch(s.db, vouch) - if err != nil { - l.Error("failed to add vouch to db", "err", err) - http.Error(w, "failed to add vouch", http.StatusInternalServerError) - return - } - + if err := r.ParseForm(); err != nil { + l.Warn("failed to parse form", "err", err) + s.pages.Notice(w, "error", "Invalid form data.") return + } - case http.MethodDelete: - subjectDid := subjectIdent.DID.String() + subjectDid := subjectIdent.DID.String() + // handle "none" by deleting any existing vouch + if r.FormValue("kind") == "none" { _, err := db.GetVouch(s.db, currentUser.Active.Did, subjectDid) if err != nil { - l.Error("failed to get vouch relationship", "err", err) - http.Error(w, "vouch not found", http.StatusNotFound) + l.Info("no existing vouch to delete") + s.pages.HxRefresh(w) return } @@ -118,7 +73,7 @@ func (s *State) Vouch(w http.ResponseWriter, r *http.Request) { if err != nil { l.Error("failed to delete vouch record", "err", err) - http.Error(w, "failed to delete vouch", http.StatusInternalServerError) + s.pages.Notice(w, "error", "Failed to delete vouch record.") return } @@ -127,6 +82,74 @@ func (s *State) Vouch(w http.ResponseWriter, r *http.Request) { l.Warn("failed to delete vouch from DB", "err", err) } + l.Info("deleted vouch record") + s.pages.HxRefresh(w) return } + + kind, err := models.ParseVouchKind(r.FormValue("kind")) + if err != nil { + l.Warn("failed to parse vouch kind", "err", err) + s.pages.Notice(w, "error", "Invalid action type.") + return + } + + reason := r.FormValue("reason") + createdAt := time.Now().Format(time.RFC3339) + + var reasonPtr *string + if reason != "" { + reasonPtr = &reason + } + + var swapCid *string + existingVouch, err := db.GetVouch(s.db, currentUser.Active.Did, subjectDid) + if err == nil { + cidStr := existingVouch.Cid.String() + swapCid = &cidStr + } + + resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + Collection: tangled.GraphVouchNSID, + Repo: currentUser.Active.Did, + Rkey: subjectDid, + SwapRecord: swapCid, + Record: &lexutil.LexiconTypeDecoder{ + Val: &tangled.GraphVouch{ + Kind: string(kind), + Reason: reasonPtr, + CreatedAt: createdAt, + }}, + }) + if err != nil { + l.Error("failed to create atproto record", "err", err) + s.pages.Notice(w, "error", "Failed to create vouch record.") + return + } + + l.Info("created atproto record", "uri", resp.Uri, "kind", kind) + + newCid, err := cid.Parse(resp.Cid) + if err != nil { + l.Error("failed to parse returned cid", "err", err) + s.pages.Notice(w, "error", "Failed to save vouch.") + return + } + + vouch := &models.Vouch{ + Did: currentUser.Active.Did, + SubjectDid: subjectDid, + Cid: newCid, + Kind: kind, + Reason: reasonPtr, + } + + err = db.AddVouch(s.db, vouch) + if err != nil { + l.Error("failed to add vouch to db", "err", err) + s.pages.Notice(w, "error", "Failed to save vouch.") + return + } + + s.pages.HxRefresh(w) } diff --git a/input.css b/input.css index dc094b35..023b7866 100644 --- a/input.css +++ b/input.css @@ -147,6 +147,15 @@ disabled:before:bg-green-400 dark:disabled:before:bg-green-600; } + .btn-cancel { + @apply btn text-white + before:bg-red-600 hover:before:bg-red-700 + dark:before:bg-red-700 dark:hover:before:bg-red-800 + before:border before:border-red-700 hover:before:border-red-800 + focus-visible:before:outline-red-500 + disabled:before:bg-red-400 dark:disabled:before:bg-red-600; + } + .prose { overflow-wrap: anywhere; } diff --git a/lexicons/graph/vouch.json b/lexicons/graph/vouch.json index 834d1fe8..a9da4248 100644 --- a/lexicons/graph/vouch.json +++ b/lexicons/graph/vouch.json @@ -10,6 +10,7 @@ "record": { "type": "object", "required": [ + "kind", "createdAt" ], "properties": { @@ -21,7 +22,9 @@ }, "reason": { "type": "string", - "description": "The reason for this vouch/denouncement" + "description": "The reason for this vouch/denouncement", + "maxGraphemes": 256, + "maxLength": 2560 }, "createdAt": { "type": "string", -- 2.51.2