···81818282func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
8383 l := rp.logger.With("handler", "RepoSingleIssue")
8484- user := rp.oauth.GetUser(r)
8484+ user := rp.oauth.GetMultiAccountUser(r)
8585 f, err := rp.repoResolver.Resolve(r)
8686 if err != nil {
8787 l.Error("failed to get repo and knot", "err", err)
···102102103103 userReactions := map[models.ReactionKind]bool{}
104104 if user != nil {
105105- userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri())
105105+ userReactions = db.GetReactionStatusMap(rp.db, user.Active.Did, issue.AtUri())
106106 }
107107108108 backlinks, err := db.GetBacklinks(rp.db, issue.AtUri())
···143143144144func (rp *Issues) EditIssue(w http.ResponseWriter, r *http.Request) {
145145 l := rp.logger.With("handler", "EditIssue")
146146- user := rp.oauth.GetUser(r)
146146+ user := rp.oauth.GetMultiAccountUser(r)
147147148148 issue, ok := r.Context().Value("issue").(*models.Issue)
149149 if !ok {
···182182 return
183183 }
184184185185- ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueNSID, user.Did, newIssue.Rkey)
185185+ ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueNSID, user.Active.Did, newIssue.Rkey)
186186 if err != nil {
187187 l.Error("failed to get record", "err", err)
188188 rp.pages.Notice(w, noticeId, "Failed to edit issue, no record found on PDS.")
···191191192192 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
193193 Collection: tangled.RepoIssueNSID,
194194- Repo: user.Did,
194194+ Repo: user.Active.Did,
195195 Rkey: newIssue.Rkey,
196196 SwapRecord: ex.Cid,
197197 Record: &lexutil.LexiconTypeDecoder{
···292292293293func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) {
294294 l := rp.logger.With("handler", "CloseIssue")
295295- user := rp.oauth.GetUser(r)
295295+ user := rp.oauth.GetMultiAccountUser(r)
296296 f, err := rp.repoResolver.Resolve(r)
297297 if err != nil {
298298 l.Error("failed to get repo and knot", "err", err)
···306306 return
307307 }
308308309309- roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
309309+ roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
310310 isRepoOwner := roles.IsOwner()
311311 isCollaborator := roles.IsCollaborator()
312312- isIssueOwner := user.Did == issue.Did
312312+ isIssueOwner := user.Active.Did == issue.Did
313313314314 // TODO: make this more granular
315315 if isIssueOwner || isRepoOwner || isCollaborator {
···326326 issue.Open = false
327327328328 // notify about the issue closure
329329- rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue)
329329+ rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Active.Did), issue)
330330331331 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
332332 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
···340340341341func (rp *Issues) ReopenIssue(w http.ResponseWriter, r *http.Request) {
342342 l := rp.logger.With("handler", "ReopenIssue")
343343- user := rp.oauth.GetUser(r)
343343+ user := rp.oauth.GetMultiAccountUser(r)
344344 f, err := rp.repoResolver.Resolve(r)
345345 if err != nil {
346346 l.Error("failed to get repo and knot", "err", err)
···354354 return
355355 }
356356357357- roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
357357+ roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
358358 isRepoOwner := roles.IsOwner()
359359 isCollaborator := roles.IsCollaborator()
360360- isIssueOwner := user.Did == issue.Did
360360+ isIssueOwner := user.Active.Did == issue.Did
361361362362 if isCollaborator || isRepoOwner || isIssueOwner {
363363 err := db.ReopenIssues(
···373373 issue.Open = true
374374375375 // notify about the issue reopen
376376- rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue)
376376+ rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Active.Did), issue)
377377378378 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
379379 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
···387387388388func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) {
389389 l := rp.logger.With("handler", "NewIssueComment")
390390- user := rp.oauth.GetUser(r)
390390+ user := rp.oauth.GetMultiAccountUser(r)
391391 f, err := rp.repoResolver.Resolve(r)
392392 if err != nil {
393393 l.Error("failed to get repo and knot", "err", err)
···416416 mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
417417418418 comment := models.IssueComment{
419419- Did: user.Did,
419419+ Did: user.Active.Did,
420420 Rkey: tid.TID(),
421421 IssueAt: issue.AtUri().String(),
422422 ReplyTo: replyTo,
···495495496496func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
497497 l := rp.logger.With("handler", "IssueComment")
498498- user := rp.oauth.GetUser(r)
498498+ user := rp.oauth.GetMultiAccountUser(r)
499499500500 issue, ok := r.Context().Value("issue").(*models.Issue)
501501 if !ok {
···531531532532func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) {
533533 l := rp.logger.With("handler", "EditIssueComment")
534534- user := rp.oauth.GetUser(r)
534534+ user := rp.oauth.GetMultiAccountUser(r)
535535536536 issue, ok := r.Context().Value("issue").(*models.Issue)
537537 if !ok {
···557557 }
558558 comment := comments[0]
559559560560- if comment.Did != user.Did {
561561- l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did)
560560+ if comment.Did != user.Active.Did {
561561+ l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Active.Did)
562562 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
563563 return
564564 }
···608608 // rkey is optional, it was introduced later
609609 if newComment.Rkey != "" {
610610 // update the record on pds
611611- ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey)
611611+ ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Active.Did, comment.Rkey)
612612 if err != nil {
613613 l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey)
614614 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
···617617618618 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
619619 Collection: tangled.RepoIssueCommentNSID,
620620- Repo: user.Did,
620620+ Repo: user.Active.Did,
621621 Rkey: newComment.Rkey,
622622 SwapRecord: ex.Cid,
623623 Record: &lexutil.LexiconTypeDecoder{
···641641642642func (rp *Issues) ReplyIssueCommentPlaceholder(w http.ResponseWriter, r *http.Request) {
643643 l := rp.logger.With("handler", "ReplyIssueCommentPlaceholder")
644644- user := rp.oauth.GetUser(r)
644644+ user := rp.oauth.GetMultiAccountUser(r)
645645646646 issue, ok := r.Context().Value("issue").(*models.Issue)
647647 if !ok {
···677677678678func (rp *Issues) ReplyIssueComment(w http.ResponseWriter, r *http.Request) {
679679 l := rp.logger.With("handler", "ReplyIssueComment")
680680- user := rp.oauth.GetUser(r)
680680+ user := rp.oauth.GetMultiAccountUser(r)
681681682682 issue, ok := r.Context().Value("issue").(*models.Issue)
683683 if !ok {
···713713714714func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
715715 l := rp.logger.With("handler", "DeleteIssueComment")
716716- user := rp.oauth.GetUser(r)
716716+ user := rp.oauth.GetMultiAccountUser(r)
717717718718 issue, ok := r.Context().Value("issue").(*models.Issue)
719719 if !ok {
···739739 }
740740 comment := comments[0]
741741742742- if comment.Did != user.Did {
743743- l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did)
742742+ if comment.Did != user.Active.Did {
743743+ l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Active.Did)
744744 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
745745 return
746746 }
···769769 }
770770 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
771771 Collection: tangled.RepoIssueCommentNSID,
772772- Repo: user.Did,
772772+ Repo: user.Active.Did,
773773 Rkey: comment.Rkey,
774774 })
775775 if err != nil {
···807807808808 page := pagination.FromContext(r.Context())
809809810810- user := rp.oauth.GetUser(r)
810810+ user := rp.oauth.GetMultiAccountUser(r)
811811 f, err := rp.repoResolver.Resolve(r)
812812 if err != nil {
813813 l.Error("failed to get repo and knot", "err", err)
···884884 }
885885886886 rp.pages.RepoIssues(w, pages.RepoIssuesParams{
887887- LoggedInUser: rp.oauth.GetUser(r),
887887+ LoggedInUser: rp.oauth.GetMultiAccountUser(r),
888888 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
889889 Issues: issues,
890890 IssueCount: totalIssues,
···897897898898func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) {
899899 l := rp.logger.With("handler", "NewIssue")
900900- user := rp.oauth.GetUser(r)
900900+ user := rp.oauth.GetMultiAccountUser(r)
901901902902 f, err := rp.repoResolver.Resolve(r)
903903 if err != nil {
···921921 Title: r.FormValue("title"),
922922 Body: body,
923923 Open: true,
924924- Did: user.Did,
924924+ Did: user.Active.Did,
925925 Created: time.Now(),
926926 Mentions: mentions,
927927 References: references,
···945945 }
946946 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
947947 Collection: tangled.RepoIssueNSID,
948948- Repo: user.Did,
948948+ Repo: user.Active.Did,
949949 Rkey: issue.Rkey,
950950 Record: &lexutil.LexiconTypeDecoder{
951951 Val: &record,
+2-2
appview/issues/opengraph.go
···193193 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2)
194194 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25
195195 dollyColor := color.RGBA{180, 180, 180, 255} // light gray
196196- err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor)
196196+ err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor)
197197 if err != nil {
198198- log.Printf("dolly silhouette not available (this is ok): %v", err)
198198+ log.Printf("dolly not available (this is ok): %v", err)
199199 }
200200201201 // Draw "opened by @author" and date at the bottom with more spacing
+31-36
appview/knots/knots.go
···7070}
71717272func (k *Knots) knots(w http.ResponseWriter, r *http.Request) {
7373- user := k.OAuth.GetUser(r)
7373+ user := k.OAuth.GetMultiAccountUser(r)
7474 registrations, err := db.GetRegistrations(
7575 k.Db,
7676- orm.FilterEq("did", user.Did),
7676+ orm.FilterEq("did", user.Active.Did),
7777 )
7878 if err != nil {
7979 k.Logger.Error("failed to fetch knot registrations", "err", err)
···9292func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) {
9393 l := k.Logger.With("handler", "dashboard")
94949595- user := k.OAuth.GetUser(r)
9696- l = l.With("user", user.Did)
9595+ user := k.OAuth.GetMultiAccountUser(r)
9696+ l = l.With("user", user.Active.Did)
97979898 domain := chi.URLParam(r, "domain")
9999 if domain == "" {
···103103104104 registrations, err := db.GetRegistrations(
105105 k.Db,
106106- orm.FilterEq("did", user.Did),
106106+ orm.FilterEq("did", user.Active.Did),
107107 orm.FilterEq("domain", domain),
108108 )
109109 if err != nil {
···154154}
155155156156func (k *Knots) register(w http.ResponseWriter, r *http.Request) {
157157- user := k.OAuth.GetUser(r)
157157+ user := k.OAuth.GetMultiAccountUser(r)
158158 l := k.Logger.With("handler", "register")
159159160160 noticeId := "register-error"
···175175 return
176176 }
177177 l = l.With("domain", domain)
178178- l = l.With("user", user.Did)
178178+ l = l.With("user", user.Active.Did)
179179180180 tx, err := k.Db.Begin()
181181 if err != nil {
···188188 k.Enforcer.E.LoadPolicy()
189189 }()
190190191191- err = db.AddKnot(tx, domain, user.Did)
191191+ err = db.AddKnot(tx, domain, user.Active.Did)
192192 if err != nil {
193193 l.Error("failed to insert", "err", err)
194194 fail()
···210210 return
211211 }
212212213213- ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Did, domain)
213213+ ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Active.Did, domain)
214214 var exCid *string
215215 if ex != nil {
216216 exCid = ex.Cid
···219219 // re-announce by registering under same rkey
220220 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
221221 Collection: tangled.KnotNSID,
222222- Repo: user.Did,
222222+ Repo: user.Active.Did,
223223 Rkey: domain,
224224 Record: &lexutil.LexiconTypeDecoder{
225225 Val: &tangled.Knot{
···250250 }
251251252252 // begin verification
253253- err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev)
253253+ err = serververify.RunVerification(r.Context(), domain, user.Active.Did, k.Config.Core.Dev)
254254 if err != nil {
255255 l.Error("verification failed", "err", err)
256256 k.Pages.HxRefresh(w)
257257 return
258258 }
259259260260- err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did)
260260+ err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Active.Did)
261261 if err != nil {
262262 l.Error("failed to mark verified", "err", err)
263263 k.Pages.HxRefresh(w)
···275275}
276276277277func (k *Knots) delete(w http.ResponseWriter, r *http.Request) {
278278- user := k.OAuth.GetUser(r)
278278+ user := k.OAuth.GetMultiAccountUser(r)
279279 l := k.Logger.With("handler", "delete")
280280281281 noticeId := "operation-error"
···294294 // get record from db first
295295 registrations, err := db.GetRegistrations(
296296 k.Db,
297297- orm.FilterEq("did", user.Did),
297297+ orm.FilterEq("did", user.Active.Did),
298298 orm.FilterEq("domain", domain),
299299 )
300300 if err != nil {
···322322323323 err = db.DeleteKnot(
324324 tx,
325325- orm.FilterEq("did", user.Did),
325325+ orm.FilterEq("did", user.Active.Did),
326326 orm.FilterEq("domain", domain),
327327 )
328328 if err != nil {
···350350351351 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
352352 Collection: tangled.KnotNSID,
353353- Repo: user.Did,
353353+ Repo: user.Active.Did,
354354 Rkey: domain,
355355 })
356356 if err != nil {
···382382}
383383384384func (k *Knots) retry(w http.ResponseWriter, r *http.Request) {
385385- user := k.OAuth.GetUser(r)
385385+ user := k.OAuth.GetMultiAccountUser(r)
386386 l := k.Logger.With("handler", "retry")
387387388388 noticeId := "operation-error"
···398398 return
399399 }
400400 l = l.With("domain", domain)
401401- l = l.With("user", user.Did)
401401+ l = l.With("user", user.Active.Did)
402402403403 // get record from db first
404404 registrations, err := db.GetRegistrations(
405405 k.Db,
406406- orm.FilterEq("did", user.Did),
406406+ orm.FilterEq("did", user.Active.Did),
407407 orm.FilterEq("domain", domain),
408408 )
409409 if err != nil {
···419419 registration := registrations[0]
420420421421 // begin verification
422422- err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev)
422422+ err = serververify.RunVerification(r.Context(), domain, user.Active.Did, k.Config.Core.Dev)
423423 if err != nil {
424424 l.Error("verification failed", "err", err)
425425···437437 return
438438 }
439439440440- err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did)
440440+ err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Active.Did)
441441 if err != nil {
442442 l.Error("failed to mark verified", "err", err)
443443 k.Pages.Notice(w, noticeId, err.Error())
···456456 return
457457 }
458458459459- ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Did, domain)
459459+ ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Active.Did, domain)
460460 var exCid *string
461461 if ex != nil {
462462 exCid = ex.Cid
···465465 // ignore the error here
466466 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
467467 Collection: tangled.KnotNSID,
468468- Repo: user.Did,
468468+ Repo: user.Active.Did,
469469 Rkey: domain,
470470 Record: &lexutil.LexiconTypeDecoder{
471471 Val: &tangled.Knot{
···494494 // Get updated registration to show
495495 registrations, err = db.GetRegistrations(
496496 k.Db,
497497- orm.FilterEq("did", user.Did),
497497+ orm.FilterEq("did", user.Active.Did),
498498 orm.FilterEq("domain", domain),
499499 )
500500 if err != nil {
···516516}
517517518518func (k *Knots) addMember(w http.ResponseWriter, r *http.Request) {
519519- user := k.OAuth.GetUser(r)
519519+ user := k.OAuth.GetMultiAccountUser(r)
520520 l := k.Logger.With("handler", "addMember")
521521522522 domain := chi.URLParam(r, "domain")
···526526 return
527527 }
528528 l = l.With("domain", domain)
529529- l = l.With("user", user.Did)
529529+ l = l.With("user", user.Active.Did)
530530531531 registrations, err := db.GetRegistrations(
532532 k.Db,
533533- orm.FilterEq("did", user.Did),
533533+ orm.FilterEq("did", user.Active.Did),
534534 orm.FilterEq("domain", domain),
535535 orm.FilterIsNot("registered", "null"),
536536 )
···583583584584 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
585585 Collection: tangled.KnotMemberNSID,
586586- Repo: user.Did,
586586+ Repo: user.Active.Did,
587587 Rkey: rkey,
588588 Record: &lexutil.LexiconTypeDecoder{
589589 Val: &tangled.KnotMember{
···618618}
619619620620func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) {
621621- user := k.OAuth.GetUser(r)
621621+ user := k.OAuth.GetMultiAccountUser(r)
622622 l := k.Logger.With("handler", "removeMember")
623623624624 noticeId := "operation-error"
···634634 return
635635 }
636636 l = l.With("domain", domain)
637637- l = l.With("user", user.Did)
637637+ l = l.With("user", user.Active.Did)
638638639639 registrations, err := db.GetRegistrations(
640640 k.Db,
641641- orm.FilterEq("did", user.Did),
641641+ orm.FilterEq("did", user.Active.Did),
642642 orm.FilterEq("domain", domain),
643643 orm.FilterIsNot("registered", "null"),
644644 )
···663663 memberId, err := k.IdResolver.ResolveIdent(r.Context(), member)
664664 if err != nil {
665665 l.Error("failed to resolve member identity to handle", "err", err)
666666- k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
667667- return
668668- }
669669- if memberId.Handle.IsInvalidHandle() {
670670- l.Error("failed to resolve member identity to handle")
671666 k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
672667 return
673668 }
+2-2
appview/labels/labels.go
···6868// - this handler should calculate the diff in order to create the labelop record
6969// - we need the diff in order to maintain a "history" of operations performed by users
7070func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) {
7171- user := l.oauth.GetUser(r)
7171+ user := l.oauth.GetMultiAccountUser(r)
72727373 noticeId := "add-label-error"
7474···8282 return
8383 }
84848585- did := user.Did
8585+ did := user.Active.Did
8686 rkey := tid.TID()
8787 performedAt := time.Now()
8888 indexedAt := time.Now()
+10-8
appview/middleware/middleware.go
···115115 return func(next http.Handler) http.Handler {
116116 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
117117 // requires auth also
118118- actor := mw.oauth.GetUser(r)
118118+ actor := mw.oauth.GetMultiAccountUser(r)
119119 if actor == nil {
120120 // we need a logged in user
121121 log.Printf("not logged in, redirecting")
···128128 return
129129 }
130130131131- ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Did, group, domain)
131131+ ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Active.Did, group, domain)
132132 if err != nil || !ok {
133133- // we need a logged in user
134134- log.Printf("%s does not have perms of a %s in domain %s", actor.Did, group, domain)
133133+ log.Printf("%s does not have perms of a %s in domain %s", actor.Active.Did, group, domain)
135134 http.Error(w, "Forbiden", http.StatusUnauthorized)
136135 return
137136 }
···149148 return func(next http.Handler) http.Handler {
150149 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151150 // requires auth also
152152- actor := mw.oauth.GetUser(r)
151151+ actor := mw.oauth.GetMultiAccountUser(r)
153152 if actor == nil {
154153 // we need a logged in user
155154 log.Printf("not logged in, redirecting")
···162161 return
163162 }
164163165165- ok, err := mw.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
164164+ ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
166165 if err != nil || !ok {
167167- // we need a logged in user
168168- log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.DidSlashRepo())
166166+ log.Printf("%s does not have perms of a %s in repo %s", actor.Active.Did, requiredPerm, f.DidSlashRepo())
169167 http.Error(w, "Forbiden", http.StatusUnauthorized)
170168 return
171169 }
···223221 )
224222 if err != nil {
225223 log.Println("failed to resolve repo", "err", err)
224224+ w.WriteHeader(http.StatusNotFound)
226225 mw.pages.ErrorKnot404(w)
227226 return
228227 }
···240239 f, err := mw.repoResolver.Resolve(r)
241240 if err != nil {
242241 log.Println("failed to fully resolve repo", err)
242242+ w.WriteHeader(http.StatusNotFound)
243243 mw.pages.ErrorKnot404(w)
244244 return
245245 }
···288288 f, err := mw.repoResolver.Resolve(r)
289289 if err != nil {
290290 log.Println("failed to fully resolve repo", err)
291291+ w.WriteHeader(http.StatusNotFound)
291292 mw.pages.ErrorKnot404(w)
292293 return
293294 }
···324325 f, err := mw.repoResolver.Resolve(r)
325326 if err != nil {
326327 log.Println("failed to fully resolve repo", err)
328328+ w.WriteHeader(http.StatusNotFound)
327329 mw.pages.ErrorKnot404(w)
328330 return
329331 }
+38
appview/models/pipeline.go
···33import (
44 "fmt"
55 "slices"
66+ "strings"
67 "time"
7889 "github.com/bluesky-social/indigo/atproto/syntax"
···5657 }
57585859 return 0
6060+}
6161+6262+// produces short summary of successes:
6363+// - "0/4" when zero successes of 4 workflows
6464+// - "4/4" when all successes of 4 workflows
6565+// - "0/0" when no workflows run in this pipeline
6666+func (p Pipeline) ShortStatusSummary() string {
6767+ counts := make(map[spindle.StatusKind]int)
6868+ for _, w := range p.Statuses {
6969+ counts[w.Latest().Status] += 1
7070+ }
7171+7272+ total := len(p.Statuses)
7373+ successes := counts[spindle.StatusKindSuccess]
7474+7575+ return fmt.Sprintf("%d/%d", successes, total)
7676+}
7777+7878+// produces a string of the form "3/4 success, 2/4 failed, 1/4 pending"
7979+func (p Pipeline) LongStatusSummary() string {
8080+ counts := make(map[spindle.StatusKind]int)
8181+ for _, w := range p.Statuses {
8282+ counts[w.Latest().Status] += 1
8383+ }
8484+8585+ total := len(p.Statuses)
8686+8787+ var result []string
8888+ // finish states first, followed by start states
8989+ states := append(spindle.FinishStates[:], spindle.StartStates[:]...)
9090+ for _, state := range states {
9191+ if count, ok := counts[state]; ok {
9292+ result = append(result, fmt.Sprintf("%d/%d %s", count, total, state.String()))
9393+ }
9494+ }
9595+9696+ return strings.Join(result, ", ")
5997}
60986199func (p Pipeline) Counts() map[string]int {
+8-18
appview/models/pull.go
···8383 Repo *Repo
8484}
85858686+// NOTE: This method does not include patch blob in returned atproto record
8687func (p Pull) AsRecord() tangled.RepoPull {
8788 var source *tangled.RepoPull_Source
8889 if p.PullSource != nil {
···113114 Repo: p.RepoAt.String(),
114115 Branch: p.TargetBranch,
115116 },
116116- Patch: p.LatestPatch(),
117117 Source: source,
118118 }
119119 return record
···171171 return syntax.ATURI(p.CommentAt)
172172}
173173174174-// func (p *PullComment) AsRecord() tangled.RepoPullComment {
175175-// mentions := make([]string, len(p.Mentions))
176176-// for i, did := range p.Mentions {
177177-// mentions[i] = string(did)
178178-// }
179179-// references := make([]string, len(p.References))
180180-// for i, uri := range p.References {
181181-// references[i] = string(uri)
182182-// }
183183-// return tangled.RepoPullComment{
184184-// Pull: p.PullAt,
185185-// Body: p.Body,
186186-// Mentions: mentions,
187187-// References: references,
188188-// CreatedAt: p.Created.Format(time.RFC3339),
189189-// }
190190-// }
174174+func (p *Pull) TotalComments() int {
175175+ total := 0
176176+ for _, s := range p.Submissions {
177177+ total += len(s.Comments)
178178+ }
179179+ return total
180180+}
191181192182func (p *Pull) LastRoundNumber() int {
193183 return len(p.Submissions) - 1
+7-6
appview/notifications/notifications.go
···48484949func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) {
5050 l := n.logger.With("handler", "notificationsPage")
5151- user := n.oauth.GetUser(r)
5151+ user := n.oauth.GetMultiAccountUser(r)
52525353 page := pagination.FromContext(r.Context())
54545555 total, err := db.CountNotifications(
5656 n.db,
5757- orm.FilterEq("recipient_did", user.Did),
5757+ orm.FilterEq("recipient_did", user.Active.Did),
5858 )
5959 if err != nil {
6060 l.Error("failed to get total notifications", "err", err)
···6565 notifications, err := db.GetNotificationsWithEntities(
6666 n.db,
6767 page,
6868- orm.FilterEq("recipient_did", user.Did),
6868+ orm.FilterEq("recipient_did", user.Active.Did),
6969 )
7070 if err != nil {
7171 l.Error("failed to get notifications", "err", err)
···7373 return
7474 }
75757676- err = db.MarkAllNotificationsRead(n.db, user.Did)
7676+ err = db.MarkAllNotificationsRead(n.db, user.Active.Did)
7777 if err != nil {
7878 l.Error("failed to mark notifications as read", "err", err)
7979 }
···9090}
91919292func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) {
9393- user := n.oauth.GetUser(r)
9393+ user := n.oauth.GetMultiAccountUser(r)
9494 if user == nil {
9595+ http.Error(w, "Forbidden", http.StatusUnauthorized)
9596 return
9697 }
97989899 count, err := db.CountNotifications(
99100 n.db,
100100- orm.FilterEq("recipient_did", user.Did),
101101+ orm.FilterEq("recipient_did", user.Active.Did),
101102 orm.FilterEq("read", 0),
102103 )
103104 if err != nil {
···22title: Tangled docs
33author: The Tangled Contributors
44date: 21 Sun, Dec 2025
55----
66-77-# Introduction
88-99-Tangled is a decentralized code hosting and collaboration
1010-platform. Every component of Tangled is open-source and
1111-self-hostable. [tangled.org](https://tangled.org) also
1212-provides hosting and CI services that are free to use.
55+abstract: |
66+ Tangled is a decentralized code hosting and collaboration
77+ platform. Every component of Tangled is open-source and
88+ self-hostable. [tangled.org](https://tangled.org) also
99+ provides hosting and CI services that are free to use.
13101414-There are several models for decentralized code
1515-collaboration platforms, ranging from ActivityPubโs
1616-(Forgejo) federated model, to Radicleโs entirely P2P model.
1717-Our approach attempts to be the best of both worlds by
1818-adopting the AT Protocolโa protocol for building decentralized
1919-social applications with a central identity
1111+ There are several models for decentralized code
1212+ collaboration platforms, ranging from ActivityPubโs
1313+ (Forgejo) federated model, to Radicleโs entirely P2P model.
1414+ Our approach attempts to be the best of both worlds by
1515+ adopting the AT Protocolโa protocol for building decentralized
1616+ social applications with a central identity
20172121-Our approach to this is the idea of โknotsโ. Knots are
2222-lightweight, headless servers that enable users to host Git
2323-repositories with ease. Knots are designed for either single
2424-or multi-tenant use which is perfect for self-hosting on a
2525-Raspberry Pi at home, or larger โcommunityโ servers. By
2626-default, Tangled provides managed knots where you can host
2727-your repositories for free.
1818+ Our approach to this is the idea of โknotsโ. Knots are
1919+ lightweight, headless servers that enable users to host Git
2020+ repositories with ease. Knots are designed for either single
2121+ or multi-tenant use which is perfect for self-hosting on a
2222+ Raspberry Pi at home, or larger โcommunityโ servers. By
2323+ default, Tangled provides managed knots where you can host
2424+ your repositories for free.
28252929-The appview at tangled.org acts as a consolidated "view"
3030-into the whole network, allowing users to access, clone and
3131-contribute to repositories hosted across different knots
3232-seamlessly.
2626+ The appview at tangled.org acts as a consolidated "view"
2727+ into the whole network, allowing users to access, clone and
2828+ contribute to repositories hosted across different knots
2929+ seamlessly.
3030+---
33313432# Quick start guide
3533···665663 nixpkgs:
666664 - nodejs
667665 - go
666666+ # unstable
667667+ nixpkgs/nixpkgs-unstable:
668668+ - bun
668669 # custom registry
669670 git+https://tangled.org/@example.com/my_pkg:
670671 - my_pkg
···11package types
2233import (
44+ "net/url"
55+46 "github.com/bluekeyes/go-gitdiff/gitdiff"
77+ "tangled.org/core/appview/filetree"
58)
69710type DiffOpts struct {
811 Split bool `json:"split"`
912}
10131111-type TextFragment struct {
1212- Header string `json:"comment"`
1313- Lines []gitdiff.Line `json:"lines"`
1414+func (d DiffOpts) Encode() string {
1515+ values := make(url.Values)
1616+ if d.Split {
1717+ values.Set("diff", "split")
1818+ } else {
1919+ values.Set("diff", "unified")
2020+ }
2121+ return values.Encode()
2222+}
2323+2424+// A nicer git diff representation.
2525+type NiceDiff struct {
2626+ Commit Commit `json:"commit"`
2727+ Stat DiffStat `json:"stat"`
2828+ Diff []Diff `json:"diff"`
1429}
15301631type Diff struct {
···2641 IsRename bool `json:"is_rename"`
2742}
28432929-type DiffStat struct {
3030- Insertions int64
3131- Deletions int64
3232-}
3333-3434-func (d *Diff) Stats() DiffStat {
3535- var stats DiffStat
4444+func (d Diff) Stats() DiffFileStat {
4545+ var stats DiffFileStat
3646 for _, f := range d.TextFragments {
3747 stats.Insertions += f.LinesAdded
3848 stats.Deletions += f.LinesDeleted
···4050 return stats
4151}
42524343-// A nicer git diff representation.
4444-type NiceDiff struct {
4545- Commit Commit `json:"commit"`
4646- Stat struct {
4747- FilesChanged int `json:"files_changed"`
4848- Insertions int `json:"insertions"`
4949- Deletions int `json:"deletions"`
5050- } `json:"stat"`
5151- Diff []Diff `json:"diff"`
5353+type DiffStat struct {
5454+ Insertions int64 `json:"insertions"`
5555+ Deletions int64 `json:"deletions"`
5656+ FilesChanged int `json:"files_changed"`
5757+}
5858+5959+type DiffFileStat struct {
6060+ Insertions int64
6161+ Deletions int64
5262}
53635464type DiffTree struct {
···5868 Diff []*gitdiff.File `json:"diff"`
5969}
60706161-func (d *NiceDiff) ChangedFiles() []string {
6262- files := make([]string, len(d.Diff))
7171+type DiffFileName struct {
7272+ Old string
7373+ New string
7474+}
7575+7676+func (d NiceDiff) ChangedFiles() []DiffFileRenderer {
7777+ drs := make([]DiffFileRenderer, len(d.Diff))
7878+ for i, s := range d.Diff {
7979+ drs[i] = s
8080+ }
8181+ return drs
8282+}
63836464- for i, f := range d.Diff {
6565- if f.IsDelete {
6666- files[i] = f.Name.Old
8484+func (d NiceDiff) FileTree() *filetree.FileTreeNode {
8585+ fs := make([]string, len(d.Diff))
8686+ for i, s := range d.Diff {
8787+ n := s.Names()
8888+ if n.New == "" {
8989+ fs[i] = n.Old
6790 } else {
6868- files[i] = f.Name.New
9191+ fs[i] = n.New
6992 }
7093 }
9494+ return filetree.FileTree(fs)
9595+}
71967272- return files
9797+func (d NiceDiff) Stats() DiffStat {
9898+ return d.Stat
7399}
741007575-// used by html elements as a unique ID for hrefs
7676-func (d *Diff) Id() string {
101101+func (d Diff) Id() string {
102102+ if d.IsDelete {
103103+ return d.Name.Old
104104+ }
77105 return d.Name.New
78106}
791078080-func (d *Diff) Split() *SplitDiff {
108108+func (d Diff) Names() DiffFileName {
109109+ var n DiffFileName
110110+ if d.IsDelete {
111111+ n.Old = d.Name.Old
112112+ return n
113113+ } else if d.IsCopy || d.IsRename {
114114+ n.Old = d.Name.Old
115115+ n.New = d.Name.New
116116+ return n
117117+ } else {
118118+ n.New = d.Name.New
119119+ return n
120120+ }
121121+}
122122+123123+func (d Diff) CanRender() string {
124124+ if d.IsBinary {
125125+ return "This is a binary file and will not be displayed."
126126+ }
127127+128128+ return ""
129129+}
130130+131131+func (d Diff) Split() SplitDiff {
81132 fragments := make([]SplitFragment, len(d.TextFragments))
82133 for i, fragment := range d.TextFragments {
83134 leftLines, rightLines := SeparateLines(&fragment)
···88139 }
89140 }
901419191- return &SplitDiff{
142142+ return SplitDiff{
92143 Name: d.Id(),
93144 TextFragments: fragments,
94145 }
+31
types/diff_renderer.go
···11+package types
22+33+import "tangled.org/core/appview/filetree"
44+55+type DiffRenderer interface {
66+ // list of file affected by these diffs
77+ ChangedFiles() []DiffFileRenderer
88+99+ // filetree
1010+ FileTree() *filetree.FileTreeNode
1111+1212+ Stats() DiffStat
1313+}
1414+1515+type DiffFileRenderer interface {
1616+ // html ID for each file in the diff
1717+ Id() string
1818+1919+ // produce a splitdiff
2020+ Split() SplitDiff
2121+2222+ // stats for this single file
2323+ Stats() DiffFileStat
2424+2525+ // old and new name of file
2626+ Names() DiffFileName
2727+2828+ // whether this diff can be displayed,
2929+ // returns a reason if not, and the empty string if it can
3030+ CanRender() string
3131+}
···2222 TextFragments []SplitFragment `json:"fragments"`
2323}
24242525-// used by html elements as a unique ID for hrefs
2626-func (d *SplitDiff) Id() string {
2525+func (d SplitDiff) Id() string {
2726 return d.Name
2827}
2928