Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

appview: show banner for users with read-only knots

Signed-off-by: oppiliappan <me@oppi.li>

+188 -167
+72 -138
appview/db/registration.go
··· 1 1 package db 2 2 3 3 import ( 4 - "crypto/rand" 5 4 "database/sql" 6 - "encoding/hex" 7 5 "fmt" 8 - "log" 9 6 "strings" 10 7 "time" 11 8 ) ··· 15 18 ByDid string 16 19 Created *time.Time 17 20 Registered *time.Time 21 + ReadOnly bool 18 22 } 19 23 20 24 func (r *Registration) Status() Status { 21 - if r.Registered != nil { 25 + if r.ReadOnly { 26 + return ReadOnly 27 + } else if r.Registered != nil { 22 28 return Registered 23 29 } else { 24 30 return Pending 25 31 } 32 + } 33 + 34 + func (r *Registration) IsRegistered() bool { 35 + return r.Status() == Registered 36 + } 37 + 38 + func (r *Registration) IsReadOnly() bool { 39 + return r.Status() == ReadOnly 40 + } 41 + 42 + func (r *Registration) IsPending() bool { 43 + return r.Status() == Pending 26 44 } 27 45 28 46 type Status uint32 ··· 45 33 const ( 46 34 Registered Status = iota 47 35 Pending 36 + ReadOnly 48 37 ) 49 38 50 - // returns registered status, did of owner, error 51 - func RegistrationsByDid(e Execer, did string) ([]Registration, error) { 39 + func GetRegistrations(e Execer, filters ...filter) ([]Registration, error) { 52 40 var registrations []Registration 53 41 54 - rows, err := e.Query(` 55 - select id, domain, did, created, registered from registrations 56 - where did = ? 57 - `, did) 42 + var conditions []string 43 + var args []any 44 + for _, filter := range filters { 45 + conditions = append(conditions, filter.Condition()) 46 + args = append(args, filter.Arg()...) 47 + } 48 + 49 + whereClause := "" 50 + if conditions != nil { 51 + whereClause = " where " + strings.Join(conditions, " and ") 52 + } 53 + 54 + query := fmt.Sprintf(` 55 + select id, domain, did, created, registered, read_only 56 + from registrations 57 + %s 58 + order by created 59 + `, 60 + whereClause, 61 + ) 62 + 63 + rows, err := e.Query(query, args...) 58 64 if err != nil { 59 65 return nil, err 60 66 } 61 67 62 68 for rows.Next() { 63 - var createdAt *string 64 - var registeredAt *string 65 - var registration Registration 66 - err = rows.Scan(&registration.Id, &registration.Domain, &registration.ByDid, &createdAt, &registeredAt) 69 + var createdAt string 70 + var registeredAt sql.Null[string] 71 + var readOnly int 72 + var reg Registration 67 73 74 + err = rows.Scan(&reg.Id, &reg.Domain, &reg.ByDid, &createdAt, &registeredAt, &readOnly) 68 75 if err != nil { 69 - log.Println(err) 70 - } else { 71 - createdAtTime, _ := time.Parse(time.RFC3339, *createdAt) 72 - var registeredAtTime *time.Time 73 - if registeredAt != nil { 74 - x, _ := time.Parse(time.RFC3339, *registeredAt) 75 - registeredAtTime = &x 76 - } 77 - 78 - registration.Created = &createdAtTime 79 - registration.Registered = registeredAtTime 80 - registrations = append(registrations, registration) 76 + return nil, err 81 77 } 78 + 79 + if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 80 + reg.Created = &t 81 + } 82 + 83 + if registeredAt.Valid { 84 + if t, err := time.Parse(time.RFC3339, registeredAt.V); err == nil { 85 + reg.Registered = &t 86 + } 87 + } 88 + 89 + if readOnly != 0 { 90 + reg.ReadOnly = true 91 + } 92 + 93 + registrations = append(registrations, reg) 82 94 } 83 95 84 96 return registrations, nil 85 97 } 86 98 87 - // returns registered status, did of owner, error 88 - func RegistrationByDomain(e Execer, domain string) (*Registration, error) { 89 - var createdAt *string 90 - var registeredAt *string 91 - var registration Registration 92 - 93 - err := e.QueryRow(` 94 - select id, domain, did, created, registered from registrations 95 - where domain = ? 96 - `, domain).Scan(&registration.Id, &registration.Domain, &registration.ByDid, &createdAt, &registeredAt) 97 - 98 - if err != nil { 99 - if err == sql.ErrNoRows { 100 - return nil, nil 101 - } else { 102 - return nil, err 103 - } 99 + func MarkRegistered(e Execer, filters ...filter) error { 100 + var conditions []string 101 + var args []any 102 + for _, filter := range filters { 103 + conditions = append(conditions, filter.Condition()) 104 + args = append(args, filter.Arg()...) 104 105 } 105 106 106 - createdAtTime, _ := time.Parse(time.RFC3339, *createdAt) 107 - var registeredAtTime *time.Time 108 - if registeredAt != nil { 109 - x, _ := time.Parse(time.RFC3339, *registeredAt) 110 - registeredAtTime = &x 107 + query := "update registrations set registered = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), read_only = 0" 108 + if len(conditions) > 0 { 109 + query += " where " + strings.Join(conditions, " and ") 111 110 } 112 111 113 - registration.Created = &createdAtTime 114 - registration.Registered = registeredAtTime 115 - 116 - return &registration, nil 117 - } 118 - 119 - func genSecret() string { 120 - key := make([]byte, 32) 121 - rand.Read(key) 122 - return hex.EncodeToString(key) 123 - } 124 - 125 - func GenerateRegistrationKey(e Execer, domain, did string) (string, error) { 126 - // sanity check: does this domain already have a registration? 127 - reg, err := RegistrationByDomain(e, domain) 128 - if err != nil { 129 - return "", err 130 - } 131 - 132 - // registration is open 133 - if reg != nil { 134 - switch reg.Status() { 135 - case Registered: 136 - // already registered by `owner` 137 - return "", fmt.Errorf("%s already registered by %s", domain, reg.ByDid) 138 - case Pending: 139 - // TODO: be loud about this 140 - log.Printf("%s registered by %s, status pending", domain, reg.ByDid) 141 - } 142 - } 143 - 144 - secret := genSecret() 145 - 146 - _, err = e.Exec(` 147 - insert into registrations (domain, did, secret) 148 - values (?, ?, ?) 149 - on conflict(domain) do update set did = excluded.did, secret = excluded.secret, created = excluded.created 150 - `, domain, did, secret) 151 - 152 - if err != nil { 153 - return "", err 154 - } 155 - 156 - return secret, nil 157 - } 158 - 159 - func GetRegistrationKey(e Execer, domain string) (string, error) { 160 - res := e.QueryRow(`select secret from registrations where domain = ?`, domain) 161 - 162 - var secret string 163 - err := res.Scan(&secret) 164 - if err != nil || secret == "" { 165 - return "", err 166 - } 167 - 168 - return secret, nil 169 - } 170 - 171 - func GetCompletedRegistrations(e Execer) ([]string, error) { 172 - rows, err := e.Query(`select domain from registrations where registered not null`) 173 - if err != nil { 174 - return nil, err 175 - } 176 - 177 - var domains []string 178 - for rows.Next() { 179 - var domain string 180 - err = rows.Scan(&domain) 181 - 182 - if err != nil { 183 - log.Println(err) 184 - } else { 185 - domains = append(domains, domain) 186 - } 187 - } 188 - 189 - if err = rows.Err(); err != nil { 190 - return nil, err 191 - } 192 - 193 - return domains, nil 194 - } 195 - 196 - func Register(e Execer, domain string) error { 197 - _, err := e.Exec(` 198 - update registrations 199 - set registered = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 200 - where domain = ?; 201 - `, domain) 202 - 112 + _, err := e.Exec(query, args...) 203 113 return err 204 114 } 205 115
+40 -16
appview/knots/knots.go
··· 3 3 import ( 4 4 "errors" 5 5 "fmt" 6 + "log" 6 7 "log/slog" 7 8 "net/http" 8 9 "slices" ··· 50 49 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/add", k.addMember) 51 50 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/remove", k.removeMember) 52 51 52 + r.With(middleware.AuthMiddleware(k.OAuth)).Get("/upgradeBanner", k.banner) 53 + 53 54 return r 54 55 } 55 56 56 57 func (k *Knots) knots(w http.ResponseWriter, r *http.Request) { 57 58 user := k.OAuth.GetUser(r) 58 - registrations, err := db.RegistrationsByDid(k.Db, user.Did) 59 + registrations, err := db.GetRegistrations( 60 + k.Db, 61 + db.FilterEq("did", user.Did), 62 + ) 59 63 if err != nil { 60 64 k.Logger.Error("failed to fetch knot registrations", "err", err) 61 65 w.WriteHeader(http.StatusInternalServerError) ··· 95 89 http.Error(w, "Not found", http.StatusNotFound) 96 90 return 97 91 } 98 - 99 - // Find the specific registration for this domain 100 - var registration *db.Registration 101 - for _, reg := range registrations { 102 - if reg.Domain == domain && reg.ByDid == user.Did && reg.Registered != nil { 103 - registration = &reg 104 - break 105 - } 106 - } 107 - 108 - if registration == nil { 109 - l.Error("registration not found or not verified") 110 - http.Error(w, "Not found", http.StatusNotFound) 92 + if len(registrations) != 1 { 93 + l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1) 111 94 return 112 95 } 113 96 registration := registrations[0] ··· 513 518 db.FilterIsNot("registered", "null"), 514 519 ) 515 520 if err != nil { 516 - l.Error("failed to retrieve domain registration", "err", err) 517 - http.Error(w, "Not found", http.StatusNotFound) 521 + l.Error("failed to get registration", "err", err) 518 522 return 519 523 } 524 + if len(registrations) != 1 { 525 + l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1) 526 + return 527 + } 528 + registration := registrations[0] 520 529 521 530 noticeId := fmt.Sprintf("add-member-error-%d", registration.Id) 522 531 defaultErr := "Failed to add member. Try again later." ··· 677 678 678 679 // ok 679 680 k.Pages.HxRefresh(w) 681 + } 682 + 683 + func (k *Knots) banner(w http.ResponseWriter, r *http.Request) { 684 + user := k.OAuth.GetUser(r) 685 + l := k.Logger.With("handler", "removeMember") 686 + l = l.With("did", user.Did) 687 + l = l.With("handle", user.Handle) 688 + 689 + registrations, err := db.GetRegistrations( 690 + k.Db, 691 + db.FilterEq("did", user.Did), 692 + db.FilterEq("read_only", 1), 693 + ) 694 + if err != nil { 695 + l.Error("non-fatal: failed to get registrations") 696 + return 697 + } 698 + 699 + if registrations == nil { 700 + return 701 + } 702 + 703 + k.Pages.KnotBanner(w, pages.KnotBannerParams{ 704 + Registrations: registrations, 705 + }) 680 706 }
+9 -1
appview/pages/pages.go
··· 338 338 return p.execute("user/settings/emails", w, params) 339 339 } 340 340 341 + type KnotBannerParams struct { 342 + Registrations []db.Registration 343 + } 344 + 345 + func (p *Pages) KnotBanner(w io.Writer, params KnotBannerParams) error { 346 + return p.executePlain("knots/fragments/banner", w, params) 347 + } 348 + 341 349 type KnotsParams struct { 342 350 LoggedInUser *oauth.User 343 351 Registrations []db.Registration ··· 368 360 } 369 361 370 362 type KnotListingParams struct { 371 - db.Registration 363 + *db.Registration 372 364 } 373 365 374 366 func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error {
+9
appview/pages/templates/knots/fragments/banner.html
··· 1 + {{ define "knots/fragments/banner" }} 2 + <div class="w-full px-6 py-2 -z-15 bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 border border-yellow-200 dark:border-yellow-800 rounded-b drop-shadow-sm"> 3 + A knot ({{range $i, $r := .Registrations}}{{if ne $i 0}}, {{end}}{{ $r.Domain }}{{ end }}) 4 + that you administer is presently read-only. Consider upgrading this knot to 5 + continue creating repositories on it. 6 + <a href="https://tangled.sh/@tangled.sh/core/tree/master/docs/migrations">Click to read the upgrade guide</a>. 7 + </div> 8 + {{ end }} 9 +
+11 -2
appview/pages/templates/knots/fragments/knotListing.html
··· 30 30 {{ define "knotRightSide" }} 31 31 <div id="right-side" class="flex gap-2"> 32 32 {{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2 text-sm" }} 33 - {{ if .Registered }} 34 - <span class="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 {{$style}}">{{ i "shield-check" "w-4 h-4" }} verified</span> 33 + {{ if .IsRegistered }} 34 + <span class="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 {{$style}}"> 35 + {{ i "shield-check" "w-4 h-4" }} verified 36 + </span> 35 37 {{ template "knots/fragments/addMemberModal" . }} 38 + {{ block "knotDeleteButton" . }} {{ end }} 39 + {{ else if .IsReadOnly }} 40 + <span class="bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 {{$style}}"> 41 + {{ i "shield-alert" "w-4 h-4" }} read-only 42 + </span> 43 + {{ block "knotRetryButton" . }} {{ end }} 44 + {{ block "knotDeleteButton" . }} {{ end }} 36 45 {{ else }} 37 46 <span class="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 {{$style}}"> 38 47 {{ i "shield-off" "w-4 h-4" }} unverified
+1 -1
appview/pages/templates/knots/index.html
··· 77 77 </button> 78 78 </div> 79 79 80 - <div id="registration-error" class="error dark:text-red-400"></div> 80 + <div id="register-error" class="error dark:text-red-400"></div> 81 81 </form> 82 82 83 83 </section>
+7
appview/pages/templates/layouts/topbar.html
··· 21 21 </div> 22 22 </div> 23 23 </nav> 24 + {{ if .LoggedInUser }} 25 + <div id="upgrade-banner" 26 + hx-get="/knots/upgradeBanner" 27 + hx-trigger="load" 28 + hx-swap="innerHTML"> 29 + </div> 30 + {{ end }} 24 31 {{ end }} 25 32 26 33 {{ define "newButton" }}
+3 -1
appview/pages/templates/spindles/fragments/spindleListing.html
··· 9 9 {{ if .Verified }} 10 10 <a href="/spindles/{{ .Instance }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]"> 11 11 {{ i "hard-drive" "w-4 h-4" }} 12 - {{ .Instance }} 12 + <span class="hover:underline"> 13 + {{ .Instance }} 14 + </span> 13 15 <span class="text-gray-500"> 14 16 {{ template "repo/fragments/shortTimeAgo" .Created }} 15 17 </span>
+5 -2
appview/state/knotstream.go
··· 24 24 ) 25 25 26 26 func Knotstream(ctx context.Context, c *config.Config, d *db.DB, enforcer *rbac.Enforcer, posthog posthog.Client) (*ec.Consumer, error) { 27 - knots, err := db.GetCompletedRegistrations(d) 27 + knots, err := db.GetRegistrations( 28 + d, 29 + db.FilterIsNot("registered", "null"), 30 + ) 28 31 if err != nil { 29 32 return nil, err 30 33 } 31 34 32 35 srcs := make(map[ec.Source]struct{}) 33 36 for _, k := range knots { 34 - s := ec.NewKnotSource(k) 37 + s := ec.NewKnotSource(k.Domain) 35 38 srcs[s] = struct{}{} 36 39 } 37 40
+3 -3
appview/state/state.go
··· 435 435 Rkey: rkey, 436 436 }, 437 437 ) 438 - if err != nil { 439 - l.Error("xrpc request failed", "err", err) 440 - s.pages.Notice(w, "repo", fmt.Sprintf("Failed to create repository on knot server: %s.", err.Error())) 438 + if err := xrpcclient.HandleXrpcErr(xe); err != nil { 439 + l.Error("xrpc error", "xe", xe) 440 + s.pages.Notice(w, "repo", err.Error()) 441 441 return 442 442 } 443 443
+25
appview/xrpcclient/xrpc.go
··· 3 3 import ( 4 4 "bytes" 5 5 "context" 6 + "errors" 7 + "fmt" 6 8 "io" 9 + "net/http" 7 10 8 11 "github.com/bluesky-social/indigo/api/atproto" 9 12 "github.com/bluesky-social/indigo/xrpc" 13 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 10 14 oauth "tangled.sh/icyphox.sh/atproto-oauth" 11 15 ) 12 16 ··· 105 101 } 106 102 107 103 return &out, nil 104 + } 105 + 106 + // produces a more manageable error 107 + func HandleXrpcErr(err error) error { 108 + if err == nil { 109 + return nil 110 + } 111 + 112 + var xrpcerr *indigoxrpc.Error 113 + if ok := errors.As(err, &xrpcerr); !ok { 114 + return fmt.Errorf("Recieved invalid XRPC error response.") 115 + } 116 + 117 + switch xrpcerr.StatusCode { 118 + case http.StatusNotFound: 119 + return fmt.Errorf("XRPC is unsupported on this knot, consider upgrading your knot.") 120 + case http.StatusUnauthorized: 121 + return fmt.Errorf("Unauthorized XRPC request.") 122 + default: 123 + return fmt.Errorf("Failed to perform operation. Try again later.") 124 + } 108 125 }
+3 -3
knotserver/ingester.go
··· 73 73 } 74 74 l.Info("added member from firehose", "member", record.Subject) 75 75 76 - if err := h.db.AddDid(did); err != nil { 76 + if err := h.db.AddDid(record.Subject); err != nil { 77 77 l.Error("failed to add did", "error", err) 78 78 return fmt.Errorf("failed to add did: %w", err) 79 79 } 80 - h.jc.AddDid(did) 80 + h.jc.AddDid(record.Subject) 81 81 82 - if err := h.fetchAndAddKeys(ctx, did); err != nil { 82 + if err := h.fetchAndAddKeys(ctx, record.Subject); err != nil { 83 83 return fmt.Errorf("failed to fetch and add keys: %w", err) 84 84 } 85 85