forked from tangled.org/core
this repo has no description

Compare changes

Choose any two refs to compare.

+2 -122
api/tangled/cbor_gen.go
··· 5512 5512 } 5513 5513 5514 5514 cw := cbg.NewCborWriter(w) 5515 - fieldCount := 7 5515 + fieldCount := 6 5516 5516 5517 5517 if t.Body == nil { 5518 5518 fieldCount-- ··· 5642 5642 return err 5643 5643 } 5644 5644 5645 - // t.IssueId (int64) (int64) 5646 - if len("issueId") > 1000000 { 5647 - return xerrors.Errorf("Value in field \"issueId\" was too long") 5648 - } 5649 - 5650 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("issueId"))); err != nil { 5651 - return err 5652 - } 5653 - if _, err := cw.WriteString(string("issueId")); err != nil { 5654 - return err 5655 - } 5656 - 5657 - if t.IssueId >= 0 { 5658 - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.IssueId)); err != nil { 5659 - return err 5660 - } 5661 - } else { 5662 - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.IssueId-1)); err != nil { 5663 - return err 5664 - } 5665 - } 5666 - 5667 5645 // t.CreatedAt (string) (string) 5668 5646 if len("createdAt") > 1000000 { 5669 5647 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 5795 5773 5796 5774 t.Title = string(sval) 5797 5775 } 5798 - // t.IssueId (int64) (int64) 5799 - case "issueId": 5800 - { 5801 - maj, extra, err := cr.ReadHeader() 5802 - if err != nil { 5803 - return err 5804 - } 5805 - var extraI int64 5806 - switch maj { 5807 - case cbg.MajUnsignedInt: 5808 - extraI = int64(extra) 5809 - if extraI < 0 { 5810 - return fmt.Errorf("int64 positive overflow") 5811 - } 5812 - case cbg.MajNegativeInt: 5813 - extraI = int64(extra) 5814 - if extraI < 0 { 5815 - return fmt.Errorf("int64 negative overflow") 5816 - } 5817 - extraI = -1 - extraI 5818 - default: 5819 - return fmt.Errorf("wrong type for int64 field: %d", maj) 5820 - } 5821 - 5822 - t.IssueId = int64(extraI) 5823 - } 5824 5776 // t.CreatedAt (string) (string) 5825 5777 case "createdAt": 5826 5778 ··· 5850 5802 } 5851 5803 5852 5804 cw := cbg.NewCborWriter(w) 5853 - fieldCount := 7 5854 - 5855 - if t.CommentId == nil { 5856 - fieldCount-- 5857 - } 5805 + fieldCount := 6 5858 5806 5859 5807 if t.Owner == nil { 5860 5808 fieldCount-- ··· 5997 5945 } 5998 5946 } 5999 5947 6000 - // t.CommentId (int64) (int64) 6001 - if t.CommentId != nil { 6002 - 6003 - if len("commentId") > 1000000 { 6004 - return xerrors.Errorf("Value in field \"commentId\" was too long") 6005 - } 6006 - 6007 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("commentId"))); err != nil { 6008 - return err 6009 - } 6010 - if _, err := cw.WriteString(string("commentId")); err != nil { 6011 - return err 6012 - } 6013 - 6014 - if t.CommentId == nil { 6015 - if _, err := cw.Write(cbg.CborNull); err != nil { 6016 - return err 6017 - } 6018 - } else { 6019 - if *t.CommentId >= 0 { 6020 - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.CommentId)); err != nil { 6021 - return err 6022 - } 6023 - } else { 6024 - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-*t.CommentId-1)); err != nil { 6025 - return err 6026 - } 6027 - } 6028 - } 6029 - 6030 - } 6031 - 6032 5948 // t.CreatedAt (string) (string) 6033 5949 if len("createdAt") > 1000000 { 6034 5950 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 6168 6084 } 6169 6085 6170 6086 t.Owner = (*string)(&sval) 6171 - } 6172 - } 6173 - // t.CommentId (int64) (int64) 6174 - case "commentId": 6175 - { 6176 - 6177 - b, err := cr.ReadByte() 6178 - if err != nil { 6179 - return err 6180 - } 6181 - if b != cbg.CborNull[0] { 6182 - if err := cr.UnreadByte(); err != nil { 6183 - return err 6184 - } 6185 - maj, extra, err := cr.ReadHeader() 6186 - if err != nil { 6187 - return err 6188 - } 6189 - var extraI int64 6190 - switch maj { 6191 - case cbg.MajUnsignedInt: 6192 - extraI = int64(extra) 6193 - if extraI < 0 { 6194 - return fmt.Errorf("int64 positive overflow") 6195 - } 6196 - case cbg.MajNegativeInt: 6197 - extraI = int64(extra) 6198 - if extraI < 0 { 6199 - return fmt.Errorf("int64 negative overflow") 6200 - } 6201 - extraI = -1 - extraI 6202 - default: 6203 - return fmt.Errorf("wrong type for int64 field: %d", maj) 6204 - } 6205 - 6206 - t.CommentId = (*int64)(&extraI) 6207 6087 } 6208 6088 } 6209 6089 // t.CreatedAt (string) (string)
-1
api/tangled/issuecomment.go
··· 19 19 type RepoIssueComment struct { 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue.comment" cborgen:"$type,const=sh.tangled.repo.issue.comment"` 21 21 Body string `json:"body" cborgen:"body"` 22 - CommentId *int64 `json:"commentId,omitempty" cborgen:"commentId,omitempty"` 23 22 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 24 23 Issue string `json:"issue" cborgen:"issue"` 25 24 Owner *string `json:"owner,omitempty" cborgen:"owner,omitempty"`
-1
api/tangled/repoissue.go
··· 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue" cborgen:"$type,const=sh.tangled.repo.issue"` 21 21 Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 22 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 - IssueId int64 `json:"issueId" cborgen:"issueId"` 24 23 Owner string `json:"owner" cborgen:"owner"` 25 24 Repo string `json:"repo" cborgen:"repo"` 26 25 Title string `json:"title" cborgen:"title"`
+41 -144
appview/db/follow.go
··· 1 1 package db 2 2 3 3 import ( 4 - "fmt" 5 4 "log" 6 - "strings" 7 5 "time" 8 6 ) 9 7 ··· 55 53 return err 56 54 } 57 55 58 - type FollowStats struct { 59 - Followers int 60 - Following int 61 - } 62 - 63 - func GetFollowerFollowingCount(e Execer, did string) (FollowStats, error) { 56 + func GetFollowerFollowingCount(e Execer, did string) (int, int, error) { 64 57 followers, following := 0, 0 65 58 err := e.QueryRow( 66 - `SELECT 59 + `SELECT 67 60 COUNT(CASE WHEN subject_did = ? THEN 1 END) AS followers, 68 61 COUNT(CASE WHEN user_did = ? THEN 1 END) AS following 69 62 FROM follows;`, did, did).Scan(&followers, &following) 70 63 if err != nil { 71 - return FollowStats{}, err 64 + return 0, 0, err 72 65 } 73 - return FollowStats{ 74 - Followers: followers, 75 - Following: following, 76 - }, nil 66 + return followers, following, nil 77 67 } 78 68 79 - func GetFollowerFollowingCounts(e Execer, dids []string) (map[string]FollowStats, error) { 80 - if len(dids) == 0 { 81 - return nil, nil 82 - } 69 + type FollowStatus int 83 70 84 - placeholders := make([]string, len(dids)) 85 - for i := range placeholders { 86 - placeholders[i] = "?" 87 - } 88 - placeholderStr := strings.Join(placeholders, ",") 71 + const ( 72 + IsNotFollowing FollowStatus = iota 73 + IsFollowing 74 + IsSelf 75 + ) 89 76 90 - args := make([]any, len(dids)*2) 91 - for i, did := range dids { 92 - args[i] = did 93 - args[i+len(dids)] = did 77 + func (s FollowStatus) String() string { 78 + switch s { 79 + case IsNotFollowing: 80 + return "IsNotFollowing" 81 + case IsFollowing: 82 + return "IsFollowing" 83 + case IsSelf: 84 + return "IsSelf" 85 + default: 86 + return "IsNotFollowing" 94 87 } 95 - 96 - query := fmt.Sprintf(` 97 - select 98 - coalesce(f.did, g.did) as did, 99 - coalesce(f.followers, 0) as followers, 100 - coalesce(g.following, 0) as following 101 - from ( 102 - select subject_did as did, count(*) as followers 103 - from follows 104 - where subject_did in (%s) 105 - group by subject_did 106 - ) f 107 - full outer join ( 108 - select user_did as did, count(*) as following 109 - from follows 110 - where user_did in (%s) 111 - group by user_did 112 - ) g on f.did = g.did`, 113 - placeholderStr, placeholderStr) 114 - 115 - result := make(map[string]FollowStats) 116 - 117 - rows, err := e.Query(query, args...) 118 - if err != nil { 119 - return nil, err 120 - } 121 - defer rows.Close() 88 + } 122 89 123 - for rows.Next() { 124 - var did string 125 - var followers, following int 126 - if err := rows.Scan(&did, &followers, &following); err != nil { 127 - return nil, err 128 - } 129 - result[did] = FollowStats{ 130 - Followers: followers, 131 - Following: following, 132 - } 90 + func GetFollowStatus(e Execer, userDid, subjectDid string) FollowStatus { 91 + if userDid == subjectDid { 92 + return IsSelf 93 + } else if _, err := GetFollow(e, userDid, subjectDid); err != nil { 94 + return IsNotFollowing 95 + } else { 96 + return IsFollowing 133 97 } 134 - 135 - for _, did := range dids { 136 - if _, exists := result[did]; !exists { 137 - result[did] = FollowStats{ 138 - Followers: 0, 139 - Following: 0, 140 - } 141 - } 142 - } 143 - 144 - return result, nil 145 98 } 146 99 147 - func GetFollows(e Execer, limit int, filters ...filter) ([]Follow, error) { 100 + func GetAllFollows(e Execer, limit int) ([]Follow, error) { 148 101 var follows []Follow 149 102 150 - var conditions []string 151 - var args []any 152 - for _, filter := range filters { 153 - conditions = append(conditions, filter.Condition()) 154 - args = append(args, filter.Arg()...) 155 - } 156 - 157 - whereClause := "" 158 - if conditions != nil { 159 - whereClause = " where " + strings.Join(conditions, " and ") 160 - } 161 - limitClause := "" 162 - if limit > 0 { 163 - limitClause = " limit ?" 164 - args = append(args, limit) 165 - } 166 - 167 - query := fmt.Sprintf( 168 - `select user_did, subject_did, followed_at, rkey 103 + rows, err := e.Query(` 104 + select user_did, subject_did, followed_at, rkey 169 105 from follows 170 - %s 171 106 order by followed_at desc 172 - %s 173 - `, whereClause, limitClause) 174 - 175 - rows, err := e.Query(query, args...) 107 + limit ?`, limit, 108 + ) 176 109 if err != nil { 177 110 return nil, err 178 111 } 112 + defer rows.Close() 113 + 179 114 for rows.Next() { 180 115 var follow Follow 181 116 var followedAt string 182 - err := rows.Scan( 183 - &follow.UserDid, 184 - &follow.SubjectDid, 185 - &followedAt, 186 - &follow.Rkey, 187 - ) 188 - if err != nil { 117 + if err := rows.Scan(&follow.UserDid, &follow.SubjectDid, &followedAt, &follow.Rkey); err != nil { 189 118 return nil, err 190 119 } 120 + 191 121 followedAtTime, err := time.Parse(time.RFC3339, followedAt) 192 122 if err != nil { 193 123 log.Println("unable to determine followed at time") ··· 195 125 } else { 196 126 follow.FollowedAt = followedAtTime 197 127 } 128 + 198 129 follows = append(follows, follow) 199 130 } 200 - return follows, nil 201 - } 202 - 203 - func GetFollowers(e Execer, did string) ([]Follow, error) { 204 - return GetFollows(e, 0, FilterEq("subject_did", did)) 205 - } 206 131 207 - func GetFollowing(e Execer, did string) ([]Follow, error) { 208 - return GetFollows(e, 0, FilterEq("user_did", did)) 209 - } 210 - 211 - type FollowStatus int 212 - 213 - const ( 214 - IsNotFollowing FollowStatus = iota 215 - IsFollowing 216 - IsSelf 217 - ) 218 - 219 - func (s FollowStatus) String() string { 220 - switch s { 221 - case IsNotFollowing: 222 - return "IsNotFollowing" 223 - case IsFollowing: 224 - return "IsFollowing" 225 - case IsSelf: 226 - return "IsSelf" 227 - default: 228 - return "IsNotFollowing" 132 + if err := rows.Err(); err != nil { 133 + return nil, err 229 134 } 230 - } 231 135 232 - func GetFollowStatus(e Execer, userDid, subjectDid string) FollowStatus { 233 - if userDid == subjectDid { 234 - return IsSelf 235 - } else if _, err := GetFollow(e, userDid, subjectDid); err != nil { 236 - return IsNotFollowing 237 - } else { 238 - return IsFollowing 239 - } 136 + return follows, nil 240 137 }
+105
appview/db/issues.go
··· 3 3 import ( 4 4 "database/sql" 5 5 "fmt" 6 + mathrand "math/rand/v2" 6 7 "strings" 7 8 "time" 8 9 ··· 47 48 48 49 func (i *Issue) AtUri() syntax.ATURI { 49 50 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.OwnerDid, tangled.RepoIssueNSID, i.Rkey)) 51 + } 52 + 53 + func IssueFromRecord(did, rkey string, record tangled.RepoIssue) Issue { 54 + created, err := time.Parse(time.RFC3339, record.CreatedAt) 55 + if err != nil { 56 + created = time.Now() 57 + } 58 + 59 + body := "" 60 + if record.Body != nil { 61 + body = *record.Body 62 + } 63 + 64 + return Issue{ 65 + RepoAt: syntax.ATURI(record.Repo), 66 + OwnerDid: record.Owner, 67 + Rkey: rkey, 68 + Created: created, 69 + Title: record.Title, 70 + Body: body, 71 + Open: true, // new issues are open by default 72 + } 73 + } 74 + 75 + func ResolveIssueFromAtUri(e Execer, issueUri syntax.ATURI) (syntax.ATURI, int, error) { 76 + ownerDid := issueUri.Authority().String() 77 + issueRkey := issueUri.RecordKey().String() 78 + 79 + var repoAt string 80 + var issueId int 81 + 82 + query := `select repo_at, issue_id from issues where owner_did = ? and rkey = ?` 83 + err := e.QueryRow(query, ownerDid, issueRkey).Scan(&repoAt, &issueId) 84 + if err != nil { 85 + return "", 0, err 86 + } 87 + 88 + return syntax.ATURI(repoAt), issueId, nil 89 + } 90 + 91 + func IssueCommentFromRecord(e Execer, did, rkey string, record tangled.RepoIssueComment) (Comment, error) { 92 + created, err := time.Parse(time.RFC3339, record.CreatedAt) 93 + if err != nil { 94 + created = time.Now() 95 + } 96 + 97 + ownerDid := did 98 + if record.Owner != nil { 99 + ownerDid = *record.Owner 100 + } 101 + 102 + issueUri, err := syntax.ParseATURI(record.Issue) 103 + if err != nil { 104 + return Comment{}, err 105 + } 106 + 107 + repoAt, issueId, err := ResolveIssueFromAtUri(e, issueUri) 108 + if err != nil { 109 + return Comment{}, err 110 + } 111 + 112 + comment := Comment{ 113 + OwnerDid: ownerDid, 114 + RepoAt: repoAt, 115 + Rkey: rkey, 116 + Body: record.Body, 117 + Issue: issueId, 118 + CommentId: mathrand.IntN(1000000), 119 + Created: &created, 120 + } 121 + 122 + return comment, nil 50 123 } 51 124 52 125 func NewIssue(tx *sql.Tx, issue *Issue) error { ··· 550 623 deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 551 624 where repo_at = ? and issue_id = ? and comment_id = ? 552 625 `, repoAt, issueId, commentId) 626 + return err 627 + } 628 + 629 + func UpdateCommentByRkey(e Execer, ownerDid, rkey, newBody string) error { 630 + _, err := e.Exec( 631 + ` 632 + update comments 633 + set body = ?, 634 + edited = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 635 + where owner_did = ? and rkey = ? 636 + `, newBody, ownerDid, rkey) 637 + return err 638 + } 639 + 640 + func DeleteCommentByRkey(e Execer, ownerDid, rkey string) error { 641 + _, err := e.Exec( 642 + ` 643 + update comments 644 + set body = "", 645 + deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 646 + where owner_did = ? and rkey = ? 647 + `, ownerDid, rkey) 648 + return err 649 + } 650 + 651 + func UpdateIssueByRkey(e Execer, ownerDid, rkey, title, body string) error { 652 + _, err := e.Exec(`update issues set title = ?, body = ? where owner_did = ? and rkey = ?`, title, body, ownerDid, rkey) 653 + return err 654 + } 655 + 656 + func DeleteIssueByRkey(e Execer, ownerDid, rkey string) error { 657 + _, err := e.Exec(`delete from issues where owner_did = ? and rkey = ?`, ownerDid, rkey) 553 658 return err 554 659 } 555 660
+7 -2
appview/db/profile.go
··· 348 348 return tx.Commit() 349 349 } 350 350 351 - func GetProfiles(e Execer, filters ...filter) (map[string]*Profile, error) { 351 + func GetProfiles(e Execer, filters ...filter) ([]Profile, error) { 352 352 var conditions []string 353 353 var args []any 354 354 for _, filter := range filters { ··· 448 448 idxs[did] = idx + 1 449 449 } 450 450 451 - return profileMap, nil 451 + var profiles []Profile 452 + for _, p := range profileMap { 453 + profiles = append(profiles, *p) 454 + } 455 + 456 + return profiles, nil 452 457 } 453 458 454 459 func GetProfile(e Execer, did string) (*Profile, error) {
+22 -6
appview/db/timeline.go
··· 20 20 *FollowStats 21 21 } 22 22 23 + type FollowStats struct { 24 + Followers int 25 + Following int 26 + } 27 + 23 28 const Limit = 50 24 29 25 30 // TODO: this gathers heterogenous events from different sources and aggregates ··· 132 137 } 133 138 134 139 func getTimelineFollows(e Execer) ([]TimelineEvent, error) { 135 - follows, err := GetFollows(e, Limit) 140 + follows, err := GetAllFollows(e, Limit) 136 141 if err != nil { 137 142 return nil, err 138 143 } ··· 146 151 return nil, nil 147 152 } 148 153 154 + profileMap := make(map[string]Profile) 149 155 profiles, err := GetProfiles(e, FilterIn("did", subjects)) 150 156 if err != nil { 151 157 return nil, err 158 + } 159 + for _, p := range profiles { 160 + profileMap[p.Did] = p 152 161 } 153 162 154 - followStatMap, err := GetFollowerFollowingCounts(e, subjects) 155 - if err != nil { 156 - return nil, err 163 + followStatMap := make(map[string]FollowStats) 164 + for _, s := range subjects { 165 + followers, following, err := GetFollowerFollowingCount(e, s) 166 + if err != nil { 167 + return nil, err 168 + } 169 + followStatMap[s] = FollowStats{ 170 + Followers: followers, 171 + Following: following, 172 + } 157 173 } 158 174 159 175 var events []TimelineEvent 160 176 for _, f := range follows { 161 - profile, _ := profiles[f.SubjectDid] 177 + profile, _ := profileMap[f.SubjectDid] 162 178 followStatMap, _ := followStatMap[f.SubjectDid] 163 179 164 180 events = append(events, TimelineEvent{ 165 181 Follow: &f, 166 - Profile: profile, 182 + Profile: &profile, 167 183 FollowStats: &followStatMap, 168 184 EventAt: f.FollowedAt, 169 185 })
+179 -6
appview/ingester.go
··· 5 5 "encoding/json" 6 6 "fmt" 7 7 "log/slog" 8 + "strings" 8 9 "time" 9 10 10 11 "github.com/bluesky-social/indigo/atproto/syntax" ··· 14 15 "tangled.sh/tangled.sh/core/api/tangled" 15 16 "tangled.sh/tangled.sh/core/appview/config" 16 17 "tangled.sh/tangled.sh/core/appview/db" 18 + "tangled.sh/tangled.sh/core/appview/pages/markup" 17 19 "tangled.sh/tangled.sh/core/appview/spindleverify" 18 20 "tangled.sh/tangled.sh/core/idresolver" 19 21 "tangled.sh/tangled.sh/core/rbac" ··· 61 63 case tangled.ActorProfileNSID: 62 64 err = i.ingestProfile(e) 63 65 case tangled.SpindleMemberNSID: 64 - err = i.ingestSpindleMember(e) 66 + err = i.ingestSpindleMember(ctx, e) 65 67 case tangled.SpindleNSID: 66 - err = i.ingestSpindle(e) 68 + err = i.ingestSpindle(ctx, e) 67 69 case tangled.StringNSID: 68 70 err = i.ingestString(e) 71 + case tangled.RepoIssueNSID: 72 + err = i.ingestIssue(ctx, e) 73 + case tangled.RepoIssueCommentNSID: 74 + err = i.ingestIssueComment(e) 69 75 } 70 76 l = i.Logger.With("nsid", e.Commit.Collection) 71 77 } ··· 336 342 return nil 337 343 } 338 344 339 - func (i *Ingester) ingestSpindleMember(e *models.Event) error { 345 + func (i *Ingester) ingestSpindleMember(ctx context.Context, e *models.Event) error { 340 346 did := e.Did 341 347 var err error 342 348 ··· 359 365 return fmt.Errorf("failed to enforce permissions: %w", err) 360 366 } 361 367 362 - memberId, err := i.IdResolver.ResolveIdent(context.Background(), record.Subject) 368 + memberId, err := i.IdResolver.ResolveIdent(ctx, record.Subject) 363 369 if err != nil { 364 370 return err 365 371 } ··· 442 448 return nil 443 449 } 444 450 445 - func (i *Ingester) ingestSpindle(e *models.Event) error { 451 + func (i *Ingester) ingestSpindle(ctx context.Context, e *models.Event) error { 446 452 did := e.Did 447 453 var err error 448 454 ··· 475 481 return err 476 482 } 477 483 478 - err = spindleverify.RunVerification(context.Background(), instance, did, i.Config.Core.Dev) 484 + err = spindleverify.RunVerification(ctx, instance, did, i.Config.Core.Dev) 479 485 if err != nil { 480 486 l.Error("failed to add spindle to db", "err", err, "instance", instance) 481 487 return err ··· 609 615 610 616 return nil 611 617 } 618 + 619 + func (i *Ingester) ingestIssue(ctx context.Context, e *models.Event) error { 620 + did := e.Did 621 + rkey := e.Commit.RKey 622 + 623 + var err error 624 + 625 + l := i.Logger.With("handler", "ingestIssue", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 626 + l.Info("ingesting record") 627 + 628 + ddb, ok := i.Db.Execer.(*db.DB) 629 + if !ok { 630 + return fmt.Errorf("failed to index issue record, invalid db cast") 631 + } 632 + 633 + switch e.Commit.Operation { 634 + case models.CommitOperationCreate: 635 + raw := json.RawMessage(e.Commit.Record) 636 + record := tangled.RepoIssue{} 637 + err = json.Unmarshal(raw, &record) 638 + if err != nil { 639 + l.Error("invalid record", "err", err) 640 + return err 641 + } 642 + 643 + issue := db.IssueFromRecord(did, rkey, record) 644 + 645 + sanitizer := markup.NewSanitizer() 646 + if st := strings.TrimSpace(sanitizer.SanitizeDescription(issue.Title)); st == "" { 647 + return fmt.Errorf("title is empty after HTML sanitization") 648 + } 649 + if sb := strings.TrimSpace(sanitizer.SanitizeDefault(issue.Body)); sb == "" { 650 + return fmt.Errorf("body is empty after HTML sanitization") 651 + } 652 + 653 + tx, err := ddb.BeginTx(ctx, nil) 654 + if err != nil { 655 + l.Error("failed to begin transaction", "err", err) 656 + return err 657 + } 658 + 659 + err = db.NewIssue(tx, &issue) 660 + if err != nil { 661 + l.Error("failed to create issue", "err", err) 662 + return err 663 + } 664 + 665 + return nil 666 + 667 + case models.CommitOperationUpdate: 668 + raw := json.RawMessage(e.Commit.Record) 669 + record := tangled.RepoIssue{} 670 + err = json.Unmarshal(raw, &record) 671 + if err != nil { 672 + l.Error("invalid record", "err", err) 673 + return err 674 + } 675 + 676 + body := "" 677 + if record.Body != nil { 678 + body = *record.Body 679 + } 680 + 681 + sanitizer := markup.NewSanitizer() 682 + if st := strings.TrimSpace(sanitizer.SanitizeDescription(record.Title)); st == "" { 683 + return fmt.Errorf("title is empty after HTML sanitization") 684 + } 685 + if sb := strings.TrimSpace(sanitizer.SanitizeDefault(body)); sb == "" { 686 + return fmt.Errorf("body is empty after HTML sanitization") 687 + } 688 + 689 + err = db.UpdateIssueByRkey(ddb, did, rkey, record.Title, body) 690 + if err != nil { 691 + l.Error("failed to update issue", "err", err) 692 + return err 693 + } 694 + 695 + return nil 696 + 697 + case models.CommitOperationDelete: 698 + if err := db.DeleteIssueByRkey(ddb, did, rkey); err != nil { 699 + l.Error("failed to delete", "err", err) 700 + return fmt.Errorf("failed to delete issue record: %w", err) 701 + } 702 + 703 + return nil 704 + } 705 + 706 + return fmt.Errorf("unknown operation: %s", e.Commit.Operation) 707 + } 708 + 709 + func (i *Ingester) ingestIssueComment(e *models.Event) error { 710 + did := e.Did 711 + rkey := e.Commit.RKey 712 + 713 + var err error 714 + 715 + l := i.Logger.With("handler", "ingestIssueComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 716 + l.Info("ingesting record") 717 + 718 + ddb, ok := i.Db.Execer.(*db.DB) 719 + if !ok { 720 + return fmt.Errorf("failed to index issue comment record, invalid db cast") 721 + } 722 + 723 + switch e.Commit.Operation { 724 + case models.CommitOperationCreate: 725 + raw := json.RawMessage(e.Commit.Record) 726 + record := tangled.RepoIssueComment{} 727 + err = json.Unmarshal(raw, &record) 728 + if err != nil { 729 + l.Error("invalid record", "err", err) 730 + return err 731 + } 732 + 733 + comment, err := db.IssueCommentFromRecord(ddb, did, rkey, record) 734 + if err != nil { 735 + l.Error("failed to parse comment from record", "err", err) 736 + return err 737 + } 738 + 739 + sanitizer := markup.NewSanitizer() 740 + if sb := strings.TrimSpace(sanitizer.SanitizeDefault(comment.Body)); sb == "" { 741 + return fmt.Errorf("body is empty after HTML sanitization") 742 + } 743 + 744 + err = db.NewIssueComment(ddb, &comment) 745 + if err != nil { 746 + l.Error("failed to create issue comment", "err", err) 747 + return err 748 + } 749 + 750 + return nil 751 + 752 + case models.CommitOperationUpdate: 753 + raw := json.RawMessage(e.Commit.Record) 754 + record := tangled.RepoIssueComment{} 755 + err = json.Unmarshal(raw, &record) 756 + if err != nil { 757 + l.Error("invalid record", "err", err) 758 + return err 759 + } 760 + 761 + sanitizer := markup.NewSanitizer() 762 + if sb := strings.TrimSpace(sanitizer.SanitizeDefault(record.Body)); sb == "" { 763 + return fmt.Errorf("body is empty after HTML sanitization") 764 + } 765 + 766 + err = db.UpdateCommentByRkey(ddb, did, rkey, record.Body) 767 + if err != nil { 768 + l.Error("failed to update issue comment", "err", err) 769 + return err 770 + } 771 + 772 + return nil 773 + 774 + case models.CommitOperationDelete: 775 + if err := db.DeleteCommentByRkey(ddb, did, rkey); err != nil { 776 + l.Error("failed to delete", "err", err) 777 + return fmt.Errorf("failed to delete issue comment record: %w", err) 778 + } 779 + 780 + return nil 781 + } 782 + 783 + return fmt.Errorf("unknown operation: %s", e.Commit.Operation) 784 + }
+4 -9
appview/issues/issues.go
··· 278 278 } 279 279 280 280 createdAt := time.Now().Format(time.RFC3339) 281 - commentIdInt64 := int64(commentId) 282 281 ownerDid := user.Did 283 282 issueAt, err := db.GetIssueAt(rp.db, f.RepoAt(), issueIdInt) 284 283 if err != nil { ··· 302 301 Val: &tangled.RepoIssueComment{ 303 302 Repo: &atUri, 304 303 Issue: issueAt, 305 - CommentId: &commentIdInt64, 306 304 Owner: &ownerDid, 307 305 Body: body, 308 306 CreatedAt: createdAt, ··· 451 449 repoAt := record["repo"].(string) 452 450 issueAt := record["issue"].(string) 453 451 createdAt := record["createdAt"].(string) 454 - commentIdInt64 := int64(commentIdInt) 455 452 456 453 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 457 454 Collection: tangled.RepoIssueCommentNSID, ··· 462 459 Val: &tangled.RepoIssueComment{ 463 460 Repo: &repoAt, 464 461 Issue: issueAt, 465 - CommentId: &commentIdInt64, 466 462 Owner: &comment.OwnerDid, 467 463 Body: newBody, 468 464 CreatedAt: createdAt, ··· 687 683 Rkey: issue.Rkey, 688 684 Record: &lexutil.LexiconTypeDecoder{ 689 685 Val: &tangled.RepoIssue{ 690 - Repo: atUri, 691 - Title: title, 692 - Body: &body, 693 - Owner: user.Did, 694 - IssueId: int64(issue.IssueId), 686 + Repo: atUri, 687 + Title: title, 688 + Body: &body, 689 + Owner: user.Did, 695 690 }, 696 691 }, 697 692 })
+3 -3
appview/middleware/middleware.go
··· 217 217 if err != nil { 218 218 // invalid did or handle 219 219 log.Println("failed to resolve repo") 220 - mw.pages.ErrorKnot404(w) 220 + mw.pages.Error404(w) 221 221 return 222 222 } 223 223 ··· 234 234 f, err := mw.repoResolver.Resolve(r) 235 235 if err != nil { 236 236 log.Println("failed to fully resolve repo", err) 237 - mw.pages.ErrorKnot404(w) 237 + http.Error(w, "invalid repo url", http.StatusNotFound) 238 238 return 239 239 } 240 240 ··· 283 283 f, err := mw.repoResolver.Resolve(r) 284 284 if err != nil { 285 285 log.Println("failed to fully resolve repo", err) 286 - mw.pages.ErrorKnot404(w) 286 + http.Error(w, "invalid repo url", http.StatusNotFound) 287 287 return 288 288 } 289 289
-5
appview/pages/funcmap.go
··· 277 277 "layoutCenter": func() string { 278 278 return "col-span-1 md:col-span-8 lg:col-span-6" 279 279 }, 280 - 281 - "normalizeForHtmlId": func(s string) string { 282 - // TODO: extend this to handle other cases? 283 - return strings.ReplaceAll(s, ":", "_") 284 - }, 285 280 } 286 281 } 287 282
+7 -39
appview/pages/pages.go
··· 408 408 return p.execute("repo/fork", w, params) 409 409 } 410 410 411 - type ProfileHomePageParams struct { 411 + type ProfilePageParams struct { 412 412 LoggedInUser *oauth.User 413 413 Repos []db.Repo 414 414 CollaboratingRepos []db.Repo ··· 418 418 } 419 419 420 420 type ProfileCard struct { 421 - UserDid string 422 - UserHandle string 423 - FollowStatus db.FollowStatus 424 - FollowersCount int 425 - FollowingCount int 421 + UserDid string 422 + UserHandle string 423 + FollowStatus db.FollowStatus 424 + Followers int 425 + Following int 426 426 427 427 Profile *db.Profile 428 428 } 429 429 430 - func (p *Pages) ProfileHomePage(w io.Writer, params ProfileHomePageParams) error { 430 + func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error { 431 431 return p.execute("user/profile", w, params) 432 432 } 433 433 ··· 439 439 440 440 func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error { 441 441 return p.execute("user/repos", w, params) 442 - } 443 - 444 - type FollowCard struct { 445 - UserDid string 446 - FollowStatus db.FollowStatus 447 - FollowersCount int 448 - FollowingCount int 449 - Profile *db.Profile 450 - } 451 - 452 - type FollowersPageParams struct { 453 - LoggedInUser *oauth.User 454 - Followers []FollowCard 455 - Card ProfileCard 456 - } 457 - 458 - func (p *Pages) FollowersPage(w io.Writer, params FollowersPageParams) error { 459 - return p.execute("user/followers", w, params) 460 - } 461 - 462 - type FollowingPageParams struct { 463 - LoggedInUser *oauth.User 464 - Following []FollowCard 465 - Card ProfileCard 466 - } 467 - 468 - func (p *Pages) FollowingPage(w io.Writer, params FollowingPageParams) error { 469 - return p.execute("user/following", w, params) 470 442 } 471 443 472 444 type FollowFragmentParams struct { ··· 1298 1270 1299 1271 func (p *Pages) Error404(w io.Writer) error { 1300 1272 return p.execute("errors/404", w, nil) 1301 - } 1302 - 1303 - func (p *Pages) ErrorKnot404(w io.Writer) error { 1304 - return p.execute("errors/knot404", w, nil) 1305 1273 } 1306 1274 1307 1275 func (p *Pages) Error503(w io.Writer) error {
+4 -24
appview/pages/templates/errors/404.html
··· 1 1 {{ define "title" }}404 &middot; tangled{{ end }} 2 2 3 3 {{ define "content" }} 4 - <div class="flex flex-col items-center justify-center min-h-[60vh] text-center"> 5 - <div class="bg-white dark:bg-gray-800 rounded-lg drop-shadow-sm p-8 max-w-lg mx-auto"> 6 - <div class="mb-6"> 7 - <div class="w-16 h-16 mx-auto mb-4 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center"> 8 - {{ i "search-x" "w-8 h-8 text-gray-400 dark:text-gray-500" }} 9 - </div> 10 - </div> 11 - 12 - <div class="space-y-4"> 13 - <h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white"> 14 - 404 &mdash; page not found 15 - </h1> 16 - <p class="text-gray-600 dark:text-gray-300"> 17 - The page you're looking for doesn't exist. It may have been moved, deleted, or you have the wrong URL. 18 - </p> 19 - <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6"> 20 - <a href="javascript:history.back()" class="btn px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"> 21 - {{ i "arrow-left" "w-4 h-4" }} 22 - go back 23 - </a> 24 - </div> 25 - </div> 26 - </div> 27 - </div> 4 + <h1>404 &mdash; nothing like that here!</h1> 5 + <p> 6 + It seems we couldn't find what you were looking for. Sorry about that! 7 + </p> 28 8 {{ end }}
+3 -36
appview/pages/templates/errors/500.html
··· 1 1 {{ define "title" }}500 &middot; tangled{{ end }} 2 2 3 3 {{ define "content" }} 4 - <div class="flex flex-col items-center justify-center min-h-[60vh] text-center"> 5 - <div class="bg-white dark:bg-gray-800 rounded-lg drop-shadow-sm p-8 max-w-lg mx-auto"> 6 - <div class="mb-6"> 7 - <div class="w-16 h-16 mx-auto mb-4 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center"> 8 - {{ i "alert-triangle" "w-8 h-8 text-red-500 dark:text-red-400" }} 9 - </div> 10 - </div> 11 - 12 - <div class="space-y-4"> 13 - <h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white"> 14 - 500 &mdash; internal server error 15 - </h1> 16 - <p class="text-gray-600 dark:text-gray-300"> 17 - Something went wrong on our end. We've been notified and are working to fix the issue. 18 - </p> 19 - <div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded p-3 text-sm text-yellow-800 dark:text-yellow-200"> 20 - <div class="flex items-center gap-2"> 21 - {{ i "info" "w-4 h-4" }} 22 - <span class="font-medium">we're on it!</span> 23 - </div> 24 - <p class="mt-1">Our team has been automatically notified about this error.</p> 25 - </div> 26 - <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6"> 27 - <button onclick="location.reload()" class="btn-create px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline hover:text-white"> 28 - {{ i "refresh-cw" "w-4 h-4" }} 29 - try again 30 - </button> 31 - <a href="/" class="btn px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"> 32 - {{ i "home" "w-4 h-4" }} 33 - back to home 34 - </a> 35 - </div> 36 - </div> 37 - </div> 38 - </div> 39 - {{ end }} 4 + <h1>500 &mdash; something broke!</h1> 5 + <p>We're working on getting service back up. Hang tight!</p> 6 + {{ end }}
+5 -28
appview/pages/templates/errors/503.html
··· 1 1 {{ define "title" }}503 &middot; tangled{{ end }} 2 2 3 3 {{ define "content" }} 4 - <div class="flex flex-col items-center justify-center min-h-[60vh] text-center"> 5 - <div class="bg-white dark:bg-gray-800 rounded-lg drop-shadow-sm p-8 max-w-lg mx-auto"> 6 - <div class="mb-6"> 7 - <div class="w-16 h-16 mx-auto mb-4 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center"> 8 - {{ i "server-off" "w-8 h-8 text-blue-500 dark:text-blue-400" }} 9 - </div> 10 - </div> 11 - 12 - <div class="space-y-4"> 13 - <h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white"> 14 - 503 &mdash; service unavailable 15 - </h1> 16 - <p class="text-gray-600 dark:text-gray-300"> 17 - We were unable to reach the knot hosting this repository. The service may be temporarily unavailable. 18 - </p> 19 - <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6"> 20 - <button onclick="location.reload()" class="btn-create px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline hover:text-white"> 21 - {{ i "refresh-cw" "w-4 h-4" }} 22 - try again 23 - </button> 24 - <a href="/" class="btn px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"> 25 - {{ i "arrow-left" "w-4 h-4" }} 26 - back to timeline 27 - </a> 28 - </div> 29 - </div> 30 - </div> 31 - </div> 4 + <h1>503 &mdash; unable to reach knot</h1> 5 + <p> 6 + We were unable to reach the knot hosting this repository. Try again 7 + later. 8 + </p> 32 9 {{ end }}
-28
appview/pages/templates/errors/knot404.html
··· 1 - {{ define "title" }}404 &middot; tangled{{ end }} 2 - 3 - {{ define "content" }} 4 - <div class="flex flex-col items-center justify-center min-h-[60vh] text-center"> 5 - <div class="bg-white dark:bg-gray-800 rounded-lg drop-shadow-sm p-8 max-w-lg mx-auto"> 6 - <div class="mb-6"> 7 - <div class="w-16 h-16 mx-auto mb-4 rounded-full bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center"> 8 - {{ i "book-x" "w-8 h-8 text-orange-500 dark:text-orange-400" }} 9 - </div> 10 - </div> 11 - 12 - <div class="space-y-4"> 13 - <h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white"> 14 - 404 &mdash; repository not found 15 - </h1> 16 - <p class="text-gray-600 dark:text-gray-300"> 17 - The repository you were looking for could not be found. The knot serving the repository may be unavailable. 18 - </p> 19 - <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6"> 20 - <a href="/" class="btn px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline"> 21 - {{ i "arrow-left" "w-4 h-4" }} 22 - back to timeline 23 - </a> 24 - </div> 25 - </div> 26 - </div> 27 - </div> 28 - {{ end }}
+14 -22
appview/pages/templates/repo/index.html
··· 356 356 357 357 {{ define "repoAfter" }} 358 358 {{- if or .HTMLReadme .Readme -}} 359 - <div class="mt-4 rounded bg-white dark:bg-gray-800 drop-shadow-sm w-full mx-auto overflow-hidden"> 360 - {{- if .ReadmeFileName -}} 361 - <div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 flex items-center gap-2"> 362 - {{ i "file-text" "w-4 h-4" "text-gray-600 dark:text-gray-400" }} 363 - <span class="font-mono text-sm text-gray-800 dark:text-gray-200">{{ .ReadmeFileName }}</span> 364 - </div> 365 - {{- end -}} 366 - <section 367 - class="p-6 overflow-auto {{ if not .Raw }} 368 - prose dark:prose-invert dark:[&_pre]:bg-gray-900 369 - dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900 370 - dark:[&_pre]:border dark:[&_pre]:border-gray-700 371 - {{ end }}" 372 - > 373 - <article class="{{ if .Raw }}whitespace-pre{{ end }}">{{- if .Raw -}}<pre class="dark:bg-gray-800 dark:text-white overflow-x-auto"> 374 - {{- .Readme -}} 375 - </pre> 376 - {{- else -}} 377 - {{ .HTMLReadme }} 378 - {{- end -}}</article> 379 - </section> 380 - </div> 359 + <section 360 + class="p-6 mt-4 rounded-br rounded-bl bg-white dark:bg-gray-800 dark:text-white drop-shadow-sm w-full mx-auto overflow-auto {{ if not .Raw }} 361 + prose dark:prose-invert dark:[&_pre]:bg-gray-900 362 + dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900 363 + dark:[&_pre]:border dark:[&_pre]:border-gray-700 364 + {{ end }}" 365 + > 366 + <article class="{{ if .Raw }}whitespace-pre{{ end }}">{{- if .Raw -}}<pre class="dark:bg-gray-800 dark:text-white overflow-x-auto"> 367 + {{- .Readme -}} 368 + </pre> 369 + {{- else -}} 370 + {{ .HTMLReadme }} 371 + {{- end -}}</article> 372 + </section> 381 373 {{- end -}} 382 374 {{ end }}
+3 -3
appview/pages/templates/timeline/timeline.html
··· 171 171 {{ end }} 172 172 {{ end }} 173 173 {{ with $stat }} 174 - <div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 174 + <div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm"> 175 175 <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 176 - <span id="followers"><a href="/{{ $subjectHandle }}?tab=followers">{{ .Followers }} followers</a></span> 176 + <span id="followers">{{ .Followers }} followers</span> 177 177 <span class="select-none after:content-['ยท']"></span> 178 - <span id="following"><a href="/{{ $subjectHandle }}?tab=following">{{ .Following }} following</a></span> 178 + <span id="following">{{ .Following }} following</span> 179 179 </div> 180 180 {{ end }} 181 181 </div>
-30
appview/pages/templates/user/followers.html
··· 1 - {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท followers {{ end }} 2 - 3 - {{ define "extrameta" }} 4 - <meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}'s followers" /> 5 - <meta property="og:type" content="object" /> 6 - <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}?tab=followers" /> 7 - <meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" /> 8 - {{ end }} 9 - 10 - {{ define "content" }} 11 - <div class="grid grid-cols-1 md:grid-cols-11 gap-4"> 12 - <div class="md:col-span-3 order-1 md:order-1"> 13 - {{ template "user/fragments/profileCard" .Card }} 14 - </div> 15 - <div id="all-followers" class="md:col-span-8 order-2 md:order-2"> 16 - {{ block "followers" . }}{{ end }} 17 - </div> 18 - </div> 19 - {{ end }} 20 - 21 - {{ define "followers" }} 22 - <p class="text-sm font-bold p-2 dark:text-white">ALL FOLLOWERS</p> 23 - <div id="followers" class="grid grid-cols-1 gap-4 mb-6"> 24 - {{ range .Followers }} 25 - {{ template "user/fragments/followCard" . }} 26 - {{ else }} 27 - <p class="px-6 dark:text-white">This user does not have any followers yet.</p> 28 - {{ end }} 29 - </div> 30 - {{ end }}
-30
appview/pages/templates/user/following.html
··· 1 - {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท following {{ end }} 2 - 3 - {{ define "extrameta" }} 4 - <meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}'s following" /> 5 - <meta property="og:type" content="object" /> 6 - <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}?tab=following" /> 7 - <meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" /> 8 - {{ end }} 9 - 10 - {{ define "content" }} 11 - <div class="grid grid-cols-1 md:grid-cols-11 gap-4"> 12 - <div class="md:col-span-3 order-1 md:order-1"> 13 - {{ template "user/fragments/profileCard" .Card }} 14 - </div> 15 - <div id="all-following" class="md:col-span-8 order-2 md:order-2"> 16 - {{ block "following" . }}{{ end }} 17 - </div> 18 - </div> 19 - {{ end }} 20 - 21 - {{ define "following" }} 22 - <p class="text-sm font-bold p-2 dark:text-white">ALL FOLLOWING</p> 23 - <div id="following" class="grid grid-cols-1 gap-4 mb-6"> 24 - {{ range .Following }} 25 - {{ template "user/fragments/followCard" . }} 26 - {{ else }} 27 - <p class="px-6 dark:text-white">This user does not follow anyone yet.</p> 28 - {{ end }} 29 - </div> 30 - {{ end }}
+2 -2
appview/pages/templates/user/fragments/follow.html
··· 1 1 {{ define "user/fragments/follow" }} 2 - <button id="{{ normalizeForHtmlId .UserDid }}" 2 + <button id="followBtn" 3 3 class="btn mt-2 w-full flex gap-2 items-center group" 4 4 5 5 {{ if eq .FollowStatus.String "IsNotFollowing" }} ··· 9 9 {{ end }} 10 10 11 11 hx-trigger="click" 12 - hx-target="#{{ normalizeForHtmlId .UserDid }}" 12 + hx-target="#followBtn" 13 13 hx-swap="outerHTML" 14 14 > 15 15 {{ if eq .FollowStatus.String "IsNotFollowing" }}Follow{{ else }}Unfollow{{ end }}
-29
appview/pages/templates/user/fragments/followCard.html
··· 1 - {{ define "user/fragments/followCard" }} 2 - {{ $userIdent := resolve .UserDid }} 3 - <div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm"> 4 - <div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex items-center gap-4"> 5 - <div class="flex-shrink-0 max-h-full w-24 h-24"> 6 - <img class="object-cover rounded-full p-2" src="{{ fullAvatar $userIdent }}" /> 7 - </div> 8 - 9 - <div class="flex-1 min-h-0 justify-around flex flex-col"> 10 - <a href="/{{ $userIdent }}"> 11 - <span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $userIdent | truncateAt30 }}</span> 12 - </a> 13 - <p class="text-sm pb-2 md:pb-2">{{.Profile.Description}}</p> 14 - <div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 15 - <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 16 - <span id="followers"><a href="/{{ $userIdent }}?tab=followers">{{ .FollowersCount }} followers</a></span> 17 - <span class="select-none after:content-['ยท']"></span> 18 - <span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .FollowingCount }} following</a></span> 19 - </div> 20 - </div> 21 - 22 - {{ if ne .FollowStatus.String "IsSelf" }} 23 - <div class="max-w-24"> 24 - {{ template "user/fragments/follow" . }} 25 - </div> 26 - {{ end }} 27 - </div> 28 - </div> 29 - {{ end }}
+14 -17
appview/pages/templates/user/fragments/profileCard.html
··· 1 1 {{ define "user/fragments/profileCard" }} 2 - {{ $userIdent := didOrHandle .UserDid .UserHandle }} 3 2 <div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm max-h-fit"> 4 3 <div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center"> 5 4 <div id="avatar" class="col-span-1 flex justify-center items-center"> ··· 9 8 </div> 10 9 <div class="col-span-2"> 11 10 <div class="flex items-center flex-row flex-nowrap gap-2"> 12 - <p title="{{ $userIdent }}" 11 + <p title="{{ didOrHandle .UserDid .UserHandle }}" 13 12 class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap"> 14 - {{ $userIdent }} 13 + {{ didOrHandle .UserDid .UserHandle }} 15 14 </p> 16 - <a href="/{{ $userIdent }}/feed.atom">{{ i "rss" "size-4" }}</a> 15 + <a href="/{{ didOrHandle .UserDid .UserHandle }}/feed.atom">{{ i "rss" "size-4" }}</a> 17 16 </div> 18 17 19 18 <div class="md:hidden"> 20 - {{ block "followerFollowing" (list . $userIdent) }} {{ end }} 19 + {{ block "followerFollowing" (list .Followers .Following) }} {{ end }} 21 20 </div> 22 21 </div> 23 22 <div class="col-span-3 md:col-span-full"> ··· 30 29 {{ end }} 31 30 32 31 <div class="hidden md:block"> 33 - {{ block "followerFollowing" (list $ $userIdent) }} {{ end }} 32 + {{ block "followerFollowing" (list $.Followers $.Following) }} {{ end }} 34 33 </div> 35 34 36 35 <div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> ··· 43 42 {{ if .IncludeBluesky }} 44 43 <div class="flex items-center gap-2"> 45 44 <span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white" }}</span> 46 - <a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ $userIdent }}</a> 45 + <a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ didOrHandle $.UserDid $.UserHandle }}</a> 47 46 </div> 48 47 {{ end }} 49 48 {{ range $link := .Links }} ··· 89 88 {{ end }} 90 89 91 90 {{ define "followerFollowing" }} 92 - {{ $root := index . 0 }} 93 - {{ $userIdent := index . 1 }} 94 - {{ with $root }} 95 - <div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm"> 96 - <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 97 - <span id="followers"><a href="/{{ $userIdent }}?tab=followers">{{ .FollowersCount }} followers</a></span> 98 - <span class="select-none after:content-['ยท']"></span> 99 - <span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .FollowingCount }} following</a></span> 100 - </div> 101 - {{ end }} 91 + {{ $followers := index . 0 }} 92 + {{ $following := index . 1 }} 93 + <div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm"> 94 + <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 95 + <span id="followers">{{ $followers }} followers</span> 96 + <span class="select-none after:content-['ยท']"></span> 97 + <span id="following">{{ $following }} following</span> 98 + </div> 102 99 {{ end }} 103 100
+1 -1
appview/pages/templates/user/repos.html
··· 3 3 {{ define "extrameta" }} 4 4 <meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}'s repos" /> 5 5 <meta property="og:type" content="object" /> 6 - <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}?tab=repos" /> 6 + <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}/repos" /> 7 7 <meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" /> 8 8 {{ end }} 9 9
-27
appview/repo/repo.go
··· 125 125 126 126 repolog, err := us.Log(f.OwnerDid(), f.Name, ref, page) 127 127 if err != nil { 128 - rp.pages.Error503(w) 129 128 log.Println("failed to reach knotserver", err) 130 129 return 131 130 } 132 131 133 132 tagResult, err := us.Tags(f.OwnerDid(), f.Name) 134 133 if err != nil { 135 - rp.pages.Error503(w) 136 134 log.Println("failed to reach knotserver", err) 137 135 return 138 136 } ··· 148 146 149 147 branchResult, err := us.Branches(f.OwnerDid(), f.Name) 150 148 if err != nil { 151 - rp.pages.Error503(w) 152 149 log.Println("failed to reach knotserver", err) 153 150 return 154 151 } ··· 315 312 316 313 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref)) 317 314 if err != nil { 318 - rp.pages.Error503(w) 319 315 log.Println("failed to reach knotserver", err) 320 316 return 321 317 } ··· 379 375 if !rp.config.Core.Dev { 380 376 protocol = "https" 381 377 } 382 - 383 - // if the tree path has a trailing slash, let's strip it 384 - // so we don't 404 385 - treePath = strings.TrimSuffix(treePath, "/") 386 - 387 378 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, treePath)) 388 379 if err != nil { 389 - rp.pages.Error503(w) 390 380 log.Println("failed to reach knotserver", err) 391 381 return 392 382 } 393 383 394 - // uhhh so knotserver returns a 500 if the entry isn't found in 395 - // the requested tree path, so let's stick to not-OK here. 396 - // we can fix this once we build out the xrpc apis for these operations. 397 - if resp.StatusCode != http.StatusOK { 398 - rp.pages.Error404(w) 399 - return 400 - } 401 - 402 384 body, err := io.ReadAll(resp.Body) 403 385 if err != nil { 404 386 log.Printf("Error reading response body: %v", err) ··· 456 438 457 439 result, err := us.Tags(f.OwnerDid(), f.Name) 458 440 if err != nil { 459 - rp.pages.Error503(w) 460 441 log.Println("failed to reach knotserver", err) 461 442 return 462 443 } ··· 514 495 515 496 result, err := us.Branches(f.OwnerDid(), f.Name) 516 497 if err != nil { 517 - rp.pages.Error503(w) 518 498 log.Println("failed to reach knotserver", err) 519 499 return 520 500 } ··· 544 524 } 545 525 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath)) 546 526 if err != nil { 547 - rp.pages.Error503(w) 548 527 log.Println("failed to reach knotserver", err) 549 - return 550 - } 551 - 552 - if resp.StatusCode == http.StatusNotFound { 553 - rp.pages.Error404(w) 554 528 return 555 529 } 556 530 ··· 1268 1242 1269 1243 result, err := us.Branches(f.OwnerDid(), f.Name) 1270 1244 if err != nil { 1271 - rp.pages.Error503(w) 1272 1245 log.Println("failed to reach knotserver", err) 1273 1246 return 1274 1247 }
+62 -212
appview/state/profile.go
··· 17 17 "github.com/gorilla/feeds" 18 18 "tangled.sh/tangled.sh/core/api/tangled" 19 19 "tangled.sh/tangled.sh/core/appview/db" 20 - "tangled.sh/tangled.sh/core/appview/oauth" 21 20 "tangled.sh/tangled.sh/core/appview/pages" 22 21 ) 23 22 ··· 25 24 tabVal := r.URL.Query().Get("tab") 26 25 switch tabVal { 27 26 case "": 28 - s.profileHomePage(w, r) 27 + s.profilePage(w, r) 29 28 case "repos": 30 29 s.reposPage(w, r) 31 - case "followers": 32 - s.followersPage(w, r) 33 - case "following": 34 - s.followingPage(w, r) 35 30 } 36 31 } 37 32 38 - type ProfilePageParams struct { 39 - Id identity.Identity 40 - LoggedInUser *oauth.User 41 - Card pages.ProfileCard 42 - } 43 - 44 - func (s *State) profilePage(w http.ResponseWriter, r *http.Request) *ProfilePageParams { 33 + func (s *State) profilePage(w http.ResponseWriter, r *http.Request) { 45 34 didOrHandle := chi.URLParam(r, "user") 46 35 if didOrHandle == "" { 47 - http.Error(w, "bad request", http.StatusBadRequest) 48 - return nil 36 + http.Error(w, "Bad request", http.StatusBadRequest) 37 + return 49 38 } 50 39 51 40 ident, ok := r.Context().Value("resolvedId").(identity.Identity) 52 41 if !ok { 53 - log.Printf("malformed middleware") 54 - w.WriteHeader(http.StatusInternalServerError) 55 - return nil 42 + s.pages.Error404(w) 43 + return 56 44 } 57 - did := ident.DID.String() 58 45 59 - profile, err := db.GetProfile(s.db, did) 46 + profile, err := db.GetProfile(s.db, ident.DID.String()) 60 47 if err != nil { 61 - log.Printf("getting profile data for %s: %s", did, err) 62 - s.pages.Error500(w) 63 - return nil 48 + log.Printf("getting profile data for %s: %s", ident.DID.String(), err) 64 49 } 65 50 66 - followStats, err := db.GetFollowerFollowingCount(s.db, did) 67 - if err != nil { 68 - log.Printf("getting follow stats for %s: %s", did, err) 69 - } 70 - 71 - loggedInUser := s.oauth.GetUser(r) 72 - followStatus := db.IsNotFollowing 73 - if loggedInUser != nil { 74 - followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did) 75 - } 76 - 77 - return &ProfilePageParams{ 78 - Id: ident, 79 - LoggedInUser: loggedInUser, 80 - Card: pages.ProfileCard{ 81 - UserDid: did, 82 - UserHandle: ident.Handle.String(), 83 - Profile: profile, 84 - FollowStatus: followStatus, 85 - FollowersCount: followStats.Followers, 86 - FollowingCount: followStats.Following, 87 - }, 88 - } 89 - } 90 - 91 - func (s *State) profileHomePage(w http.ResponseWriter, r *http.Request) { 92 - pageWithProfile := s.profilePage(w, r) 93 - if pageWithProfile == nil { 94 - return 95 - } 96 - 97 - id := pageWithProfile.Id 98 51 repos, err := db.GetRepos( 99 52 s.db, 100 53 0, 101 - db.FilterEq("did", id.DID), 54 + db.FilterEq("did", ident.DID.String()), 102 55 ) 103 56 if err != nil { 104 - log.Printf("getting repos for %s: %s", id.DID, err) 57 + log.Printf("getting repos for %s: %s", ident.DID.String(), err) 105 58 } 106 59 107 - profile := pageWithProfile.Card.Profile 108 60 // filter out ones that are pinned 109 61 pinnedRepos := []db.Repo{} 110 62 for i, r := range repos { ··· 119 71 } 120 72 } 121 73 122 - collaboratingRepos, err := db.CollaboratingIn(s.db, id.DID.String()) 74 + collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String()) 123 75 if err != nil { 124 - log.Printf("getting collaborating repos for %s: %s", id.DID, err) 76 + log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err) 125 77 } 126 78 127 79 pinnedCollaboratingRepos := []db.Repo{} ··· 132 84 } 133 85 } 134 86 135 - timeline, err := db.MakeProfileTimeline(s.db, id.DID.String()) 87 + timeline, err := db.MakeProfileTimeline(s.db, ident.DID.String()) 136 88 if err != nil { 137 - log.Printf("failed to create profile timeline for %s: %s", id.DID, err) 89 + log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err) 138 90 } 139 91 140 - var didsToResolve []string 141 - for _, r := range collaboratingRepos { 142 - didsToResolve = append(didsToResolve, r.Did) 92 + followers, following, err := db.GetFollowerFollowingCount(s.db, ident.DID.String()) 93 + if err != nil { 94 + log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 143 95 } 144 - for _, byMonth := range timeline.ByMonth { 145 - for _, pe := range byMonth.PullEvents.Items { 146 - didsToResolve = append(didsToResolve, pe.Repo.Did) 147 - } 148 - for _, ie := range byMonth.IssueEvents.Items { 149 - didsToResolve = append(didsToResolve, ie.Metadata.Repo.Did) 150 - } 151 - for _, re := range byMonth.RepoEvents { 152 - didsToResolve = append(didsToResolve, re.Repo.Did) 153 - if re.Source != nil { 154 - didsToResolve = append(didsToResolve, re.Source.Did) 155 - } 156 - } 96 + 97 + loggedInUser := s.oauth.GetUser(r) 98 + followStatus := db.IsNotFollowing 99 + if loggedInUser != nil { 100 + followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) 157 101 } 158 102 159 103 now := time.Now() 160 104 startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC) 161 105 punchcard, err := db.MakePunchcard( 162 106 s.db, 163 - db.FilterEq("did", id.DID), 107 + db.FilterEq("did", ident.DID.String()), 164 108 db.FilterGte("date", startOfYear.Format(time.DateOnly)), 165 109 db.FilterLte("date", now.Format(time.DateOnly)), 166 110 ) 167 111 if err != nil { 168 - log.Println("failed to get punchcard for did", "did", id.DID, "err", err) 112 + log.Println("failed to get punchcard for did", "did", ident.DID.String(), "err", err) 169 113 } 170 114 171 - s.pages.ProfileHomePage(w, pages.ProfileHomePageParams{ 172 - LoggedInUser: pageWithProfile.LoggedInUser, 115 + s.pages.ProfilePage(w, pages.ProfilePageParams{ 116 + LoggedInUser: loggedInUser, 173 117 Repos: pinnedRepos, 174 118 CollaboratingRepos: pinnedCollaboratingRepos, 175 - Card: pageWithProfile.Card, 176 - Punchcard: punchcard, 177 - ProfileTimeline: timeline, 119 + Card: pages.ProfileCard{ 120 + UserDid: ident.DID.String(), 121 + UserHandle: ident.Handle.String(), 122 + Profile: profile, 123 + FollowStatus: followStatus, 124 + Followers: followers, 125 + Following: following, 126 + }, 127 + Punchcard: punchcard, 128 + ProfileTimeline: timeline, 178 129 }) 179 130 } 180 131 181 132 func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { 182 - pageWithProfile := s.profilePage(w, r) 183 - if pageWithProfile == nil { 133 + ident, ok := r.Context().Value("resolvedId").(identity.Identity) 134 + if !ok { 135 + s.pages.Error404(w) 184 136 return 185 137 } 186 138 187 - id := pageWithProfile.Id 139 + profile, err := db.GetProfile(s.db, ident.DID.String()) 140 + if err != nil { 141 + log.Printf("getting profile data for %s: %s", ident.DID.String(), err) 142 + } 143 + 188 144 repos, err := db.GetRepos( 189 145 s.db, 190 146 0, 191 - db.FilterEq("did", id.DID), 147 + db.FilterEq("did", ident.DID.String()), 192 148 ) 193 149 if err != nil { 194 - log.Printf("getting repos for %s: %s", id.DID, err) 195 - } 196 - 197 - s.pages.ReposPage(w, pages.ReposPageParams{ 198 - LoggedInUser: pageWithProfile.LoggedInUser, 199 - Repos: repos, 200 - Card: pageWithProfile.Card, 201 - }) 202 - } 203 - 204 - type FollowsPageParams struct { 205 - LoggedInUser *oauth.User 206 - Follows []pages.FollowCard 207 - Card pages.ProfileCard 208 - } 209 - 210 - func (s *State) followPage(w http.ResponseWriter, r *http.Request, fetchFollows func(db.Execer, string) ([]db.Follow, error), extractDid func(db.Follow) string) (FollowsPageParams, error) { 211 - pageWithProfile := s.profilePage(w, r) 212 - if pageWithProfile == nil { 213 - return FollowsPageParams{}, nil 150 + log.Printf("getting repos for %s: %s", ident.DID.String(), err) 214 151 } 215 152 216 - id := pageWithProfile.Id 217 - loggedInUser := pageWithProfile.LoggedInUser 218 - 219 - follows, err := fetchFollows(s.db, id.DID.String()) 220 - if err != nil { 221 - log.Printf("getting followers for %s: %s", id.DID, err) 222 - return FollowsPageParams{}, err 153 + loggedInUser := s.oauth.GetUser(r) 154 + followStatus := db.IsNotFollowing 155 + if loggedInUser != nil { 156 + followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) 223 157 } 224 158 225 - if len(follows) == 0 { 226 - return FollowsPageParams{ 227 - LoggedInUser: loggedInUser, 228 - Follows: []pages.FollowCard{}, 229 - Card: pageWithProfile.Card, 230 - }, nil 231 - } 232 - 233 - followDids := make([]string, 0, len(follows)) 234 - for _, follow := range follows { 235 - followDids = append(followDids, extractDid(follow)) 236 - } 237 - 238 - profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids)) 159 + followers, following, err := db.GetFollowerFollowingCount(s.db, ident.DID.String()) 239 160 if err != nil { 240 - log.Printf("getting profile for %s: %s", followDids, err) 241 - return FollowsPageParams{}, err 242 - } 243 - 244 - followStatsMap, err := db.GetFollowerFollowingCounts(s.db, followDids) 245 - if err != nil { 246 - log.Printf("getting follow counts for %s: %s", followDids, err) 247 - } 248 - 249 - var loggedInUserFollowing map[string]struct{} 250 - if loggedInUser != nil { 251 - following, err := db.GetFollowing(s.db, loggedInUser.Did) 252 - if err != nil { 253 - return FollowsPageParams{}, err 254 - } 255 - if len(following) > 0 { 256 - loggedInUserFollowing = make(map[string]struct{}, len(following)) 257 - for _, follow := range following { 258 - loggedInUserFollowing[follow.SubjectDid] = struct{}{} 259 - } 260 - } 161 + log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 261 162 } 262 163 263 - followCards := make([]pages.FollowCard, 0, len(follows)) 264 - for _, did := range followDids { 265 - followStats, exists := followStatsMap[did] 266 - if !exists { 267 - followStats = db.FollowStats{} 268 - } 269 - followStatus := db.IsNotFollowing 270 - if loggedInUserFollowing != nil { 271 - if _, exists := loggedInUserFollowing[did]; exists { 272 - followStatus = db.IsFollowing 273 - } else if loggedInUser.Did == did { 274 - followStatus = db.IsSelf 275 - } 276 - } 277 - var profile *db.Profile 278 - if p, exists := profiles[did]; exists { 279 - profile = p 280 - } else { 281 - profile = &db.Profile{} 282 - profile.Did = did 283 - } 284 - followCards = append(followCards, pages.FollowCard{ 285 - UserDid: did, 286 - FollowStatus: followStatus, 287 - FollowersCount: followStats.Followers, 288 - FollowingCount: followStats.Following, 289 - Profile: profile, 290 - }) 291 - } 292 - 293 - return FollowsPageParams{ 164 + s.pages.ReposPage(w, pages.ReposPageParams{ 294 165 LoggedInUser: loggedInUser, 295 - Follows: followCards, 296 - Card: pageWithProfile.Card, 297 - }, nil 298 - } 299 - 300 - func (s *State) followersPage(w http.ResponseWriter, r *http.Request) { 301 - followPage, err := s.followPage(w, r, db.GetFollowers, func(f db.Follow) string { return f.UserDid }) 302 - if err != nil { 303 - s.pages.Notice(w, "all-followers", "Failed to load followers") 304 - return 305 - } 306 - 307 - s.pages.FollowersPage(w, pages.FollowersPageParams{ 308 - LoggedInUser: followPage.LoggedInUser, 309 - Followers: followPage.Follows, 310 - Card: followPage.Card, 311 - }) 312 - } 313 - 314 - func (s *State) followingPage(w http.ResponseWriter, r *http.Request) { 315 - followPage, err := s.followPage(w, r, db.GetFollowing, func(f db.Follow) string { return f.SubjectDid }) 316 - if err != nil { 317 - s.pages.Notice(w, "all-following", "Failed to load following") 318 - return 319 - } 320 - 321 - s.pages.FollowingPage(w, pages.FollowingPageParams{ 322 - LoggedInUser: followPage.LoggedInUser, 323 - Following: followPage.Follows, 324 - Card: followPage.Card, 166 + Repos: repos, 167 + Card: pages.ProfileCard{ 168 + UserDid: ident.DID.String(), 169 + UserHandle: ident.Handle.String(), 170 + Profile: profile, 171 + FollowStatus: followStatus, 172 + Followers: followers, 173 + Following: following, 174 + }, 325 175 }) 326 176 } 327 177
+2
appview/state/state.go
··· 94 94 tangled.SpindleMemberNSID, 95 95 tangled.SpindleNSID, 96 96 tangled.StringNSID, 97 + tangled.RepoIssueNSID, 98 + tangled.RepoIssueCommentNSID, 97 99 }, 98 100 nil, 99 101 slog.Default(),
+7 -7
appview/strings/strings.go
··· 202 202 followStatus = db.GetFollowStatus(s.Db, loggedInUser.Did, id.DID.String()) 203 203 } 204 204 205 - followStats, err := db.GetFollowerFollowingCount(s.Db, id.DID.String()) 205 + followers, following, err := db.GetFollowerFollowingCount(s.Db, id.DID.String()) 206 206 if err != nil { 207 207 l.Error("failed to get follow stats", "err", err) 208 208 } ··· 210 210 s.Pages.StringsDashboard(w, pages.StringsDashboardParams{ 211 211 LoggedInUser: s.OAuth.GetUser(r), 212 212 Card: pages.ProfileCard{ 213 - UserDid: id.DID.String(), 214 - UserHandle: id.Handle.String(), 215 - Profile: profile, 216 - FollowStatus: followStatus, 217 - FollowersCount: followStats.Followers, 218 - FollowingCount: followStats.Following, 213 + UserDid: id.DID.String(), 214 + UserHandle: id.Handle.String(), 215 + Profile: profile, 216 + FollowStatus: followStatus, 217 + Followers: followers, 218 + Following: following, 219 219 }, 220 220 Strings: all, 221 221 })
+1 -8
lexicons/issue/comment.json
··· 9 9 "key": "tid", 10 10 "record": { 11 11 "type": "object", 12 - "required": [ 13 - "issue", 14 - "body", 15 - "createdAt" 16 - ], 12 + "required": ["issue", "body", "createdAt"], 17 13 "properties": { 18 14 "issue": { 19 15 "type": "string", ··· 22 18 "repo": { 23 19 "type": "string", 24 20 "format": "at-uri" 25 - }, 26 - "commentId": { 27 - "type": "integer" 28 21 }, 29 22 "owner": { 30 23 "type": "string",
+1 -10
lexicons/issue/issue.json
··· 9 9 "key": "tid", 10 10 "record": { 11 11 "type": "object", 12 - "required": [ 13 - "repo", 14 - "issueId", 15 - "owner", 16 - "title", 17 - "createdAt" 18 - ], 12 + "required": ["repo", "owner", "title", "createdAt"], 19 13 "properties": { 20 14 "repo": { 21 15 "type": "string", 22 16 "format": "at-uri" 23 - }, 24 - "issueId": { 25 - "type": "integer" 26 17 }, 27 18 "owner": { 28 19 "type": "string",
-3
spindle/engines/nixery/engine.go
··· 201 201 Tty: false, 202 202 Hostname: "spindle", 203 203 WorkingDir: workspaceDir, 204 - Labels: map[string]string{ 205 - "sh.tangled.pipeline/workflow_id": wid.String(), 206 - }, 207 204 // TODO(winter): investigate whether environment variables passed here 208 205 // get propagated to ContainerExec processes 209 206 }, &container.HostConfig{