Monorepo for Tangled tangled.org

go.mod: bump indigo version #1147

closed opened by boltless.me targeting master from sl/zwywnyxswzkn

We will start using our own forked version of indigo package.

Signed-off-by: Seongmin Lee git@boltless.me

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.pull/3mgs6go47y422
+4099 -1233
Diff #0
+1 -1
.air/appview.toml
··· 6 6 bin = "out/appview.out" 7 7 8 8 include_ext = ["go"] 9 - exclude_dir = ["avatar", "camo", "indexes", "nix", "tmp"] 9 + exclude_dir = ["avatar", "camo", "sites", "indexes", "nix", "tmp"] 10 10 stop_on_error = true
+1 -1
.air/knot.toml
··· 7 7 args_bin = ["server"] 8 8 9 9 include_ext = ["go"] 10 - exclude_dir = ["avatar", "camo", "indexes", "nix", "tmp"] 10 + exclude_dir = ["avatar", "camo", "sites", "indexes", "nix", "tmp"] 11 11 stop_on_error = true
+1 -1
.air/spindle.toml
··· 6 6 bin = "out/spindle.out" 7 7 8 8 include_ext = ["go"] 9 - exclude_dir = ["avatar", "camo", "indexes", "nix", "tmp"] 9 + exclude_dir = ["avatar", "camo", "sites", "indexes", "nix", "tmp"] 10 10 stop_on_error = true
+416
api/tangled/cbor_gen.go
··· 604 604 605 605 return nil 606 606 } 607 + func (t *Comment) MarshalCBOR(w io.Writer) error { 608 + if t == nil { 609 + _, err := w.Write(cbg.CborNull) 610 + return err 611 + } 612 + 613 + cw := cbg.NewCborWriter(w) 614 + fieldCount := 7 615 + 616 + if t.Mentions == nil { 617 + fieldCount-- 618 + } 619 + 620 + if t.References == nil { 621 + fieldCount-- 622 + } 623 + 624 + if t.ReplyTo == nil { 625 + fieldCount-- 626 + } 627 + 628 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 629 + return err 630 + } 631 + 632 + // t.Body (string) (string) 633 + if len("body") > 1000000 { 634 + return xerrors.Errorf("Value in field \"body\" was too long") 635 + } 636 + 637 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil { 638 + return err 639 + } 640 + if _, err := cw.WriteString(string("body")); err != nil { 641 + return err 642 + } 643 + 644 + if len(t.Body) > 1000000 { 645 + return xerrors.Errorf("Value in field t.Body was too long") 646 + } 647 + 648 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Body))); err != nil { 649 + return err 650 + } 651 + if _, err := cw.WriteString(string(t.Body)); err != nil { 652 + return err 653 + } 654 + 655 + // t.LexiconTypeID (string) (string) 656 + if len("$type") > 1000000 { 657 + return xerrors.Errorf("Value in field \"$type\" was too long") 658 + } 659 + 660 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 661 + return err 662 + } 663 + if _, err := cw.WriteString(string("$type")); err != nil { 664 + return err 665 + } 666 + 667 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.comment"))); err != nil { 668 + return err 669 + } 670 + if _, err := cw.WriteString(string("sh.tangled.comment")); err != nil { 671 + return err 672 + } 673 + 674 + // t.ReplyTo (string) (string) 675 + if t.ReplyTo != nil { 676 + 677 + if len("replyTo") > 1000000 { 678 + return xerrors.Errorf("Value in field \"replyTo\" was too long") 679 + } 680 + 681 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("replyTo"))); err != nil { 682 + return err 683 + } 684 + if _, err := cw.WriteString(string("replyTo")); err != nil { 685 + return err 686 + } 687 + 688 + if t.ReplyTo == nil { 689 + if _, err := cw.Write(cbg.CborNull); err != nil { 690 + return err 691 + } 692 + } else { 693 + if len(*t.ReplyTo) > 1000000 { 694 + return xerrors.Errorf("Value in field t.ReplyTo was too long") 695 + } 696 + 697 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.ReplyTo))); err != nil { 698 + return err 699 + } 700 + if _, err := cw.WriteString(string(*t.ReplyTo)); err != nil { 701 + return err 702 + } 703 + } 704 + } 705 + 706 + // t.Subject (string) (string) 707 + if len("subject") > 1000000 { 708 + return xerrors.Errorf("Value in field \"subject\" was too long") 709 + } 710 + 711 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil { 712 + return err 713 + } 714 + if _, err := cw.WriteString(string("subject")); err != nil { 715 + return err 716 + } 717 + 718 + if len(t.Subject) > 1000000 { 719 + return xerrors.Errorf("Value in field t.Subject was too long") 720 + } 721 + 722 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Subject))); err != nil { 723 + return err 724 + } 725 + if _, err := cw.WriteString(string(t.Subject)); err != nil { 726 + return err 727 + } 728 + 729 + // t.Mentions ([]string) (slice) 730 + if t.Mentions != nil { 731 + 732 + if len("mentions") > 1000000 { 733 + return xerrors.Errorf("Value in field \"mentions\" was too long") 734 + } 735 + 736 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil { 737 + return err 738 + } 739 + if _, err := cw.WriteString(string("mentions")); err != nil { 740 + return err 741 + } 742 + 743 + if len(t.Mentions) > 8192 { 744 + return xerrors.Errorf("Slice value in field t.Mentions was too long") 745 + } 746 + 747 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil { 748 + return err 749 + } 750 + for _, v := range t.Mentions { 751 + if len(v) > 1000000 { 752 + return xerrors.Errorf("Value in field v was too long") 753 + } 754 + 755 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { 756 + return err 757 + } 758 + if _, err := cw.WriteString(string(v)); err != nil { 759 + return err 760 + } 761 + 762 + } 763 + } 764 + 765 + // t.CreatedAt (string) (string) 766 + if len("createdAt") > 1000000 { 767 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 768 + } 769 + 770 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 771 + return err 772 + } 773 + if _, err := cw.WriteString(string("createdAt")); err != nil { 774 + return err 775 + } 776 + 777 + if len(t.CreatedAt) > 1000000 { 778 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 779 + } 780 + 781 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 782 + return err 783 + } 784 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 785 + return err 786 + } 787 + 788 + // t.References ([]string) (slice) 789 + if t.References != nil { 790 + 791 + if len("references") > 1000000 { 792 + return xerrors.Errorf("Value in field \"references\" was too long") 793 + } 794 + 795 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil { 796 + return err 797 + } 798 + if _, err := cw.WriteString(string("references")); err != nil { 799 + return err 800 + } 801 + 802 + if len(t.References) > 8192 { 803 + return xerrors.Errorf("Slice value in field t.References was too long") 804 + } 805 + 806 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil { 807 + return err 808 + } 809 + for _, v := range t.References { 810 + if len(v) > 1000000 { 811 + return xerrors.Errorf("Value in field v was too long") 812 + } 813 + 814 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { 815 + return err 816 + } 817 + if _, err := cw.WriteString(string(v)); err != nil { 818 + return err 819 + } 820 + 821 + } 822 + } 823 + return nil 824 + } 825 + 826 + func (t *Comment) UnmarshalCBOR(r io.Reader) (err error) { 827 + *t = Comment{} 828 + 829 + cr := cbg.NewCborReader(r) 830 + 831 + maj, extra, err := cr.ReadHeader() 832 + if err != nil { 833 + return err 834 + } 835 + defer func() { 836 + if err == io.EOF { 837 + err = io.ErrUnexpectedEOF 838 + } 839 + }() 840 + 841 + if maj != cbg.MajMap { 842 + return fmt.Errorf("cbor input should be of type map") 843 + } 844 + 845 + if extra > cbg.MaxLength { 846 + return fmt.Errorf("Comment: map struct too large (%d)", extra) 847 + } 848 + 849 + n := extra 850 + 851 + nameBuf := make([]byte, 10) 852 + for i := uint64(0); i < n; i++ { 853 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 854 + if err != nil { 855 + return err 856 + } 857 + 858 + if !ok { 859 + // Field doesn't exist on this type, so ignore it 860 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 861 + return err 862 + } 863 + continue 864 + } 865 + 866 + switch string(nameBuf[:nameLen]) { 867 + // t.Body (string) (string) 868 + case "body": 869 + 870 + { 871 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 872 + if err != nil { 873 + return err 874 + } 875 + 876 + t.Body = string(sval) 877 + } 878 + // t.LexiconTypeID (string) (string) 879 + case "$type": 880 + 881 + { 882 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 883 + if err != nil { 884 + return err 885 + } 886 + 887 + t.LexiconTypeID = string(sval) 888 + } 889 + // t.ReplyTo (string) (string) 890 + case "replyTo": 891 + 892 + { 893 + b, err := cr.ReadByte() 894 + if err != nil { 895 + return err 896 + } 897 + if b != cbg.CborNull[0] { 898 + if err := cr.UnreadByte(); err != nil { 899 + return err 900 + } 901 + 902 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 903 + if err != nil { 904 + return err 905 + } 906 + 907 + t.ReplyTo = (*string)(&sval) 908 + } 909 + } 910 + // t.Subject (string) (string) 911 + case "subject": 912 + 913 + { 914 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 915 + if err != nil { 916 + return err 917 + } 918 + 919 + t.Subject = string(sval) 920 + } 921 + // t.Mentions ([]string) (slice) 922 + case "mentions": 923 + 924 + maj, extra, err = cr.ReadHeader() 925 + if err != nil { 926 + return err 927 + } 928 + 929 + if extra > 8192 { 930 + return fmt.Errorf("t.Mentions: array too large (%d)", extra) 931 + } 932 + 933 + if maj != cbg.MajArray { 934 + return fmt.Errorf("expected cbor array") 935 + } 936 + 937 + if extra > 0 { 938 + t.Mentions = make([]string, extra) 939 + } 940 + 941 + for i := 0; i < int(extra); i++ { 942 + { 943 + var maj byte 944 + var extra uint64 945 + var err error 946 + _ = maj 947 + _ = extra 948 + _ = err 949 + 950 + { 951 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 952 + if err != nil { 953 + return err 954 + } 955 + 956 + t.Mentions[i] = string(sval) 957 + } 958 + 959 + } 960 + } 961 + // t.CreatedAt (string) (string) 962 + case "createdAt": 963 + 964 + { 965 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 966 + if err != nil { 967 + return err 968 + } 969 + 970 + t.CreatedAt = string(sval) 971 + } 972 + // t.References ([]string) (slice) 973 + case "references": 974 + 975 + maj, extra, err = cr.ReadHeader() 976 + if err != nil { 977 + return err 978 + } 979 + 980 + if extra > 8192 { 981 + return fmt.Errorf("t.References: array too large (%d)", extra) 982 + } 983 + 984 + if maj != cbg.MajArray { 985 + return fmt.Errorf("expected cbor array") 986 + } 987 + 988 + if extra > 0 { 989 + t.References = make([]string, extra) 990 + } 991 + 992 + for i := 0; i < int(extra); i++ { 993 + { 994 + var maj byte 995 + var extra uint64 996 + var err error 997 + _ = maj 998 + _ = extra 999 + _ = err 1000 + 1001 + { 1002 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1003 + if err != nil { 1004 + return err 1005 + } 1006 + 1007 + t.References[i] = string(sval) 1008 + } 1009 + 1010 + } 1011 + } 1012 + 1013 + default: 1014 + // Field doesn't exist on this type, so ignore it 1015 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 1016 + return err 1017 + } 1018 + } 1019 + } 1020 + 1021 + return nil 1022 + } 607 1023 func (t *FeedReaction) MarshalCBOR(w io.Writer) error { 608 1024 if t == nil { 609 1025 _, err := w.Write(cbg.CborNull)
+27
api/tangled/tangledcomment.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package tangled 4 + 5 + // schema: sh.tangled.comment 6 + 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 11 + const ( 12 + CommentNSID = "sh.tangled.comment" 13 + ) 14 + 15 + func init() { 16 + util.RegisterType("sh.tangled.comment", &Comment{}) 17 + } // 18 + // RECORDTYPE: Comment 19 + type Comment struct { 20 + LexiconTypeID string `json:"$type,const=sh.tangled.comment" cborgen:"$type,const=sh.tangled.comment"` 21 + Body string `json:"body" cborgen:"body"` 22 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 + Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 24 + References []string `json:"references,omitempty" cborgen:"references,omitempty"` 25 + ReplyTo *string `json:"replyTo,omitempty" cborgen:"replyTo,omitempty"` 26 + Subject string `json:"subject" cborgen:"subject"` 27 + }
+3 -2
appview/config/config.go
··· 84 84 } 85 85 86 86 type PdsConfig struct { 87 - Host string `env:"HOST, default=https://tngl.sh"` 88 - AdminSecret string `env:"ADMIN_SECRET"` 87 + Host string `env:"HOST, default=https://tngl.sh"` 88 + HandleSuffix string `env:"HANDLE_SUFFIX, default=.tngl.sh"` 89 + AdminSecret string `env:"ADMIN_SECRET"` 89 90 } 90 91 91 92 type R2Config struct {
+202
appview/db/comments.go
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + "fmt" 6 + "maps" 7 + "slices" 8 + "sort" 9 + "strings" 10 + "time" 11 + 12 + "github.com/bluesky-social/indigo/atproto/syntax" 13 + "tangled.org/core/api/tangled" 14 + "tangled.org/core/appview/models" 15 + "tangled.org/core/orm" 16 + ) 17 + 18 + func PutComment(tx *sql.Tx, c *models.Comment) error { 19 + if c.Collection == "" { 20 + c.Collection = tangled.CommentNSID 21 + } 22 + result, err := tx.Exec( 23 + `insert into comments ( 24 + did, 25 + collection, 26 + rkey, 27 + subject_at, 28 + reply_to, 29 + body, 30 + pull_submission_id, 31 + created 32 + ) 33 + values (?, ?, ?, ?, ?, ?, ?, ?) 34 + on conflict(did, collection, rkey) do update set 35 + subject_at = excluded.subject_at, 36 + reply_to = excluded.reply_to, 37 + body = excluded.body, 38 + edited = case 39 + when 40 + comments.subject_at != excluded.subject_at 41 + or comments.body != excluded.body 42 + or comments.reply_to != excluded.reply_to 43 + then ? 44 + else comments.edited 45 + end`, 46 + c.Did, 47 + c.Collection, 48 + c.Rkey, 49 + c.Subject, 50 + c.ReplyTo, 51 + c.Body, 52 + c.PullSubmissionId, 53 + c.Created.Format(time.RFC3339), 54 + time.Now().Format(time.RFC3339), 55 + ) 56 + if err != nil { 57 + return err 58 + } 59 + 60 + c.Id, err = result.LastInsertId() 61 + if err != nil { 62 + return err 63 + } 64 + 65 + if err := putReferences(tx, c.AtUri(), c.References); err != nil { 66 + return fmt.Errorf("put reference_links: %w", err) 67 + } 68 + 69 + return nil 70 + } 71 + 72 + func DeleteComments(e Execer, filters ...orm.Filter) error { 73 + var conditions []string 74 + var args []any 75 + for _, filter := range filters { 76 + conditions = append(conditions, filter.Condition()) 77 + args = append(args, filter.Arg()...) 78 + } 79 + 80 + whereClause := "" 81 + if conditions != nil { 82 + whereClause = " where " + strings.Join(conditions, " and ") 83 + } 84 + 85 + query := fmt.Sprintf(`update comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause) 86 + 87 + _, err := e.Exec(query, args...) 88 + return err 89 + } 90 + 91 + func GetComments(e Execer, filters ...orm.Filter) ([]models.Comment, error) { 92 + commentMap := make(map[string]*models.Comment) 93 + 94 + var conditions []string 95 + var args []any 96 + for _, filter := range filters { 97 + conditions = append(conditions, filter.Condition()) 98 + args = append(args, filter.Arg()...) 99 + } 100 + 101 + whereClause := "" 102 + if conditions != nil { 103 + whereClause = " where " + strings.Join(conditions, " and ") 104 + } 105 + 106 + query := fmt.Sprintf(` 107 + select 108 + id, 109 + did, 110 + collection, 111 + rkey, 112 + subject_at, 113 + reply_to, 114 + body, 115 + pull_submission_id, 116 + created, 117 + edited, 118 + deleted 119 + from 120 + comments 121 + %s 122 + `, whereClause) 123 + 124 + rows, err := e.Query(query, args...) 125 + if err != nil { 126 + return nil, err 127 + } 128 + 129 + for rows.Next() { 130 + var comment models.Comment 131 + var created string 132 + var edited, deleted, replyTo sql.Null[string] 133 + err := rows.Scan( 134 + &comment.Id, 135 + &comment.Did, 136 + &comment.Collection, 137 + &comment.Rkey, 138 + &comment.Subject, 139 + &replyTo, 140 + &comment.Body, 141 + &comment.PullSubmissionId, 142 + &created, 143 + &edited, 144 + &deleted, 145 + ) 146 + if err != nil { 147 + return nil, err 148 + } 149 + 150 + if t, err := time.Parse(time.RFC3339, created); err == nil { 151 + comment.Created = t 152 + } 153 + 154 + if edited.Valid { 155 + if t, err := time.Parse(time.RFC3339, edited.V); err == nil { 156 + comment.Edited = &t 157 + } 158 + } 159 + 160 + if deleted.Valid { 161 + if t, err := time.Parse(time.RFC3339, deleted.V); err == nil { 162 + comment.Deleted = &t 163 + } 164 + } 165 + 166 + if replyTo.Valid { 167 + rt := syntax.ATURI(replyTo.V) 168 + comment.ReplyTo = &rt 169 + } 170 + 171 + atUri := comment.AtUri().String() 172 + commentMap[atUri] = &comment 173 + } 174 + 175 + if err := rows.Err(); err != nil { 176 + return nil, err 177 + } 178 + defer rows.Close() 179 + 180 + // collect references from each comments 181 + commentAts := slices.Collect(maps.Keys(commentMap)) 182 + allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts)) 183 + if err != nil { 184 + return nil, fmt.Errorf("failed to query reference_links: %w", err) 185 + } 186 + for commentAt, references := range allReferencs { 187 + if comment, ok := commentMap[commentAt.String()]; ok { 188 + comment.References = references 189 + } 190 + } 191 + 192 + var comments []models.Comment 193 + for _, c := range commentMap { 194 + comments = append(comments, *c) 195 + } 196 + 197 + sort.Slice(comments, func(i, j int) bool { 198 + return comments[i].Created.Before(comments[j].Created) 199 + }) 200 + 201 + return comments, nil 202 + }
+81
appview/db/db.go
··· 1287 1287 return err 1288 1288 }) 1289 1289 1290 + orm.RunMigration(conn, logger, "add-comments-table", func(tx *sql.Tx) error { 1291 + _, err := tx.Exec(` 1292 + drop table if exists comments; 1293 + 1294 + create table comments ( 1295 + -- identifiers 1296 + id integer primary key autoincrement, 1297 + did text not null, 1298 + collection text not null default 'sh.tangled.comment', 1299 + rkey text not null, 1300 + at_uri text generated always as ('at://' || did || '/' || collection || '/' || rkey) stored, 1301 + 1302 + -- at identifiers 1303 + subject_at text not null, 1304 + reply_to text, -- at_uri of parent comment 1305 + 1306 + pull_submission_id integer, -- dirty fix until we atprotate the pull-rounds 1307 + 1308 + -- content 1309 + body text not null, 1310 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1311 + edited text, 1312 + deleted text, 1313 + 1314 + -- constraints 1315 + unique(did, collection, rkey) 1316 + ); 1317 + 1318 + insert into comments ( 1319 + did, 1320 + collection, 1321 + rkey, 1322 + subject_at, 1323 + reply_to, 1324 + body, 1325 + created, 1326 + edited, 1327 + deleted 1328 + ) 1329 + select 1330 + did, 1331 + 'sh.tangled.repo.issue.comment', 1332 + rkey, 1333 + issue_at, 1334 + reply_to, 1335 + body, 1336 + created, 1337 + edited, 1338 + deleted 1339 + from issue_comments 1340 + where rkey is not null; 1341 + 1342 + insert into comments ( 1343 + did, 1344 + collection, 1345 + rkey, 1346 + subject_at, 1347 + pull_submission_id, 1348 + body, 1349 + created 1350 + ) 1351 + select 1352 + c.owner_did, 1353 + 'sh.tangled.repo.pull.comment', 1354 + substr( 1355 + substr(c.comment_at, 6 + instr(substr(c.comment_at, 6), '/')), -- nsid/rkey 1356 + instr( 1357 + substr(c.comment_at, 6 + instr(substr(c.comment_at, 6), '/')), -- nsid/rkey 1358 + '/' 1359 + ) + 1 1360 + ), -- rkey 1361 + p.at_uri, 1362 + c.submission_id, 1363 + c.body, 1364 + c.created 1365 + from pull_comments c 1366 + join pulls p on c.repo_at = p.repo_at and c.pull_id = p.pull_id; 1367 + `) 1368 + return err 1369 + }) 1370 + 1290 1371 return &DB{ 1291 1372 db, 1292 1373 logger,
+6 -186
appview/db/issues.go
··· 100 100 } 101 101 102 102 func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) { 103 - issueMap := make(map[string]*models.Issue) // at-uri -> issue 103 + issueMap := make(map[syntax.ATURI]*models.Issue) // at-uri -> issue 104 104 105 105 var conditions []string 106 106 var args []any ··· 196 196 } 197 197 } 198 198 199 - atUri := issue.AtUri().String() 200 - issueMap[atUri] = &issue 199 + issueMap[issue.AtUri()] = &issue 201 200 } 202 201 203 202 // collect reverse repos ··· 229 228 // collect comments 230 229 issueAts := slices.Collect(maps.Keys(issueMap)) 231 230 232 - comments, err := GetIssueComments(e, orm.FilterIn("issue_at", issueAts)) 231 + comments, err := GetComments(e, orm.FilterIn("subject_at", issueAts)) 233 232 if err != nil { 234 233 return nil, fmt.Errorf("failed to query comments: %w", err) 235 234 } 236 235 for i := range comments { 237 - issueAt := comments[i].IssueAt 236 + issueAt := comments[i].Subject 238 237 if issue, ok := issueMap[issueAt]; ok { 239 238 issue.Comments = append(issue.Comments, comments[i]) 240 239 } ··· 246 245 return nil, fmt.Errorf("failed to query labels: %w", err) 247 246 } 248 247 for issueAt, labels := range allLabels { 249 - if issue, ok := issueMap[issueAt.String()]; ok { 248 + if issue, ok := issueMap[issueAt]; ok { 250 249 issue.Labels = labels 251 250 } 252 251 } ··· 257 256 return nil, fmt.Errorf("failed to query reference_links: %w", err) 258 257 } 259 258 for issueAt, references := range allReferencs { 260 - if issue, ok := issueMap[issueAt.String()]; ok { 259 + if issue, ok := issueMap[issueAt]; ok { 261 260 issue.References = references 262 261 } 263 262 } ··· 293 292 294 293 func GetIssues(e Execer, filters ...orm.Filter) ([]models.Issue, error) { 295 294 return GetIssuesPaginated(e, pagination.Page{}, filters...) 296 - } 297 - 298 - func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) { 299 - result, err := tx.Exec( 300 - `insert into issue_comments ( 301 - did, 302 - rkey, 303 - issue_at, 304 - body, 305 - reply_to, 306 - created, 307 - edited 308 - ) 309 - values (?, ?, ?, ?, ?, ?, null) 310 - on conflict(did, rkey) do update set 311 - issue_at = excluded.issue_at, 312 - body = excluded.body, 313 - edited = case 314 - when 315 - issue_comments.issue_at != excluded.issue_at 316 - or issue_comments.body != excluded.body 317 - or issue_comments.reply_to != excluded.reply_to 318 - then ? 319 - else issue_comments.edited 320 - end`, 321 - c.Did, 322 - c.Rkey, 323 - c.IssueAt, 324 - c.Body, 325 - c.ReplyTo, 326 - c.Created.Format(time.RFC3339), 327 - time.Now().Format(time.RFC3339), 328 - ) 329 - if err != nil { 330 - return 0, err 331 - } 332 - 333 - id, err := result.LastInsertId() 334 - if err != nil { 335 - return 0, err 336 - } 337 - 338 - if err := putReferences(tx, c.AtUri(), c.References); err != nil { 339 - return 0, fmt.Errorf("put reference_links: %w", err) 340 - } 341 - 342 - return id, nil 343 - } 344 - 345 - func DeleteIssueComments(e Execer, filters ...orm.Filter) error { 346 - var conditions []string 347 - var args []any 348 - for _, filter := range filters { 349 - conditions = append(conditions, filter.Condition()) 350 - args = append(args, filter.Arg()...) 351 - } 352 - 353 - whereClause := "" 354 - if conditions != nil { 355 - whereClause = " where " + strings.Join(conditions, " and ") 356 - } 357 - 358 - query := fmt.Sprintf(`update issue_comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause) 359 - 360 - _, err := e.Exec(query, args...) 361 - return err 362 - } 363 - 364 - func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) { 365 - commentMap := make(map[string]*models.IssueComment) 366 - 367 - var conditions []string 368 - var args []any 369 - for _, filter := range filters { 370 - conditions = append(conditions, filter.Condition()) 371 - args = append(args, filter.Arg()...) 372 - } 373 - 374 - whereClause := "" 375 - if conditions != nil { 376 - whereClause = " where " + strings.Join(conditions, " and ") 377 - } 378 - 379 - query := fmt.Sprintf(` 380 - select 381 - id, 382 - did, 383 - rkey, 384 - issue_at, 385 - reply_to, 386 - body, 387 - created, 388 - edited, 389 - deleted 390 - from 391 - issue_comments 392 - %s 393 - `, whereClause) 394 - 395 - rows, err := e.Query(query, args...) 396 - if err != nil { 397 - return nil, err 398 - } 399 - defer rows.Close() 400 - 401 - for rows.Next() { 402 - var comment models.IssueComment 403 - var created string 404 - var rkey, edited, deleted, replyTo sql.Null[string] 405 - err := rows.Scan( 406 - &comment.Id, 407 - &comment.Did, 408 - &rkey, 409 - &comment.IssueAt, 410 - &replyTo, 411 - &comment.Body, 412 - &created, 413 - &edited, 414 - &deleted, 415 - ) 416 - if err != nil { 417 - return nil, err 418 - } 419 - 420 - // this is a remnant from old times, newer comments always have rkey 421 - if rkey.Valid { 422 - comment.Rkey = rkey.V 423 - } 424 - 425 - if t, err := time.Parse(time.RFC3339, created); err == nil { 426 - comment.Created = t 427 - } 428 - 429 - if edited.Valid { 430 - if t, err := time.Parse(time.RFC3339, edited.V); err == nil { 431 - comment.Edited = &t 432 - } 433 - } 434 - 435 - if deleted.Valid { 436 - if t, err := time.Parse(time.RFC3339, deleted.V); err == nil { 437 - comment.Deleted = &t 438 - } 439 - } 440 - 441 - if replyTo.Valid { 442 - comment.ReplyTo = &replyTo.V 443 - } 444 - 445 - atUri := comment.AtUri().String() 446 - commentMap[atUri] = &comment 447 - } 448 - 449 - if err = rows.Err(); err != nil { 450 - return nil, err 451 - } 452 - 453 - // collect references for each comments 454 - commentAts := slices.Collect(maps.Keys(commentMap)) 455 - allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts)) 456 - if err != nil { 457 - return nil, fmt.Errorf("failed to query reference_links: %w", err) 458 - } 459 - for commentAt, references := range allReferencs { 460 - if comment, ok := commentMap[commentAt.String()]; ok { 461 - comment.References = references 462 - } 463 - } 464 - 465 - var comments []models.IssueComment 466 - for _, c := range commentMap { 467 - comments = append(comments, *c) 468 - } 469 - 470 - sort.Slice(comments, func(i, j int) bool { 471 - return comments[i].Created.After(comments[j].Created) 472 - }) 473 - 474 - return comments, nil 475 295 } 476 296 477 297 func DeleteIssues(tx *sql.Tx, did, rkey string) error {
+6 -121
appview/db/pulls.go
··· 391 391 return nil, err 392 392 } 393 393 394 - // Get comments for all submissions using GetPullComments 394 + // Get comments for all submissions using GetComments 395 395 submissionIds := slices.Collect(maps.Keys(submissionMap)) 396 - comments, err := GetPullComments(e, orm.FilterIn("submission_id", submissionIds)) 396 + comments, err := GetComments(e, orm.FilterIn("pull_submission_id", submissionIds)) 397 397 if err != nil { 398 398 return nil, fmt.Errorf("failed to get pull comments: %w", err) 399 399 } 400 400 for _, comment := range comments { 401 - if submission, ok := submissionMap[comment.SubmissionId]; ok { 402 - submission.Comments = append(submission.Comments, comment) 401 + if comment.PullSubmissionId != nil { 402 + if submission, ok := submissionMap[*comment.PullSubmissionId]; ok { 403 + submission.Comments = append(submission.Comments, comment) 404 + } 403 405 } 404 406 } 405 407 ··· 419 421 return m, nil 420 422 } 421 423 422 - func GetPullComments(e Execer, filters ...orm.Filter) ([]models.PullComment, error) { 423 - var conditions []string 424 - var args []any 425 - for _, filter := range filters { 426 - conditions = append(conditions, filter.Condition()) 427 - args = append(args, filter.Arg()...) 428 - } 429 - 430 - whereClause := "" 431 - if conditions != nil { 432 - whereClause = " where " + strings.Join(conditions, " and ") 433 - } 434 - 435 - query := fmt.Sprintf(` 436 - select 437 - id, 438 - pull_id, 439 - submission_id, 440 - repo_at, 441 - owner_did, 442 - comment_at, 443 - body, 444 - created 445 - from 446 - pull_comments 447 - %s 448 - order by 449 - created asc 450 - `, whereClause) 451 - 452 - rows, err := e.Query(query, args...) 453 - if err != nil { 454 - return nil, err 455 - } 456 - defer rows.Close() 457 - 458 - commentMap := make(map[string]*models.PullComment) 459 - for rows.Next() { 460 - var comment models.PullComment 461 - var createdAt string 462 - err := rows.Scan( 463 - &comment.ID, 464 - &comment.PullId, 465 - &comment.SubmissionId, 466 - &comment.RepoAt, 467 - &comment.OwnerDid, 468 - &comment.CommentAt, 469 - &comment.Body, 470 - &createdAt, 471 - ) 472 - if err != nil { 473 - return nil, err 474 - } 475 - 476 - if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 477 - comment.Created = t 478 - } 479 - 480 - atUri := comment.AtUri().String() 481 - commentMap[atUri] = &comment 482 - } 483 - 484 - if err := rows.Err(); err != nil { 485 - return nil, err 486 - } 487 - 488 - // collect references for each comments 489 - commentAts := slices.Collect(maps.Keys(commentMap)) 490 - allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts)) 491 - if err != nil { 492 - return nil, fmt.Errorf("failed to query reference_links: %w", err) 493 - } 494 - for commentAt, references := range allReferencs { 495 - if comment, ok := commentMap[commentAt.String()]; ok { 496 - comment.References = references 497 - } 498 - } 499 - 500 - var comments []models.PullComment 501 - for _, c := range commentMap { 502 - comments = append(comments, *c) 503 - } 504 - 505 - sort.Slice(comments, func(i, j int) bool { 506 - return comments[i].Created.Before(comments[j].Created) 507 - }) 508 - 509 - return comments, nil 510 - } 511 - 512 424 // timeframe here is directly passed into the sql query filter, and any 513 425 // timeframe in the past should be negative; e.g.: "-3 months" 514 426 func GetPullsByOwnerDid(e Execer, did, timeframe string) ([]models.Pull, error) { ··· 583 495 } 584 496 585 497 return pulls, nil 586 - } 587 - 588 - func NewPullComment(tx *sql.Tx, comment *models.PullComment) (int64, error) { 589 - query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)` 590 - res, err := tx.Exec( 591 - query, 592 - comment.OwnerDid, 593 - comment.RepoAt, 594 - comment.SubmissionId, 595 - comment.CommentAt, 596 - comment.PullId, 597 - comment.Body, 598 - ) 599 - if err != nil { 600 - return 0, err 601 - } 602 - 603 - i, err := res.LastInsertId() 604 - if err != nil { 605 - return 0, err 606 - } 607 - 608 - if err := putReferences(tx, comment.AtUri(), comment.References); err != nil { 609 - return 0, fmt.Errorf("put reference_links: %w", err) 610 - } 611 - 612 - return i, nil 613 498 } 614 499 615 500 func SetPullState(e Execer, repoAt syntax.ATURI, pullId int, pullState models.PullState) error {
+20 -32
appview/db/reference.go
··· 11 11 "tangled.org/core/orm" 12 12 ) 13 13 14 - // ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs. 14 + // ValidateReferenceLinks resolves refLinks to Issue/PR/Comment ATURIs. 15 15 // It will ignore missing refLinks. 16 16 func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 17 17 var ( ··· 53 53 values %s 54 54 ) 55 55 select 56 - i.did, i.rkey, 57 - c.did, c.rkey 56 + i.at_uri, c.at_uri 58 57 from input inp 59 58 join repos r 60 59 on r.did = inp.owner_did ··· 62 61 join issues i 63 62 on i.repo_at = r.at_uri 64 63 and i.issue_id = inp.issue_id 65 - left join issue_comments c 64 + left join comments c 66 65 on inp.comment_id is not null 67 - and c.issue_at = i.at_uri 66 + and c.subject_at = i.at_uri 68 67 and c.id = inp.comment_id 69 68 `, 70 69 strings.Join(vals, ","), ··· 79 78 80 79 for rows.Next() { 81 80 // Scan rows 82 - var issueOwner, issueRkey string 83 - var commentOwner, commentRkey sql.NullString 81 + var issueUri string 82 + var commentUri sql.NullString 84 83 var uri syntax.ATURI 85 - if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil { 84 + if err := rows.Scan(&issueUri, &commentUri); err != nil { 86 85 return nil, err 87 86 } 88 - if commentOwner.Valid && commentRkey.Valid { 89 - uri = syntax.ATURI(fmt.Sprintf( 90 - "at://%s/%s/%s", 91 - commentOwner.String, 92 - tangled.RepoIssueCommentNSID, 93 - commentRkey.String, 94 - )) 87 + if commentUri.Valid { 88 + uri = syntax.ATURI(commentUri.String) 95 89 } else { 96 - uri = syntax.ATURI(fmt.Sprintf( 97 - "at://%s/%s/%s", 98 - issueOwner, 99 - tangled.RepoIssueNSID, 100 - issueRkey, 101 - )) 90 + uri = syntax.ATURI(issueUri) 102 91 } 103 92 uris = append(uris, uri) 104 93 } ··· 124 113 values %s 125 114 ) 126 115 select 127 - p.owner_did, p.rkey, 128 - c.comment_at 116 + p.owner_did, p.rkey, c.at_uri 129 117 from input inp 130 118 join repos r 131 119 on r.did = inp.owner_did ··· 133 121 join pulls p 134 122 on p.repo_at = r.at_uri 135 123 and p.pull_id = inp.pull_id 136 - left join pull_comments c 124 + left join comments c 137 125 on inp.comment_id is not null 138 - and c.repo_at = r.at_uri and c.pull_id = p.pull_id 126 + and c.subject_at = ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey) 139 127 and c.id = inp.comment_id 140 128 `, 141 129 strings.Join(vals, ","), ··· 283 271 return nil, fmt.Errorf("get issue backlinks: %w", err) 284 272 } 285 273 backlinks = append(backlinks, ls...) 286 - ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID]) 274 + ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.CommentNSID]) 287 275 if err != nil { 288 276 return nil, fmt.Errorf("get issue_comment backlinks: %w", err) 289 277 } ··· 293 281 return nil, fmt.Errorf("get pull backlinks: %w", err) 294 282 } 295 283 backlinks = append(backlinks, ls...) 296 - ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.RepoPullCommentNSID]) 284 + ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.CommentNSID]) 297 285 if err != nil { 298 286 return nil, fmt.Errorf("get pull_comment backlinks: %w", err) 299 287 } ··· 352 340 rows, err := e.Query( 353 341 fmt.Sprintf( 354 342 `select r.did, r.name, i.issue_id, c.id, i.title, i.open 355 - from issue_comments c 343 + from comments c 356 344 join issues i 357 - on i.at_uri = c.issue_at 345 + on i.at_uri = c.subject_at 358 346 join repos r 359 347 on r.at_uri = i.repo_at 360 348 where %s`, ··· 428 416 if len(aturis) == 0 { 429 417 return nil, nil 430 418 } 431 - filter := orm.FilterIn("c.comment_at", aturis) 419 + filter := orm.FilterIn("c.at_uri", aturis) 432 420 rows, err := e.Query( 433 421 fmt.Sprintf( 434 422 `select r.did, r.name, p.pull_id, c.id, p.title, p.state 435 423 from repos r 436 424 join pulls p 437 425 on r.at_uri = p.repo_at 438 - join pull_comments c 439 - on r.at_uri = c.repo_at and p.pull_id = c.pull_id 426 + join comments c 427 + on ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey) = c.subject_at 440 428 where %s`, 441 429 filter.Condition(), 442 430 ),
+5 -4
appview/db/timeline.go
··· 1 1 package db 2 2 3 3 import ( 4 + "fmt" 4 5 "sort" 5 6 6 7 "github.com/bluesky-social/indigo/atproto/syntax" ··· 17 18 if limitToUsersIsFollowing { 18 19 following, err := GetFollowing(e, loggedInUserDid) 19 20 if err != nil { 20 - return nil, err 21 + return nil, fmt.Errorf("getting followings: %w", err) 21 22 } 22 23 23 24 userIsFollowing = make([]string, 0, len(following)) ··· 28 29 29 30 repos, err := getTimelineRepos(e, limit, loggedInUserDid, userIsFollowing) 30 31 if err != nil { 31 - return nil, err 32 + return nil, fmt.Errorf("getting repos: %w", err) 32 33 } 33 34 34 35 stars, err := getTimelineStars(e, limit, loggedInUserDid, userIsFollowing) 35 36 if err != nil { 36 - return nil, err 37 + return nil, fmt.Errorf("getting stars: %w", err) 37 38 } 38 39 39 40 follows, err := getTimelineFollows(e, limit, loggedInUserDid, userIsFollowing) 40 41 if err != nil { 41 - return nil, err 42 + return nil, fmt.Errorf("getting follows: %w", err) 42 43 } 43 44 44 45 events = append(events, repos...)
+86 -6
appview/ingester.go
··· 79 79 err = i.ingestString(e) 80 80 case tangled.RepoIssueNSID: 81 81 err = i.ingestIssue(ctx, e) 82 + case tangled.CommentNSID: 83 + err = i.ingestComment(e) 82 84 case tangled.RepoIssueCommentNSID: 83 85 err = i.ingestIssueComment(e) 84 86 case tangled.LabelDefinitionNSID: ··· 889 891 } 890 892 891 893 switch e.Commit.Operation { 892 - case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: 894 + case jmodels.CommitOperationUpdate: 893 895 raw := json.RawMessage(e.Commit.Record) 894 896 record := tangled.RepoIssueComment{} 895 897 err = json.Unmarshal(raw, &record) ··· 897 899 return fmt.Errorf("invalid record: %w", err) 898 900 } 899 901 900 - comment, err := models.IssueCommentFromRecord(did, rkey, record) 902 + // convert 'sh.tangled.repo.issue.comment' to 'sh.tangled.comment' 903 + comment, err := models.CommentFromRecord(syntax.DID(did), syntax.RecordKey(rkey), tangled.Comment{ 904 + Body: record.Body, 905 + CreatedAt: record.CreatedAt, 906 + Mentions: record.Mentions, 907 + References: record.References, 908 + ReplyTo: record.ReplyTo, 909 + Subject: record.Issue, 910 + }) 901 911 if err != nil { 902 912 return fmt.Errorf("failed to parse comment from record: %w", err) 903 913 } 904 914 905 - if err := i.Validator.ValidateIssueComment(comment); err != nil { 915 + if err := comment.Validate(); err != nil { 906 916 return fmt.Errorf("failed to validate comment: %w", err) 907 917 } 908 918 ··· 912 922 } 913 923 defer tx.Rollback() 914 924 915 - _, err = db.AddIssueComment(tx, *comment) 925 + err = db.PutComment(tx, comment) 916 926 if err != nil { 917 - return fmt.Errorf("failed to create issue comment: %w", err) 927 + return fmt.Errorf("failed to create comment: %w", err) 918 928 } 919 929 920 930 return tx.Commit() 921 931 922 932 case jmodels.CommitOperationDelete: 923 - if err := db.DeleteIssueComments( 933 + if err := db.DeleteComments( 924 934 ddb, 925 935 orm.FilterEq("did", did), 936 + orm.FilterEq("collection", e.Commit.Collection), 926 937 orm.FilterEq("rkey", rkey), 927 938 ); err != nil { 928 939 return fmt.Errorf("failed to delete issue comment record: %w", err) 940 + } 941 + 942 + return nil 943 + } 944 + 945 + return nil 946 + } 947 + 948 + func (i *Ingester) ingestComment(e *jmodels.Event) error { 949 + did := e.Did 950 + rkey := e.Commit.RKey 951 + 952 + var err error 953 + 954 + l := i.Logger.With("handler", "ingestComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 955 + l.Info("ingesting record") 956 + 957 + ddb, ok := i.Db.Execer.(*db.DB) 958 + if !ok { 959 + return fmt.Errorf("failed to index issue comment record, invalid db cast") 960 + } 961 + 962 + switch e.Commit.Operation { 963 + case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: 964 + raw := json.RawMessage(e.Commit.Record) 965 + record := tangled.Comment{} 966 + err = json.Unmarshal(raw, &record) 967 + if err != nil { 968 + return fmt.Errorf("invalid record: %w", err) 969 + } 970 + 971 + comment, err := models.CommentFromRecord(syntax.DID(did), syntax.RecordKey(rkey), record) 972 + if err != nil { 973 + return fmt.Errorf("failed to parse comment from record: %w", err) 974 + } 975 + 976 + // TODO: ingest pull comments 977 + // we aren't ingesting pull comments yet because pull itself isn't fully atprotated. 978 + // so we cannot know which round this comment is pointing to 979 + if comment.Subject.Collection().String() == tangled.RepoPullNSID { 980 + l.Info("skip ingesting pull comments") 981 + return nil 982 + } 983 + 984 + if err := comment.Validate(); err != nil { 985 + return fmt.Errorf("failed to validate comment: %w", err) 986 + } 987 + 988 + tx, err := ddb.Begin() 989 + if err != nil { 990 + return fmt.Errorf("failed to start transaction: %w", err) 991 + } 992 + defer tx.Rollback() 993 + 994 + err = db.PutComment(tx, comment) 995 + if err != nil { 996 + return fmt.Errorf("failed to create comment: %w", err) 997 + } 998 + 999 + return tx.Commit() 1000 + 1001 + case jmodels.CommitOperationDelete: 1002 + if err := db.DeleteComments( 1003 + ddb, 1004 + orm.FilterEq("did", did), 1005 + orm.FilterEq("collection", e.Commit.Collection), 1006 + orm.FilterEq("rkey", rkey), 1007 + ); err != nil { 1008 + return fmt.Errorf("failed to delete comment record: %w", err) 929 1009 } 930 1010 931 1011 return nil
+54 -52
appview/issues/issues.go
··· 10 10 "time" 11 11 12 12 comatproto "github.com/bluesky-social/indigo/api/atproto" 13 - atpclient "github.com/bluesky-social/indigo/atproto/client" 13 + "github.com/bluesky-social/indigo/atproto/atclient" 14 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 15 lexutil "github.com/bluesky-social/indigo/lex/util" 16 16 "github.com/go-chi/chi/v5" ··· 103 103 104 104 userReactions := map[models.ReactionKind]bool{} 105 105 if user != nil { 106 - userReactions = db.GetReactionStatusMap(rp.db, user.Active.Did, issue.AtUri()) 106 + userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri()) 107 107 } 108 108 109 109 backlinks, err := db.GetBacklinks(rp.db, issue.AtUri()) ··· 182 182 return 183 183 } 184 184 185 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueNSID, user.Active.Did, newIssue.Rkey) 185 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueNSID, user.Did, newIssue.Rkey) 186 186 if err != nil { 187 187 l.Error("failed to get record", "err", err) 188 188 rp.pages.Notice(w, noticeId, "Failed to edit issue, no record found on PDS.") ··· 191 191 192 192 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 193 193 Collection: tangled.RepoIssueNSID, 194 - Repo: user.Active.Did, 194 + Repo: user.Did, 195 195 Rkey: newIssue.Rkey, 196 196 SwapRecord: ex.Cid, 197 197 Record: &lexutil.LexiconTypeDecoder{ ··· 306 306 return 307 307 } 308 308 309 - roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 309 + roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 310 310 isRepoOwner := roles.IsOwner() 311 311 isCollaborator := roles.IsCollaborator() 312 - isIssueOwner := user.Active.Did == issue.Did 312 + isIssueOwner := user.Did == issue.Did 313 313 314 314 // TODO: make this more granular 315 315 if isIssueOwner || isRepoOwner || isCollaborator { ··· 326 326 issue.Open = false 327 327 328 328 // notify about the issue closure 329 - rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Active.Did), issue) 329 + rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue) 330 330 331 331 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 332 332 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) ··· 354 354 return 355 355 } 356 356 357 - roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 357 + roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 358 358 isRepoOwner := roles.IsOwner() 359 359 isCollaborator := roles.IsCollaborator() 360 - isIssueOwner := user.Active.Did == issue.Did 360 + isIssueOwner := user.Did == issue.Did 361 361 362 362 if isCollaborator || isRepoOwner || isIssueOwner { 363 363 err := db.ReopenIssues( ··· 373 373 issue.Open = true 374 374 375 375 // notify about the issue reopen 376 - rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Active.Did), issue) 376 + rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue) 377 377 378 378 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 379 379 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) ··· 403 403 404 404 body := r.FormValue("body") 405 405 if body == "" { 406 - rp.pages.Notice(w, "issue", "Body is required") 406 + rp.pages.Notice(w, "issue-comment", "Body is required") 407 407 return 408 408 } 409 409 410 - replyToUri := r.FormValue("reply-to") 411 - var replyTo *string 412 - if replyToUri != "" { 413 - replyTo = &replyToUri 410 + var replyTo *syntax.ATURI 411 + replyToRaw := r.FormValue("reply-to") 412 + if replyToRaw != "" { 413 + aturi, err := syntax.ParseATURI(replyToRaw) 414 + if err != nil { 415 + rp.pages.Notice(w, "issue-comment", "reply-to should be valid AT-URI") 416 + return 417 + } 418 + replyTo = &aturi 414 419 } 415 420 416 421 mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 417 422 418 - comment := models.IssueComment{ 419 - Did: user.Active.Did, 423 + comment := models.Comment{ 424 + Did: syntax.DID(user.Did), 425 + Collection: tangled.CommentNSID, 420 426 Rkey: tid.TID(), 421 - IssueAt: issue.AtUri().String(), 427 + Subject: issue.AtUri(), 422 428 ReplyTo: replyTo, 423 429 Body: body, 424 430 Created: time.Now(), 425 431 Mentions: mentions, 426 432 References: references, 427 433 } 428 - if err = rp.validator.ValidateIssueComment(&comment); err != nil { 434 + if err = comment.Validate(); err != nil { 429 435 l.Error("failed to validate comment", "err", err) 430 436 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 431 437 return 432 438 } 433 - record := comment.AsRecord() 434 439 435 440 client, err := rp.oauth.AuthorizedClient(r) 436 441 if err != nil { ··· 441 446 442 447 // create a record first 443 448 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 444 - Collection: tangled.RepoIssueCommentNSID, 445 - Repo: comment.Did, 449 + Collection: comment.Collection.String(), 450 + Repo: comment.Did.String(), 446 451 Rkey: comment.Rkey, 447 452 Record: &lexutil.LexiconTypeDecoder{ 448 - Val: &record, 453 + Val: comment.AsRecord(), 449 454 }, 450 455 }) 451 456 if err != nil { ··· 468 473 } 469 474 defer tx.Rollback() 470 475 471 - commentId, err := db.AddIssueComment(tx, comment) 476 + err = db.PutComment(tx, &comment) 472 477 if err != nil { 473 478 l.Error("failed to create comment", "err", err) 474 479 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") ··· 484 489 // reset atUri to make rollback a no-op 485 490 atUri = "" 486 491 487 - // notify about the new comment 488 - comment.Id = commentId 489 - 490 - rp.notifier.NewIssueComment(r.Context(), &comment, mentions) 492 + rp.notifier.NewComment(r.Context(), &comment) 491 493 492 494 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 493 - rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId)) 495 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, comment.Id)) 494 496 } 495 497 496 498 func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { ··· 505 507 } 506 508 507 509 commentId := chi.URLParam(r, "commentId") 508 - comments, err := db.GetIssueComments( 510 + comments, err := db.GetComments( 509 511 rp.db, 510 512 orm.FilterEq("id", commentId), 511 513 ) ··· 541 543 } 542 544 543 545 commentId := chi.URLParam(r, "commentId") 544 - comments, err := db.GetIssueComments( 546 + comments, err := db.GetComments( 545 547 rp.db, 546 548 orm.FilterEq("id", commentId), 547 549 ) ··· 557 559 } 558 560 comment := comments[0] 559 561 560 - if comment.Did != user.Active.Did { 561 - l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Active.Did) 562 + if comment.Did.String() != user.Did { 563 + l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did) 562 564 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 563 565 return 564 566 } ··· 587 589 newComment.Edited = &now 588 590 newComment.Mentions, newComment.References = rp.mentionsResolver.Resolve(r.Context(), newBody) 589 591 590 - record := newComment.AsRecord() 591 - 592 592 tx, err := rp.db.Begin() 593 593 if err != nil { 594 594 l.Error("failed to start transaction", "err", err) ··· 597 597 } 598 598 defer tx.Rollback() 599 599 600 - _, err = db.AddIssueComment(tx, newComment) 600 + err = db.PutComment(tx, &newComment) 601 601 if err != nil { 602 602 l.Error("failed to perferom update-description query", "err", err) 603 603 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") ··· 607 607 608 608 // rkey is optional, it was introduced later 609 609 if newComment.Rkey != "" { 610 + // TODO: update correct comment 611 + 610 612 // update the record on pds 611 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Active.Did, comment.Rkey) 613 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", newComment.Collection.String(), newComment.Did.String(), newComment.Rkey) 612 614 if err != nil { 613 615 l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 614 - rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") 616 + rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update comment, no record found on PDS.") 615 617 return 616 618 } 617 619 618 620 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 619 - Collection: tangled.RepoIssueCommentNSID, 620 - Repo: user.Active.Did, 621 + Collection: newComment.Collection.String(), 622 + Repo: newComment.Did.String(), 621 623 Rkey: newComment.Rkey, 622 624 SwapRecord: ex.Cid, 623 625 Record: &lexutil.LexiconTypeDecoder{ 624 - Val: &record, 626 + Val: newComment.AsRecord(), 625 627 }, 626 628 }) 627 629 if err != nil { ··· 651 653 } 652 654 653 655 commentId := chi.URLParam(r, "commentId") 654 - comments, err := db.GetIssueComments( 656 + comments, err := db.GetComments( 655 657 rp.db, 656 658 orm.FilterEq("id", commentId), 657 659 ) ··· 687 689 } 688 690 689 691 commentId := chi.URLParam(r, "commentId") 690 - comments, err := db.GetIssueComments( 692 + comments, err := db.GetComments( 691 693 rp.db, 692 694 orm.FilterEq("id", commentId), 693 695 ) ··· 723 725 } 724 726 725 727 commentId := chi.URLParam(r, "commentId") 726 - comments, err := db.GetIssueComments( 728 + comments, err := db.GetComments( 727 729 rp.db, 728 730 orm.FilterEq("id", commentId), 729 731 ) ··· 739 741 } 740 742 comment := comments[0] 741 743 742 - if comment.Did != user.Active.Did { 743 - l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Active.Did) 744 + if comment.Did.String() != user.Did { 745 + l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did) 744 746 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 745 747 return 746 748 } ··· 752 754 753 755 // optimistic deletion 754 756 deleted := time.Now() 755 - err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id)) 757 + err = db.DeleteComments(rp.db, orm.FilterEq("id", comment.Id)) 756 758 if err != nil { 757 759 l.Error("failed to delete comment", "err", err) 758 760 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") ··· 768 770 return 769 771 } 770 772 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 771 - Collection: tangled.RepoIssueCommentNSID, 772 - Repo: user.Active.Did, 773 + Collection: comment.Collection.String(), 774 + Repo: comment.Did.String(), 773 775 Rkey: comment.Rkey, 774 776 }) 775 777 if err != nil { ··· 1015 1017 Title: r.FormValue("title"), 1016 1018 Body: body, 1017 1019 Open: true, 1018 - Did: user.Active.Did, 1020 + Did: user.Did, 1019 1021 Created: time.Now(), 1020 1022 Mentions: mentions, 1021 1023 References: references, ··· 1039 1041 } 1040 1042 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1041 1043 Collection: tangled.RepoIssueNSID, 1042 - Repo: user.Active.Did, 1044 + Repo: user.Did, 1043 1045 Rkey: issue.Rkey, 1044 1046 Record: &lexutil.LexiconTypeDecoder{ 1045 1047 Val: &record, ··· 1098 1100 // this is used to rollback changes made to the PDS 1099 1101 // 1100 1102 // it is a no-op if the provided ATURI is empty 1101 - func rollbackRecord(ctx context.Context, aturi string, client *atpclient.APIClient) error { 1103 + func rollbackRecord(ctx context.Context, aturi string, client *atclient.APIClient) error { 1102 1104 if aturi == "" { 1103 1105 return nil 1104 1106 }
+24 -24
appview/knots/knots.go
··· 60 60 user := k.OAuth.GetMultiAccountUser(r) 61 61 registrations, err := db.GetRegistrations( 62 62 k.Db, 63 - orm.FilterEq("did", user.Active.Did), 63 + orm.FilterEq("did", user.Did), 64 64 ) 65 65 if err != nil { 66 66 k.Logger.Error("failed to fetch knot registrations", "err", err) ··· 78 78 l := k.Logger.With("handler", "dashboard") 79 79 80 80 user := k.OAuth.GetMultiAccountUser(r) 81 - l = l.With("user", user.Active.Did) 81 + l = l.With("user", user.Did) 82 82 83 83 domain := chi.URLParam(r, "domain") 84 84 if domain == "" { ··· 88 88 89 89 registrations, err := db.GetRegistrations( 90 90 k.Db, 91 - orm.FilterEq("did", user.Active.Did), 91 + orm.FilterEq("did", user.Did), 92 92 orm.FilterEq("domain", domain), 93 93 ) 94 94 if err != nil { ··· 158 158 return 159 159 } 160 160 l = l.With("domain", domain) 161 - l = l.With("user", user.Active.Did) 161 + l = l.With("user", user.Did) 162 162 163 163 tx, err := k.Db.Begin() 164 164 if err != nil { ··· 171 171 k.Enforcer.E.LoadPolicy() 172 172 }() 173 173 174 - err = db.AddKnot(tx, domain, user.Active.Did) 174 + err = db.AddKnot(tx, domain, user.Did) 175 175 if err != nil { 176 176 l.Error("failed to insert", "err", err) 177 177 fail() ··· 193 193 return 194 194 } 195 195 196 - ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Active.Did, domain) 196 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Did, domain) 197 197 var exCid *string 198 198 if ex != nil { 199 199 exCid = ex.Cid ··· 202 202 // re-announce by registering under same rkey 203 203 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 204 204 Collection: tangled.KnotNSID, 205 - Repo: user.Active.Did, 205 + Repo: user.Did, 206 206 Rkey: domain, 207 207 Record: &lexutil.LexiconTypeDecoder{ 208 208 Val: &tangled.Knot{ ··· 233 233 } 234 234 235 235 // begin verification 236 - err = serververify.RunVerification(r.Context(), domain, user.Active.Did, k.Config.Core.Dev) 236 + err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev) 237 237 if err != nil { 238 238 l.Error("verification failed", "err", err) 239 239 k.Pages.HxRefresh(w) 240 240 return 241 241 } 242 242 243 - err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Active.Did) 243 + err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did) 244 244 if err != nil { 245 245 l.Error("failed to mark verified", "err", err) 246 246 k.Pages.HxRefresh(w) ··· 277 277 // get record from db first 278 278 registrations, err := db.GetRegistrations( 279 279 k.Db, 280 - orm.FilterEq("did", user.Active.Did), 280 + orm.FilterEq("did", user.Did), 281 281 orm.FilterEq("domain", domain), 282 282 ) 283 283 if err != nil { ··· 305 305 306 306 err = db.DeleteKnot( 307 307 tx, 308 - orm.FilterEq("did", user.Active.Did), 308 + orm.FilterEq("did", user.Did), 309 309 orm.FilterEq("domain", domain), 310 310 ) 311 311 if err != nil { ··· 333 333 334 334 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 335 335 Collection: tangled.KnotNSID, 336 - Repo: user.Active.Did, 336 + Repo: user.Did, 337 337 Rkey: domain, 338 338 }) 339 339 if err != nil { ··· 381 381 return 382 382 } 383 383 l = l.With("domain", domain) 384 - l = l.With("user", user.Active.Did) 384 + l = l.With("user", user.Did) 385 385 386 386 // get record from db first 387 387 registrations, err := db.GetRegistrations( 388 388 k.Db, 389 - orm.FilterEq("did", user.Active.Did), 389 + orm.FilterEq("did", user.Did), 390 390 orm.FilterEq("domain", domain), 391 391 ) 392 392 if err != nil { ··· 402 402 registration := registrations[0] 403 403 404 404 // begin verification 405 - err = serververify.RunVerification(r.Context(), domain, user.Active.Did, k.Config.Core.Dev) 405 + err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev) 406 406 if err != nil { 407 407 l.Error("verification failed", "err", err) 408 408 ··· 420 420 return 421 421 } 422 422 423 - err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Active.Did) 423 + err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did) 424 424 if err != nil { 425 425 l.Error("failed to mark verified", "err", err) 426 426 k.Pages.Notice(w, noticeId, err.Error()) ··· 439 439 return 440 440 } 441 441 442 - ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Active.Did, domain) 442 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Did, domain) 443 443 var exCid *string 444 444 if ex != nil { 445 445 exCid = ex.Cid ··· 448 448 // ignore the error here 449 449 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 450 450 Collection: tangled.KnotNSID, 451 - Repo: user.Active.Did, 451 + Repo: user.Did, 452 452 Rkey: domain, 453 453 Record: &lexutil.LexiconTypeDecoder{ 454 454 Val: &tangled.Knot{ ··· 477 477 // Get updated registration to show 478 478 registrations, err = db.GetRegistrations( 479 479 k.Db, 480 - orm.FilterEq("did", user.Active.Did), 480 + orm.FilterEq("did", user.Did), 481 481 orm.FilterEq("domain", domain), 482 482 ) 483 483 if err != nil { ··· 509 509 return 510 510 } 511 511 l = l.With("domain", domain) 512 - l = l.With("user", user.Active.Did) 512 + l = l.With("user", user.Did) 513 513 514 514 registrations, err := db.GetRegistrations( 515 515 k.Db, 516 - orm.FilterEq("did", user.Active.Did), 516 + orm.FilterEq("did", user.Did), 517 517 orm.FilterEq("domain", domain), 518 518 orm.FilterIsNot("registered", "null"), 519 519 ) ··· 566 566 567 567 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 568 568 Collection: tangled.KnotMemberNSID, 569 - Repo: user.Active.Did, 569 + Repo: user.Did, 570 570 Rkey: rkey, 571 571 Record: &lexutil.LexiconTypeDecoder{ 572 572 Val: &tangled.KnotMember{ ··· 617 617 return 618 618 } 619 619 l = l.With("domain", domain) 620 - l = l.With("user", user.Active.Did) 620 + l = l.With("user", user.Did) 621 621 622 622 registrations, err := db.GetRegistrations( 623 623 k.Db, 624 - orm.FilterEq("did", user.Active.Did), 624 + orm.FilterEq("did", user.Did), 625 625 orm.FilterEq("domain", domain), 626 626 orm.FilterIsNot("registered", "null"), 627 627 )
+3 -3
appview/labels/labels.go
··· 22 22 "tangled.org/core/tid" 23 23 24 24 comatproto "github.com/bluesky-social/indigo/api/atproto" 25 - atpclient "github.com/bluesky-social/indigo/atproto/client" 25 + "github.com/bluesky-social/indigo/atproto/atclient" 26 26 "github.com/bluesky-social/indigo/atproto/syntax" 27 27 lexutil "github.com/bluesky-social/indigo/lex/util" 28 28 "github.com/go-chi/chi/v5" ··· 86 86 return 87 87 } 88 88 89 - did := user.Active.Did 89 + did := user.Did 90 90 rkey := tid.TID() 91 91 performedAt := time.Now() 92 92 indexedAt := time.Now() ··· 269 269 // this is used to rollback changes made to the PDS 270 270 // 271 271 // it is a no-op if the provided ATURI is empty 272 - func rollbackRecord(ctx context.Context, aturi string, client *atpclient.APIClient) error { 272 + func rollbackRecord(ctx context.Context, aturi string, client *atclient.APIClient) error { 273 273 if aturi == "" { 274 274 return nil 275 275 }
+4 -4
appview/middleware/middleware.go
··· 128 128 return 129 129 } 130 130 131 - ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Active.Did, group, domain) 131 + ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Did, group, domain) 132 132 if err != nil || !ok { 133 - log.Printf("%s does not have perms of a %s in domain %s", actor.Active.Did, group, domain) 133 + log.Printf("%s does not have perms of a %s in domain %s", actor.Did, group, domain) 134 134 http.Error(w, "Forbiden", http.StatusUnauthorized) 135 135 return 136 136 } ··· 161 161 return 162 162 } 163 163 164 - ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.DidSlashRepo(), requiredPerm) 164 + ok, err := mw.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm) 165 165 if err != nil || !ok { 166 - log.Printf("%s does not have perms of a %s in repo %s", actor.Active.Did, requiredPerm, f.DidSlashRepo()) 166 + log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.DidSlashRepo()) 167 167 http.Error(w, "Forbiden", http.StatusUnauthorized) 168 168 return 169 169 }
+138
appview/models/comment.go
··· 1 + package models 2 + 3 + import ( 4 + "fmt" 5 + "strings" 6 + "time" 7 + 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + "github.com/whyrusleeping/cbor-gen" 10 + "tangled.org/core/api/tangled" 11 + ) 12 + 13 + type Comment struct { 14 + Id int64 15 + Did syntax.DID 16 + Collection syntax.NSID 17 + Rkey string 18 + Subject syntax.ATURI 19 + ReplyTo *syntax.ATURI 20 + Body string 21 + Created time.Time 22 + Edited *time.Time 23 + Deleted *time.Time 24 + Mentions []syntax.DID 25 + References []syntax.ATURI 26 + PullSubmissionId *int 27 + } 28 + 29 + func (c *Comment) AtUri() syntax.ATURI { 30 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", c.Did, c.Collection, c.Rkey)) 31 + } 32 + 33 + func (c *Comment) AsRecord() typegen.CBORMarshaler { 34 + mentions := make([]string, len(c.Mentions)) 35 + for i, did := range c.Mentions { 36 + mentions[i] = string(did) 37 + } 38 + references := make([]string, len(c.References)) 39 + for i, uri := range c.References { 40 + references[i] = string(uri) 41 + } 42 + var replyTo *string 43 + if c.ReplyTo != nil { 44 + replyToStr := c.ReplyTo.String() 45 + replyTo = &replyToStr 46 + } 47 + switch c.Collection { 48 + case tangled.RepoIssueCommentNSID: 49 + return &tangled.RepoIssueComment{ 50 + Issue: c.Subject.String(), 51 + Body: c.Body, 52 + CreatedAt: c.Created.Format(time.RFC3339), 53 + ReplyTo: replyTo, 54 + Mentions: mentions, 55 + References: references, 56 + } 57 + case tangled.RepoPullCommentNSID: 58 + return &tangled.RepoPullComment{ 59 + Pull: c.Subject.String(), 60 + Body: c.Body, 61 + CreatedAt: c.Created.Format(time.RFC3339), 62 + Mentions: mentions, 63 + References: references, 64 + } 65 + default: // default to CommentNSID 66 + return &tangled.Comment{ 67 + Subject: c.Subject.String(), 68 + Body: c.Body, 69 + CreatedAt: c.Created.Format(time.RFC3339), 70 + ReplyTo: replyTo, 71 + Mentions: mentions, 72 + References: references, 73 + } 74 + } 75 + } 76 + 77 + func (c *Comment) IsTopLevel() bool { 78 + return c.ReplyTo == nil 79 + } 80 + 81 + func (c *Comment) IsReply() bool { 82 + return c.ReplyTo != nil 83 + } 84 + 85 + func (c *Comment) Validate() error { 86 + // TODO: sanitize the body and then trim space 87 + if sb := strings.TrimSpace(c.Body); sb == "" { 88 + return fmt.Errorf("body is empty after HTML sanitization") 89 + } 90 + 91 + // if it's for PR, PullSubmissionId should not be nil 92 + if c.Subject.Collection().String() == tangled.RepoPullNSID { 93 + if c.PullSubmissionId == nil { 94 + return fmt.Errorf("PullSubmissionId should not be nil") 95 + } 96 + } 97 + return nil 98 + } 99 + 100 + func CommentFromRecord(did syntax.DID, rkey syntax.RecordKey, record tangled.Comment) (*Comment, error) { 101 + created, err := time.Parse(time.RFC3339, record.CreatedAt) 102 + if err != nil { 103 + created = time.Now() 104 + } 105 + 106 + if _, err = syntax.ParseATURI(record.Subject); err != nil { 107 + return nil, err 108 + } 109 + 110 + i := record 111 + mentions := make([]syntax.DID, len(record.Mentions)) 112 + for i, did := range record.Mentions { 113 + mentions[i] = syntax.DID(did) 114 + } 115 + references := make([]syntax.ATURI, len(record.References)) 116 + for i, uri := range i.References { 117 + references[i] = syntax.ATURI(uri) 118 + } 119 + var replyTo *syntax.ATURI 120 + if record.ReplyTo != nil { 121 + replyToAtUri := syntax.ATURI(*record.ReplyTo) 122 + replyTo = &replyToAtUri 123 + } 124 + 125 + comment := Comment{ 126 + Did: did, 127 + Collection: tangled.CommentNSID, 128 + Rkey: rkey.String(), 129 + Body: record.Body, 130 + Subject: syntax.ATURI(record.Subject), 131 + ReplyTo: replyTo, 132 + Created: created, 133 + Mentions: mentions, 134 + References: references, 135 + } 136 + 137 + return &comment, nil 138 + }
+8 -89
appview/models/issue.go
··· 26 26 27 27 // optionally, populate this when querying for reverse mappings 28 28 // like comment counts, parent repo etc. 29 - Comments []IssueComment 29 + Comments []Comment 30 30 Labels LabelState 31 31 Repo *Repo 32 32 } ··· 62 62 } 63 63 64 64 type CommentListItem struct { 65 - Self *IssueComment 66 - Replies []*IssueComment 65 + Self *Comment 66 + Replies []*Comment 67 67 } 68 68 69 69 func (it *CommentListItem) Participants() []syntax.DID { ··· 88 88 89 89 func (i *Issue) CommentList() []CommentListItem { 90 90 // Create a map to quickly find comments by their aturi 91 - toplevel := make(map[string]*CommentListItem) 92 - var replies []*IssueComment 91 + toplevel := make(map[syntax.ATURI]*CommentListItem) 92 + var replies []*Comment 93 93 94 94 // collect top level comments into the map 95 95 for _, comment := range i.Comments { 96 96 if comment.IsTopLevel() { 97 - toplevel[comment.AtUri().String()] = &CommentListItem{ 97 + toplevel[comment.AtUri()] = &CommentListItem{ 98 98 Self: &comment, 99 99 } 100 100 } else { ··· 115 115 } 116 116 117 117 // sort everything 118 - sortFunc := func(a, b *IssueComment) bool { 118 + sortFunc := func(a, b *Comment) bool { 119 119 return a.Created.Before(b.Created) 120 120 } 121 121 sort.Slice(listing, func(i, j int) bool { ··· 144 144 addParticipant(i.Did) 145 145 146 146 for _, c := range i.Comments { 147 - addParticipant(c.Did) 147 + addParticipant(c.Did.String()) 148 148 } 149 149 150 150 return participants ··· 171 171 Open: true, // new issues are open by default 172 172 } 173 173 } 174 - 175 - type IssueComment struct { 176 - Id int64 177 - Did string 178 - Rkey string 179 - IssueAt string 180 - ReplyTo *string 181 - Body string 182 - Created time.Time 183 - Edited *time.Time 184 - Deleted *time.Time 185 - Mentions []syntax.DID 186 - References []syntax.ATURI 187 - } 188 - 189 - func (i *IssueComment) AtUri() syntax.ATURI { 190 - return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueCommentNSID, i.Rkey)) 191 - } 192 - 193 - func (i *IssueComment) AsRecord() tangled.RepoIssueComment { 194 - mentions := make([]string, len(i.Mentions)) 195 - for i, did := range i.Mentions { 196 - mentions[i] = string(did) 197 - } 198 - references := make([]string, len(i.References)) 199 - for i, uri := range i.References { 200 - references[i] = string(uri) 201 - } 202 - return tangled.RepoIssueComment{ 203 - Body: i.Body, 204 - Issue: i.IssueAt, 205 - CreatedAt: i.Created.Format(time.RFC3339), 206 - ReplyTo: i.ReplyTo, 207 - Mentions: mentions, 208 - References: references, 209 - } 210 - } 211 - 212 - func (i *IssueComment) IsTopLevel() bool { 213 - return i.ReplyTo == nil 214 - } 215 - 216 - func (i *IssueComment) IsReply() bool { 217 - return i.ReplyTo != nil 218 - } 219 - 220 - func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) { 221 - created, err := time.Parse(time.RFC3339, record.CreatedAt) 222 - if err != nil { 223 - created = time.Now() 224 - } 225 - 226 - ownerDid := did 227 - 228 - if _, err = syntax.ParseATURI(record.Issue); err != nil { 229 - return nil, err 230 - } 231 - 232 - i := record 233 - mentions := make([]syntax.DID, len(record.Mentions)) 234 - for i, did := range record.Mentions { 235 - mentions[i] = syntax.DID(did) 236 - } 237 - references := make([]syntax.ATURI, len(record.References)) 238 - for i, uri := range i.References { 239 - references[i] = syntax.ATURI(uri) 240 - } 241 - 242 - comment := IssueComment{ 243 - Did: ownerDid, 244 - Rkey: rkey, 245 - Body: record.Body, 246 - IssueAt: record.Issue, 247 - ReplyTo: record.ReplyTo, 248 - Created: created, 249 - Mentions: mentions, 250 - References: references, 251 - } 252 - 253 - return &comment, nil 254 - }
+2 -28
appview/models/pull.go
··· 138 138 RoundNumber int 139 139 Patch string 140 140 Combined string 141 - Comments []PullComment 141 + Comments []Comment 142 142 SourceRev string // include the rev that was used to create this submission: only for branch/fork PRs 143 143 144 144 // meta 145 145 Created time.Time 146 - } 147 - 148 - type PullComment struct { 149 - // ids 150 - ID int 151 - PullId int 152 - SubmissionId int 153 - 154 - // at ids 155 - RepoAt string 156 - OwnerDid string 157 - CommentAt string 158 - 159 - // content 160 - Body string 161 - 162 - // meta 163 - Mentions []syntax.DID 164 - References []syntax.ATURI 165 - 166 - // meta 167 - Created time.Time 168 - } 169 - 170 - func (p *PullComment) AtUri() syntax.ATURI { 171 - return syntax.ATURI(p.CommentAt) 172 146 } 173 147 174 148 func (p *Pull) TotalComments() int { ··· 279 253 addParticipant(s.PullAt.Authority().String()) 280 254 281 255 for _, c := range s.Comments { 282 - addParticipant(c.OwnerDid) 256 + addParticipant(c.Did.String()) 283 257 } 284 258 285 259 return participants
+4 -4
appview/notifications/notifications.go
··· 54 54 55 55 total, err := db.CountNotifications( 56 56 n.db, 57 - orm.FilterEq("recipient_did", user.Active.Did), 57 + orm.FilterEq("recipient_did", user.Did), 58 58 ) 59 59 if err != nil { 60 60 l.Error("failed to get total notifications", "err", err) ··· 65 65 notifications, err := db.GetNotificationsWithEntities( 66 66 n.db, 67 67 page, 68 - orm.FilterEq("recipient_did", user.Active.Did), 68 + orm.FilterEq("recipient_did", user.Did), 69 69 ) 70 70 if err != nil { 71 71 l.Error("failed to get notifications", "err", err) ··· 73 73 return 74 74 } 75 75 76 - err = db.MarkAllNotificationsRead(n.db, user.Active.Did) 76 + err = db.MarkAllNotificationsRead(n.db, user.Did) 77 77 if err != nil { 78 78 l.Error("failed to mark notifications as read", "err", err) 79 79 } ··· 98 98 99 99 count, err := db.CountNotifications( 100 100 n.db, 101 - orm.FilterEq("recipient_did", user.Active.Did), 101 + orm.FilterEq("recipient_did", user.Did), 102 102 orm.FilterEq("read", 0), 103 103 ) 104 104 if err != nil {
+110 -117
appview/notify/db/db.go
··· 77 77 // no-op 78 78 } 79 79 80 - func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 80 + func (n *databaseNotifier) NewComment(ctx context.Context, comment *models.Comment) { 81 81 l := log.FromContext(ctx) 82 82 83 - collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 83 + var ( 84 + // built the recipients list: 85 + // - the owner of the repo 86 + // - | if the comment is a reply -> everybody on that thread 87 + // | if the comment is a top level -> just the issue owner 88 + // - remove mentioned users from the recipients list 89 + recipients = sets.New[syntax.DID]() 90 + entityType string 91 + entityId string 92 + repoId *int64 93 + issueId *int64 94 + pullId *int64 95 + ) 96 + 97 + subjectDid, err := comment.Subject.Authority().AsDID() 84 98 if err != nil { 85 - l.Error("failed to fetch collaborators", "err", err) 99 + l.Error("expected did based at-uri for comment.subject") 86 100 return 87 101 } 102 + switch comment.Subject.Collection() { 103 + case tangled.RepoIssueNSID: 104 + issues, err := db.GetIssues( 105 + n.db, 106 + orm.FilterEq("did", subjectDid), 107 + orm.FilterEq("rkey", comment.Subject.RecordKey()), 108 + ) 109 + if err != nil { 110 + l.Error("failed to get issues", "err", err) 111 + return 112 + } 113 + if len(issues) == 0 { 114 + l.Error("no issue found", "subject", comment.Subject) 115 + return 116 + } 117 + issue := issues[0] 88 118 89 - // build the recipients list 90 - // - owner of the repo 91 - // - collaborators in the repo 92 - // - remove users already mentioned 93 - recipients := sets.Singleton(syntax.DID(issue.Repo.Did)) 94 - for _, c := range collaborators { 95 - recipients.Insert(c.SubjectDid) 119 + recipients.Insert(syntax.DID(issue.Repo.Did)) 120 + if comment.IsReply() { 121 + // if this comment is a reply, then notify everybody in that thread 122 + parentAtUri := *comment.ReplyTo 123 + 124 + // find the parent thread, and add all DIDs from here to the recipient list 125 + for _, t := range issue.CommentList() { 126 + if t.Self.AtUri() == parentAtUri { 127 + for _, p := range t.Participants() { 128 + recipients.Insert(p) 129 + } 130 + } 131 + } 132 + } else { 133 + // not a reply, notify just the issue author 134 + recipients.Insert(syntax.DID(issue.Did)) 135 + } 136 + 137 + entityType = "issue" 138 + entityId = issue.AtUri().String() 139 + repoId = &issue.Repo.Id 140 + issueId = &issue.Id 141 + case tangled.RepoPullNSID: 142 + pulls, err := db.GetPulls( 143 + n.db, 144 + orm.FilterEq("owner_did", subjectDid), 145 + orm.FilterEq("rkey", comment.Subject.RecordKey()), 146 + ) 147 + if err != nil { 148 + l.Error("NewComment: failed to get pulls", "err", err) 149 + return 150 + } 151 + if len(pulls) == 0 { 152 + l.Error("NewComment: no pull found", "aturi", comment.Subject) 153 + return 154 + } 155 + pull := pulls[0] 156 + 157 + pull.Repo, err = db.GetRepo(n.db, orm.FilterEq("at_uri", pull.RepoAt)) 158 + if err != nil { 159 + l.Error("NewComment: failed to get repo", "err", err) 160 + return 161 + } 162 + 163 + recipients.Insert(syntax.DID(pull.Repo.Did)) 164 + for _, p := range pull.Participants() { 165 + recipients.Insert(syntax.DID(p)) 166 + } 167 + 168 + entityType = "pull" 169 + entityId = pull.AtUri().String() 170 + repoId = &pull.Repo.Id 171 + p := int64(pull.ID) 172 + pullId = &p 173 + default: 174 + return // no-op 96 175 } 97 - for _, m := range mentions { 176 + 177 + for _, m := range comment.Mentions { 98 178 recipients.Remove(m) 99 179 } 100 180 101 - actorDid := syntax.DID(issue.Did) 102 - entityType := "issue" 103 - entityId := issue.AtUri().String() 104 - repoId := &issue.Repo.Id 105 - issueId := &issue.Id 106 - var pullId *int64 107 - 108 181 n.notifyEvent( 109 182 ctx, 110 - actorDid, 183 + comment.Did, 111 184 recipients, 112 - models.NotificationTypeIssueCreated, 185 + models.NotificationTypeIssueCommented, 113 186 entityType, 114 187 entityId, 115 188 repoId, ··· 118 191 ) 119 192 n.notifyEvent( 120 193 ctx, 121 - actorDid, 122 - sets.Collect(slices.Values(mentions)), 194 + comment.Did, 195 + sets.Collect(slices.Values(comment.Mentions)), 123 196 models.NotificationTypeUserMentioned, 124 197 entityType, 125 198 entityId, ··· 129 202 ) 130 203 } 131 204 132 - func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 205 + func (n *databaseNotifier) DeleteComment(ctx context.Context, comment *models.Comment) { 206 + // no-op 207 + } 208 + 209 + func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 133 210 l := log.FromContext(ctx) 134 211 135 - issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt)) 212 + collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 136 213 if err != nil { 137 - l.Error("failed to get issues", "err", err) 214 + l.Error("failed to fetch collaborators", "err", err) 138 215 return 139 216 } 140 - if len(issues) == 0 { 141 - l.Error("no issue found for", "err", comment.IssueAt) 142 - return 143 - } 144 - issue := issues[0] 145 217 146 - // built the recipients list: 147 - // - the owner of the repo 148 - // - | if the comment is a reply -> everybody on that thread 149 - // | if the comment is a top level -> just the issue owner 150 - // - remove mentioned users from the recipients list 218 + // build the recipients list 219 + // - owner of the repo 220 + // - collaborators in the repo 221 + // - remove users already mentioned 151 222 recipients := sets.Singleton(syntax.DID(issue.Repo.Did)) 152 - 153 - if comment.IsReply() { 154 - // if this comment is a reply, then notify everybody in that thread 155 - parentAtUri := *comment.ReplyTo 156 - 157 - // find the parent thread, and add all DIDs from here to the recipient list 158 - for _, t := range issue.CommentList() { 159 - if t.Self.AtUri().String() == parentAtUri { 160 - for _, p := range t.Participants() { 161 - recipients.Insert(p) 162 - } 163 - } 164 - } 165 - } else { 166 - // not a reply, notify just the issue author 167 - recipients.Insert(syntax.DID(issue.Did)) 223 + for _, c := range collaborators { 224 + recipients.Insert(c.SubjectDid) 168 225 } 169 - 170 226 for _, m := range mentions { 171 227 recipients.Remove(m) 172 228 } 173 229 174 - actorDid := syntax.DID(comment.Did) 230 + actorDid := syntax.DID(issue.Did) 175 231 entityType := "issue" 176 232 entityId := issue.AtUri().String() 177 233 repoId := &issue.Repo.Id ··· 182 238 ctx, 183 239 actorDid, 184 240 recipients, 185 - models.NotificationTypeIssueCommented, 241 + models.NotificationTypeIssueCreated, 186 242 entityType, 187 243 entityId, 188 244 repoId, ··· 270 326 actorDid, 271 327 recipients, 272 328 eventType, 273 - entityType, 274 - entityId, 275 - repoId, 276 - issueId, 277 - pullId, 278 - ) 279 - } 280 - 281 - func (n *databaseNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) { 282 - l := log.FromContext(ctx) 283 - 284 - pull, err := db.GetPull(n.db, 285 - syntax.ATURI(comment.RepoAt), 286 - comment.PullId, 287 - ) 288 - if err != nil { 289 - l.Error("failed to get pulls", "err", err) 290 - return 291 - } 292 - 293 - repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", comment.RepoAt)) 294 - if err != nil { 295 - l.Error("failed to get repos", "err", err) 296 - return 297 - } 298 - 299 - // build up the recipients list: 300 - // - repo owner 301 - // - all pull participants 302 - // - remove those already mentioned 303 - recipients := sets.Singleton(syntax.DID(repo.Did)) 304 - for _, p := range pull.Participants() { 305 - recipients.Insert(syntax.DID(p)) 306 - } 307 - for _, m := range mentions { 308 - recipients.Remove(m) 309 - } 310 - 311 - actorDid := syntax.DID(comment.OwnerDid) 312 - eventType := models.NotificationTypePullCommented 313 - entityType := "pull" 314 - entityId := pull.AtUri().String() 315 - repoId := &repo.Id 316 - var issueId *int64 317 - p := int64(pull.ID) 318 - pullId := &p 319 - 320 - n.notifyEvent( 321 - ctx, 322 - actorDid, 323 - recipients, 324 - eventType, 325 - entityType, 326 - entityId, 327 - repoId, 328 - issueId, 329 - pullId, 330 - ) 331 - n.notifyEvent( 332 - ctx, 333 - actorDid, 334 - sets.Collect(slices.Values(mentions)), 335 - models.NotificationTypeUserMentioned, 336 329 entityType, 337 330 entityId, 338 331 repoId,
+6 -7
appview/notify/logging_notifier.go
··· 44 44 l.inner.NewIssue(ctx, issue, mentions) 45 45 } 46 46 47 - func (l *loggingNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 47 + func (l *loggingNotifier) NewComment(ctx context.Context, comment *models.Comment) { 48 48 ctx = tlog.IntoContext(ctx, tlog.SubLogger(l.logger, "NewIssueComment")) 49 - l.inner.NewIssueComment(ctx, comment, mentions) 49 + l.inner.NewComment(ctx, comment) 50 + } 51 + func (l *loggingNotifier) DeleteComment(ctx context.Context, comment *models.Comment) { 52 + ctx = tlog.IntoContext(ctx, tlog.SubLogger(l.logger, "DeleteComment")) 53 + l.inner.DeleteComment(ctx, comment) 50 54 } 51 55 52 56 func (l *loggingNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) { ··· 82 86 func (l *loggingNotifier) NewPull(ctx context.Context, pull *models.Pull) { 83 87 ctx = tlog.IntoContext(ctx, tlog.SubLogger(l.logger, "NewPull")) 84 88 l.inner.NewPull(ctx, pull) 85 - } 86 - 87 - func (l *loggingNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) { 88 - ctx = tlog.IntoContext(ctx, tlog.SubLogger(l.logger, "NewPullComment")) 89 - l.inner.NewPullComment(ctx, comment, mentions) 90 89 } 91 90 92 91 func (l *loggingNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {
+8 -8
appview/notify/merged_notifier.go
··· 42 42 m.fanout(func(n Notifier) { n.DeleteStar(ctx, star) }) 43 43 } 44 44 45 - func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 46 - m.fanout(func(n Notifier) { n.NewIssue(ctx, issue, mentions) }) 45 + func (m *mergedNotifier) NewComment(ctx context.Context, comment *models.Comment) { 46 + m.fanout(func(n Notifier) { n.NewComment(ctx, comment) }) 47 47 } 48 48 49 - func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 50 - m.fanout(func(n Notifier) { n.NewIssueComment(ctx, comment, mentions) }) 49 + func (m *mergedNotifier) DeleteComment(ctx context.Context, comment *models.Comment) { 50 + m.fanout(func(n Notifier) { n.DeleteComment(ctx, comment) }) 51 + } 52 + 53 + func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 54 + m.fanout(func(n Notifier) { n.NewIssue(ctx, issue, mentions) }) 51 55 } 52 56 53 57 func (m *mergedNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) { ··· 76 80 77 81 func (m *mergedNotifier) NewPull(ctx context.Context, pull *models.Pull) { 78 82 m.fanout(func(n Notifier) { n.NewPull(ctx, pull) }) 79 - } 80 - 81 - func (m *mergedNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) { 82 - m.fanout(func(n Notifier) { n.NewPullComment(ctx, comment, mentions) }) 83 83 } 84 84 85 85 func (m *mergedNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {
+7 -7
appview/notify/notifier.go
··· 13 13 NewStar(ctx context.Context, star *models.Star) 14 14 DeleteStar(ctx context.Context, star *models.Star) 15 15 16 + NewComment(ctx context.Context, comment *models.Comment) 17 + DeleteComment(ctx context.Context, comment *models.Comment) 18 + 16 19 NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) 17 - NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) 18 20 NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) 19 21 DeleteIssue(ctx context.Context, issue *models.Issue) 20 22 ··· 22 24 DeleteFollow(ctx context.Context, follow *models.Follow) 23 25 24 26 NewPull(ctx context.Context, pull *models.Pull) 25 - NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) 26 27 NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) 27 28 28 29 NewIssueLabelOp(ctx context.Context, issue *models.Issue) ··· 47 48 func (m *BaseNotifier) NewStar(ctx context.Context, star *models.Star) {} 48 49 func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {} 49 50 51 + func (m *BaseNotifier) NewComment(ctx context.Context, comment *models.Comment) {} 52 + func (m *BaseNotifier) DeleteComment(ctx context.Context, comment *models.Comment) {} 53 + 50 54 func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {} 51 - func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 52 - } 53 55 func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {} 54 56 func (m *BaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {} 55 57 ··· 59 61 func (m *BaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {} 60 62 func (m *BaseNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {} 61 63 62 - func (m *BaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {} 63 - func (m *BaseNotifier) NewPullComment(ctx context.Context, models *models.PullComment, mentions []syntax.DID) { 64 - } 64 + func (m *BaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {} 65 65 func (m *BaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {} 66 66 67 67 func (m *BaseNotifier) UpdateProfile(ctx context.Context, profile *models.Profile) {}
+5 -20
appview/notify/posthog/notifier.go
··· 86 86 } 87 87 } 88 88 89 - func (n *posthogNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) { 90 - err := n.client.Enqueue(posthog.Capture{ 91 - DistinctId: comment.OwnerDid, 92 - Event: "new_pull_comment", 93 - Properties: posthog.Properties{ 94 - "repo_at": comment.RepoAt, 95 - "pull_id": comment.PullId, 96 - "mentions": mentions, 97 - }, 98 - }) 99 - if err != nil { 100 - log.Println("failed to enqueue posthog event:", err) 101 - } 102 - } 103 - 104 89 func (n *posthogNotifier) NewPullClosed(ctx context.Context, pull *models.Pull) { 105 90 err := n.client.Enqueue(posthog.Capture{ 106 91 DistinctId: pull.OwnerDid, ··· 180 165 } 181 166 } 182 167 183 - func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 168 + func (n *posthogNotifier) NewComment(ctx context.Context, comment *models.Comment) { 184 169 err := n.client.Enqueue(posthog.Capture{ 185 - DistinctId: comment.Did, 186 - Event: "new_issue_comment", 170 + DistinctId: comment.Did.String(), 171 + Event: "new_comment", 187 172 Properties: posthog.Properties{ 188 - "issue_at": comment.IssueAt, 189 - "mentions": mentions, 173 + "subject_at": comment.Subject, 174 + "mentions": comment.Mentions, 190 175 }, 191 176 }) 192 177 if err != nil {
+9 -46
appview/oauth/accounts.go
··· 13 13 14 14 type AccountInfo struct { 15 15 Did string `json:"did"` 16 - Handle string `json:"handle"` 17 16 SessionId string `json:"session_id"` 18 17 AddedAt int64 `json:"added_at"` 19 18 } ··· 23 22 } 24 23 25 24 type MultiAccountUser struct { 26 - Active *User 25 + Did string 27 26 Accounts []AccountInfo 28 27 } 29 28 30 - func (m *MultiAccountUser) Did() string { 31 - if m.Active == nil { 32 - return "" 33 - } 34 - return m.Active.Did 35 - } 36 - 37 - func (m *MultiAccountUser) Pds() string { 38 - if m.Active == nil { 39 - return "" 40 - } 41 - return m.Active.Pds 42 - } 43 - 44 29 func (o *OAuth) GetAccounts(r *http.Request) *AccountRegistry { 45 30 session, err := o.SessStore.Get(r, AccountsName) 46 31 if err != nil || session.IsNew { ··· 60 45 return &registry 61 46 } 62 47 63 - func (o *OAuth) SaveAccounts(w http.ResponseWriter, r *http.Request, registry *AccountRegistry) error { 48 + func (o *OAuth) saveAccounts(w http.ResponseWriter, r *http.Request, registry *AccountRegistry) error { 64 49 session, err := o.SessStore.Get(r, AccountsName) 65 50 if err != nil { 66 51 o.Logger.Warn("failed to decode existing accounts cookie, will create new", "err", err) ··· 84 69 for i, acc := range r.Accounts { 85 70 if acc.Did == did { 86 71 r.Accounts[i].SessionId = sessionId 87 - r.Accounts[i].Handle = handle 88 72 return nil 89 73 } 90 74 } ··· 95 79 96 80 r.Accounts = append(r.Accounts, AccountInfo{ 97 81 Did: did, 98 - Handle: handle, 99 82 SessionId: sessionId, 100 83 AddedAt: time.Now().Unix(), 101 84 }) ··· 121 104 return nil 122 105 } 123 106 124 - func (r *AccountRegistry) OtherAccounts(activeDid string) []AccountInfo { 125 - result := make([]AccountInfo, 0, len(r.Accounts)) 126 - for _, acc := range r.Accounts { 127 - if acc.Did != activeDid { 128 - result = append(result, acc) 129 - } 130 - } 131 - return result 132 - } 133 - 134 107 func (o *OAuth) GetMultiAccountUser(r *http.Request) *MultiAccountUser { 135 - user := o.GetUser(r) 136 - if user == nil { 108 + sess, err := o.ResumeSession(r) 109 + if err != nil { 137 110 return nil 138 111 } 139 112 140 113 registry := o.GetAccounts(r) 141 114 return &MultiAccountUser{ 142 - Active: user, 115 + Did: sess.Data.AccountDID.String(), 143 116 Accounts: registry.Accounts, 144 117 } 145 118 } 146 119 147 - type AuthReturnInfo struct { 148 - ReturnURL string 149 - AddAccount bool 150 - } 151 - 152 - func (o *OAuth) SetAuthReturn(w http.ResponseWriter, r *http.Request, returnURL string, addAccount bool) error { 120 + func (o *OAuth) SetAuthReturn(w http.ResponseWriter, r *http.Request, returnURL string) error { 153 121 session, err := o.SessStore.Get(r, AuthReturnName) 154 122 if err != nil { 155 123 return err 156 124 } 157 125 158 126 session.Values[AuthReturnURL] = returnURL 159 - session.Values[AuthAddAccount] = addAccount 160 127 session.Options.MaxAge = 60 * 30 161 128 session.Options.HttpOnly = true 162 129 session.Options.Secure = !o.Config.Core.Dev ··· 165 132 return session.Save(r, w) 166 133 } 167 134 168 - func (o *OAuth) GetAuthReturn(r *http.Request) *AuthReturnInfo { 135 + func (o *OAuth) GetAuthReturn(r *http.Request) string { 169 136 session, err := o.SessStore.Get(r, AuthReturnName) 170 137 if err != nil || session.IsNew { 171 - return &AuthReturnInfo{} 138 + return "" 172 139 } 173 140 174 141 returnURL, _ := session.Values[AuthReturnURL].(string) 175 - addAccount, _ := session.Values[AuthAddAccount].(bool) 176 142 177 - return &AuthReturnInfo{ 178 - ReturnURL: returnURL, 179 - AddAccount: addAccount, 180 - } 143 + return returnURL 181 144 } 182 145 183 146 func (o *OAuth) ClearAuthReturn(w http.ResponseWriter, r *http.Request) error {
+9 -65
appview/oauth/accounts_test.go
··· 28 28 { 29 29 name: "add second account", 30 30 initial: []AccountInfo{ 31 - {Did: "did:plc:abc123", Handle: "alice.bsky.social", SessionId: "session-1", AddedAt: 1000}, 31 + {Did: "did:plc:abc123", SessionId: "session-1", AddedAt: 1000}, 32 32 }, 33 33 addDid: "did:plc:def456", 34 34 addHandle: "bob.bsky.social", ··· 40 40 { 41 41 name: "update existing account session", 42 42 initial: []AccountInfo{ 43 - {Did: "did:plc:abc123", Handle: "alice.bsky.social", SessionId: "old-session", AddedAt: 1000}, 43 + {Did: "did:plc:abc123", SessionId: "old-session", AddedAt: 1000}, 44 44 }, 45 45 addDid: "did:plc:abc123", 46 46 addHandle: "alice.bsky.social", ··· 112 112 { 113 113 name: "remove existing account", 114 114 initial: []AccountInfo{ 115 - {Did: "did:plc:abc123", Handle: "alice", SessionId: "s1"}, 116 - {Did: "did:plc:def456", Handle: "bob", SessionId: "s2"}, 115 + {Did: "did:plc:abc123", SessionId: "s1"}, 116 + {Did: "did:plc:def456", SessionId: "s2"}, 117 117 }, 118 118 removeDid: "did:plc:abc123", 119 119 wantLen: 1, ··· 122 122 { 123 123 name: "remove non-existing account", 124 124 initial: []AccountInfo{ 125 - {Did: "did:plc:abc123", Handle: "alice", SessionId: "s1"}, 125 + {Did: "did:plc:abc123", SessionId: "s1"}, 126 126 }, 127 127 removeDid: "did:plc:notfound", 128 128 wantLen: 1, ··· 131 131 { 132 132 name: "remove last account", 133 133 initial: []AccountInfo{ 134 - {Did: "did:plc:abc123", Handle: "alice", SessionId: "s1"}, 134 + {Did: "did:plc:abc123", SessionId: "s1"}, 135 135 }, 136 136 removeDid: "did:plc:abc123", 137 137 wantLen: 0, ··· 171 171 func TestAccountRegistry_FindAccount(t *testing.T) { 172 172 registry := &AccountRegistry{ 173 173 Accounts: []AccountInfo{ 174 - {Did: "did:plc:first", Handle: "first", SessionId: "s1", AddedAt: 1000}, 175 - {Did: "did:plc:second", Handle: "second", SessionId: "s2", AddedAt: 2000}, 176 - {Did: "did:plc:third", Handle: "third", SessionId: "s3", AddedAt: 3000}, 174 + {Did: "did:plc:first", SessionId: "s1", AddedAt: 1000}, 175 + {Did: "did:plc:second", SessionId: "s2", AddedAt: 2000}, 176 + {Did: "did:plc:third", SessionId: "s3", AddedAt: 3000}, 177 177 }, 178 178 } 179 179 ··· 182 182 if found == nil { 183 183 t.Fatal("FindAccount() returned nil for existing account") 184 184 } 185 - if found.Handle != "second" { 186 - t.Errorf("FindAccount() handle = %s, want second", found.Handle) 187 - } 188 185 if found.SessionId != "s2" { 189 186 t.Errorf("FindAccount() sessionId = %s, want s2", found.SessionId) 190 187 } ··· 210 207 } 211 208 }) 212 209 } 213 - 214 - func TestAccountRegistry_OtherAccounts(t *testing.T) { 215 - registry := &AccountRegistry{ 216 - Accounts: []AccountInfo{ 217 - {Did: "did:plc:active", Handle: "active", SessionId: "s1"}, 218 - {Did: "did:plc:other1", Handle: "other1", SessionId: "s2"}, 219 - {Did: "did:plc:other2", Handle: "other2", SessionId: "s3"}, 220 - }, 221 - } 222 - 223 - others := registry.OtherAccounts("did:plc:active") 224 - 225 - if len(others) != 2 { 226 - t.Errorf("OtherAccounts() len = %d, want 2", len(others)) 227 - } 228 - 229 - for _, acc := range others { 230 - if acc.Did == "did:plc:active" { 231 - t.Errorf("OtherAccounts() should not include active account") 232 - } 233 - } 234 - 235 - hasDid := func(did string) bool { 236 - for _, acc := range others { 237 - if acc.Did == did { 238 - return true 239 - } 240 - } 241 - return false 242 - } 243 - 244 - if !hasDid("did:plc:other1") || !hasDid("did:plc:other2") { 245 - t.Errorf("OtherAccounts() missing expected accounts") 246 - } 247 - } 248 - 249 - func TestMultiAccountUser_Did(t *testing.T) { 250 - t.Run("with active user", func(t *testing.T) { 251 - user := &MultiAccountUser{ 252 - Active: &User{Did: "did:plc:test", Pds: "https://bsky.social"}, 253 - } 254 - if user.Did() != "did:plc:test" { 255 - t.Errorf("Did() = %s, want did:plc:test", user.Did()) 256 - } 257 - }) 258 - 259 - t.Run("with nil active", func(t *testing.T) { 260 - user := &MultiAccountUser{Active: nil} 261 - if user.Did() != "" { 262 - t.Errorf("Did() = %s, want empty string", user.Did()) 263 - } 264 - }) 265 - }
-7
appview/oauth/consts.go
··· 5 5 AccountsName = "appview-accounts-v2" 6 6 AuthReturnName = "appview-auth-return" 7 7 AuthReturnURL = "return_url" 8 - AuthAddAccount = "add_account" 9 8 SessionHandle = "handle" 10 9 SessionDid = "did" 11 10 SessionId = "id" 12 11 SessionPds = "pds" 13 - SessionAccessJwt = "accessJwt" 14 - SessionRefreshJwt = "refreshJwt" 15 - SessionExpiry = "expiry" 16 12 SessionAuthenticated = "authenticated" 17 - 18 - SessionDpopPrivateJwk = "dpopPrivateJwk" 19 - SessionDpopAuthServerNonce = "dpopAuthServerNonce" 20 13 )
+5 -7
appview/oauth/handler.go
··· 31 31 32 32 r.Get("/oauth/client-metadata.json", o.clientMetadata) 33 33 r.Get("/oauth/jwks.json", o.jwks) 34 - r.Get("/oauth/callback", o.callback) 34 + r.Get("/oauth/callback", o.Callback) 35 35 return r 36 36 } 37 37 ··· 57 57 } 58 58 } 59 59 60 - func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) { 60 + func (o *OAuth) Callback(w http.ResponseWriter, r *http.Request) { 61 61 ctx := r.Context() 62 62 l := o.Logger.With("query", r.URL.Query()) 63 63 ··· 104 104 } 105 105 } 106 106 107 - redirectURL := "/" 108 - if authReturn.ReturnURL != "" { 109 - redirectURL = authReturn.ReturnURL 107 + if authReturn == "" { 108 + authReturn = "/" 110 109 } 111 - 112 - http.Redirect(w, r, redirectURL, http.StatusFound) 110 + http.Redirect(w, r, authReturn, http.StatusFound) 113 111 } 114 112 115 113 func (o *OAuth) addToDefaultSpindle(did string) {
+7 -24
appview/oauth/oauth.go
··· 9 9 "time" 10 10 11 11 comatproto "github.com/bluesky-social/indigo/api/atproto" 12 + "github.com/bluesky-social/indigo/atproto/atclient" 13 + "github.com/bluesky-social/indigo/atproto/atcrypto" 12 14 "github.com/bluesky-social/indigo/atproto/auth/oauth" 13 - atpclient "github.com/bluesky-social/indigo/atproto/client" 14 - atcrypto "github.com/bluesky-social/indigo/atproto/crypto" 15 15 "github.com/bluesky-social/indigo/atproto/syntax" 16 16 xrpc "github.com/bluesky-social/indigo/xrpc" 17 17 "github.com/gorilla/sessions" ··· 126 126 if err := registry.AddAccount(sessData.AccountDID.String(), handle, sessData.SessionID); err != nil { 127 127 return err 128 128 } 129 - return o.SaveAccounts(w, r, registry) 129 + return o.saveAccounts(w, r, registry) 130 130 } 131 131 132 132 func (o *OAuth) ResumeSession(r *http.Request) (*oauth.ClientSession, error) { ··· 202 202 sess, err := o.ClientApp.ResumeSession(r.Context(), did, account.SessionId) 203 203 if err != nil { 204 204 registry.RemoveAccount(targetDid) 205 - _ = o.SaveAccounts(w, r, registry) 205 + _ = o.saveAccounts(w, r, registry) 206 206 return fmt.Errorf("session expired for account: %w", err) 207 207 } 208 208 ··· 232 232 } 233 233 234 234 registry.RemoveAccount(targetDid) 235 - return o.SaveAccounts(w, r, registry) 236 - } 237 - 238 - type User struct { 239 - Did string 240 - Pds string 241 - } 242 - 243 - func (o *OAuth) GetUser(r *http.Request) *User { 244 - sess, err := o.ResumeSession(r) 245 - if err != nil { 246 - return nil 247 - } 248 - 249 - return &User{ 250 - Did: sess.Data.AccountDID.String(), 251 - Pds: sess.Data.HostURL, 252 - } 235 + return o.saveAccounts(w, r, registry) 253 236 } 254 237 255 238 func (o *OAuth) GetDid(r *http.Request) string { 256 239 if u := o.GetMultiAccountUser(r); u != nil { 257 - return u.Did() 240 + return u.Did 258 241 } 259 242 260 243 return "" 261 244 } 262 245 263 - func (o *OAuth) AuthorizedClient(r *http.Request) (*atpclient.APIClient, error) { 246 + func (o *OAuth) AuthorizedClient(r *http.Request) (*atclient.APIClient, error) { 264 247 session, err := o.ResumeSession(r) 265 248 if err != nil { 266 249 return nil, fmt.Errorf("error getting session: %w", err)
+7
appview/pages/funcmap.go
··· 77 77 78 78 return identity.Handle.String() 79 79 }, 80 + "resolvePds": func(s string) string { 81 + identity, err := p.resolver.ResolveIdent(context.Background(), s) 82 + if err != nil { 83 + return "" 84 + } 85 + return identity.PDSEndpoint() 86 + }, 80 87 "ownerSlashRepo": func(repo *models.Repo) string { 81 88 ownerId, err := p.resolver.ResolveIdent(context.Background(), repo.Did) 82 89 if err != nil {
+8 -8
appview/pages/pages.go
··· 248 248 } 249 249 250 250 type LoginParams struct { 251 - ReturnUrl string 252 - ErrorCode string 253 - AddAccount bool 254 - LoggedInUser *oauth.MultiAccountUser 251 + ReturnUrl string 252 + ErrorCode string 253 + AddAccount bool 254 + Accounts []oauth.AccountInfo 255 255 } 256 256 257 257 func (p *Pages) Login(w io.Writer, params LoginParams) error { ··· 1111 1111 LoggedInUser *oauth.MultiAccountUser 1112 1112 RepoInfo repoinfo.RepoInfo 1113 1113 Issue *models.Issue 1114 - Comment *models.IssueComment 1114 + Comment *models.Comment 1115 1115 } 1116 1116 1117 1117 func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { ··· 1122 1122 LoggedInUser *oauth.MultiAccountUser 1123 1123 RepoInfo repoinfo.RepoInfo 1124 1124 Issue *models.Issue 1125 - Comment *models.IssueComment 1125 + Comment *models.Comment 1126 1126 } 1127 1127 1128 1128 func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { ··· 1133 1133 LoggedInUser *oauth.MultiAccountUser 1134 1134 RepoInfo repoinfo.RepoInfo 1135 1135 Issue *models.Issue 1136 - Comment *models.IssueComment 1136 + Comment *models.Comment 1137 1137 } 1138 1138 1139 1139 func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { ··· 1144 1144 LoggedInUser *oauth.MultiAccountUser 1145 1145 RepoInfo repoinfo.RepoInfo 1146 1146 Issue *models.Issue 1147 - Comment *models.IssueComment 1147 + Comment *models.Comment 1148 1148 } 1149 1149 1150 1150 func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
+7 -8
appview/pages/templates/layouts/fragments/topbar.html
··· 43 43 {{ end }} 44 44 45 45 {{ define "profileDropdown" }} 46 + {{ $handle := resolve .Did }} 46 47 <details class="relative inline-block text-left nav-dropdown"> 47 48 <summary class="cursor-pointer list-none flex items-center gap-1"> 48 - {{ $user := .Active.Did }} 49 - {{ template "user/fragments/pic" (list $user "size-6") }} 50 - <span class="hidden md:inline">{{ $user | resolve | truncateAt30 }}</span> 49 + {{ template "user/fragments/pic" (list .Did "size-6") }} 50 + <span class="hidden md:inline">{{ $handle | truncateAt30 }}</span> 51 51 </summary> 52 52 <div class="absolute right-0 mt-4 rounded bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700 shadow-lg z-50 text-sm" style="width: 14rem;"> 53 - {{ $active := .Active.Did }} 54 53 {{ $linkStyle := "flex items-center gap-3 px-4 py-2 hover:no-underline hover:bg-gray-50 hover:dark:bg-gray-700/50" }} 55 54 56 - {{ $others := .Accounts | otherAccounts $active }} 55 + {{ $others := .Accounts | otherAccounts .Did }} 57 56 {{ if $others }} 58 57 <div class="text-sm text-gray-500 dark:text-gray-400 px-3 py-1 pt-2">switch account</div> 59 58 {{ range $others }} ··· 81 80 </a> 82 81 83 82 <div class="border-t border-gray-200 dark:border-gray-700"> 84 - <a href="/{{ $active }}" class="{{$linkStyle}}"> 83 + <a href="/{{ $handle }}" class="{{$linkStyle}}"> 85 84 {{ i "user" "size-4" }} 86 85 profile 87 86 </a> 88 - <a href="/{{ $active }}?tab=repos" class="{{$linkStyle}}"> 87 + <a href="/{{ $handle }}?tab=repos" class="{{$linkStyle}}"> 89 88 {{ i "book-marked" "size-4" }} 90 89 repositories 91 90 </a> 92 - <a href="/{{ $active }}?tab=strings" class="{{$linkStyle}}"> 91 + <a href="/{{ $handle }}?tab=strings" class="{{$linkStyle}}"> 93 92 {{ i "line-squiggle" "size-4" }} 94 93 strings 95 94 </a>
+2 -2
appview/pages/templates/repo/issues/fragments/commentList.html
··· 41 41 {{ define "topLevelComment" }} 42 42 <div class="rounded px-6 py-4 bg-white dark:bg-gray-800 flex gap-2 "> 43 43 <div class="flex-shrink-0"> 44 - {{ template "user/fragments/picLink" (list .Comment.Did "size-8 mr-1") }} 44 + {{ template "user/fragments/picLink" (list .Comment.Did.String "size-8 mr-1") }} 45 45 </div> 46 46 <div class="flex-1 min-w-0"> 47 47 {{ template "repo/issues/fragments/issueCommentHeader" . }} ··· 53 53 {{ define "replyComment" }} 54 54 <div class="py-4 pr-4 w-full mx-auto overflow-hidden flex gap-2 "> 55 55 <div class="flex-shrink-0"> 56 - {{ template "user/fragments/picLink" (list .Comment.Did "size-8 mr-1") }} 56 + {{ template "user/fragments/picLink" (list .Comment.Did.String "size-8 mr-1") }} 57 57 </div> 58 58 <div class="flex-1 min-w-0"> 59 59 {{ template "repo/issues/fragments/issueCommentHeader" . }}
+2 -2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
··· 1 1 {{ define "repo/issues/fragments/issueCommentHeader" }} 2 2 <div class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 "> 3 - {{ $handle := resolve .Comment.Did }} 3 + {{ $handle := resolve .Comment.Did.String }} 4 4 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="/{{ $handle }}">{{ $handle }}</a> 5 5 {{ template "hats" $ }} 6 6 <span class="before:content-['ยท']"></span> 7 7 {{ template "timestamp" . }} 8 - {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }} 8 + {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did.String) }} 9 9 {{ if and $isCommentOwner (not .Comment.Deleted) }} 10 10 {{ template "editIssueComment" . }} 11 11 {{ template "deleteIssueComment" . }}
+4 -4
appview/pages/templates/repo/pulls/pull.html
··· 579 579 {{ end }} 580 580 581 581 {{ define "submissionComment" }} 582 - <div id="comment-{{.ID}}" class="flex gap-2 -ml-4 py-4 w-full mx-auto"> 582 + <div id="comment-{{.Id}}" class="flex gap-2 -ml-4 py-4 w-full mx-auto"> 583 583 <!-- left column: profile picture --> 584 584 <div class="flex-shrink-0 h-fit relative"> 585 - {{ template "user/fragments/picLink" (list .OwnerDid "size-8") }} 585 + {{ template "user/fragments/picLink" (list .Did.String "size-8") }} 586 586 </div> 587 587 <!-- right column: name and body in two rows --> 588 588 <div class="flex-1 min-w-0"> 589 589 <!-- Row 1: Author and timestamp --> 590 590 <div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1"> 591 - {{ $handle := resolve .OwnerDid }} 591 + {{ $handle := resolve .Did.String }} 592 592 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="/{{ $handle }}">{{ $handle }}</a> 593 593 <span class="before:content-['ยท']"></span> 594 - <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}"> 594 + <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.Id}}"> 595 595 {{ template "repo/fragments/shortTime" .Created }} 596 596 </a> 597 597 </div>
+1 -1
appview/pages/templates/strings/fragments/form.html
··· 31 31 name="content" 32 32 id="content-textarea" 33 33 wrap="off" 34 - class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400 font-mono" 34 + class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400" 35 35 rows="20" 36 36 spellcheck="false" 37 37 placeholder="Paste your string here!"
+2 -6
appview/pages/templates/user/login.html
··· 11 11 </div> 12 12 {{ end }} 13 13 14 - {{ if and .LoggedInUser .LoggedInUser.Accounts }} 15 - {{ $accounts := .LoggedInUser.Accounts }} 16 - {{ if $accounts }} 14 + {{ if .Accounts }} 17 15 <div class="my-4 border border-gray-200 dark:border-gray-700 rounded overflow-hidden"> 18 16 <div class="px-3 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"> 19 17 <span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide font-medium">Saved accounts</span> 20 18 </div> 21 19 <div class="divide-y divide-gray-200 dark:divide-gray-700"> 22 - {{ range $accounts }} 20 + {{ range .Accounts }} 23 21 <div class="flex items-center justify-between px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"> 24 22 <button 25 23 type="button" ··· 48 46 </div> 49 47 </div> 50 48 {{ end }} 51 - {{ end }} 52 49 53 50 <form 54 51 class="mt-4 group" ··· 76 73 </span> 77 74 </div> 78 75 <input type="hidden" name="return_url" value="{{ .ReturnUrl }}"> 79 - <input type="hidden" name="add_account" value="{{ if .AddAccount }}true{{ end }}"> 80 76 81 77 <button 82 78 class="btn w-full my-2 mt-6 text-base"
+1 -1
appview/pages/templates/user/settings/profile.html
··· 34 34 </div> 35 35 <div class="flex flex-col gap-1 p-4"> 36 36 <span class="text-sm text-gray-500 dark:text-gray-400">Personal Data Server (PDS)</span> 37 - <span class="font-bold">{{ .LoggedInUser.Pds }}</span> 37 + <span class="font-bold">{{ resolvePds .LoggedInUser.Did }}</span> 38 38 </div> 39 39 </div> 40 40 </div>
+87 -84
appview/pulls/pulls.go
··· 132 132 mergeCheckResponse := s.mergeCheck(r, f, pull, stack) 133 133 branchDeleteStatus := s.branchDeleteStatus(r, f, pull) 134 134 resubmitResult := pages.Unknown 135 - if user.Active.Did == pull.OwnerDid { 135 + if user.Did == pull.OwnerDid { 136 136 resubmitResult = s.resubmitCheck(r, f, pull, stack) 137 137 } 138 138 ··· 195 195 mergeCheckResponse := s.mergeCheck(r, f, pull, stack) 196 196 branchDeleteStatus := s.branchDeleteStatus(r, f, pull) 197 197 resubmitResult := pages.Unknown 198 - if user != nil && user.Active != nil && user.Active.Did == pull.OwnerDid { 198 + if user != nil && user.Did == pull.OwnerDid { 199 199 resubmitResult = s.resubmitCheck(r, f, pull, stack) 200 200 } 201 201 ··· 236 236 237 237 userReactions := map[models.ReactionKind]bool{} 238 238 if user != nil { 239 - userReactions = db.GetReactionStatusMap(s.db, user.Active.Did, pull.AtUri()) 239 + userReactions = db.GetReactionStatusMap(s.db, user.Did, pull.AtUri()) 240 240 } 241 241 242 242 labelDefs, err := db.GetLabelDefinitions( ··· 406 406 } 407 407 408 408 // user can only delete branch if they are a collaborator in the repo that the branch belongs to 409 - perms := s.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.DidSlashRepo()) 409 + perms := s.enforcer.GetPermissionsInRepo(user.Did, repo.Knot, repo.DidSlashRepo()) 410 410 if !slices.Contains(perms, "repo:push") { 411 411 return nil 412 412 } ··· 834 834 } 835 835 defer tx.Rollback() 836 836 837 - createdAt := time.Now().Format(time.RFC3339) 837 + comment := models.Comment{ 838 + Did: syntax.DID(user.Did), 839 + Collection: tangled.CommentNSID, 840 + Rkey: tid.TID(), 841 + Subject: pull.AtUri(), 842 + ReplyTo: nil, 843 + Body: body, 844 + Created: time.Now(), 845 + Mentions: mentions, 846 + References: references, 847 + PullSubmissionId: &pull.Submissions[roundNumber].ID, 848 + } 849 + if err = comment.Validate(); err != nil { 850 + log.Println("failed to validate comment", err) 851 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 852 + return 853 + } 838 854 839 855 client, err := s.oauth.AuthorizedClient(r) 840 856 if err != nil { ··· 842 858 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 843 859 return 844 860 } 845 - atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 846 - Collection: tangled.RepoPullCommentNSID, 847 - Repo: user.Active.Did, 848 - Rkey: tid.TID(), 861 + 862 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 863 + Collection: comment.Collection.String(), 864 + Repo: comment.Did.String(), 865 + Rkey: comment.Rkey, 849 866 Record: &lexutil.LexiconTypeDecoder{ 850 - Val: &tangled.RepoPullComment{ 851 - Pull: pull.AtUri().String(), 852 - Body: body, 853 - CreatedAt: createdAt, 854 - }, 867 + Val: comment.AsRecord(), 855 868 }, 856 869 }) 857 870 if err != nil { ··· 860 873 return 861 874 } 862 875 863 - comment := &models.PullComment{ 864 - OwnerDid: user.Active.Did, 865 - RepoAt: f.RepoAt().String(), 866 - PullId: pull.PullId, 867 - Body: body, 868 - CommentAt: atResp.Uri, 869 - SubmissionId: pull.Submissions[roundNumber].ID, 870 - Mentions: mentions, 871 - References: references, 872 - } 873 - 874 876 // Create the pull comment in the database with the commentAt field 875 - commentId, err := db.NewPullComment(tx, comment) 877 + err = db.PutComment(tx, &comment) 876 878 if err != nil { 877 879 log.Println("failed to create pull comment", err) 878 880 s.pages.Notice(w, "pull-comment", "Failed to create comment.") ··· 886 888 return 887 889 } 888 890 889 - s.notifier.NewPullComment(r.Context(), comment, mentions) 891 + s.notifier.NewComment(r.Context(), &comment) 890 892 891 893 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 892 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, commentId)) 894 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, comment.Id)) 893 895 return 894 896 } 895 897 } ··· 956 958 fromFork := r.FormValue("fork") 957 959 sourceBranch := r.FormValue("sourceBranch") 958 960 patch := r.FormValue("patch") 961 + userDid := syntax.DID(user.Did) 959 962 960 963 if targetBranch == "" { 961 964 s.pages.Notice(w, "pull", "Target branch is required.") ··· 963 966 } 964 967 965 968 // Determine PR type based on input parameters 966 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 969 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(userDid.String(), f.Knot, f.DidSlashRepo())} 967 970 isPushAllowed := roles.IsPushAllowed() 968 971 isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 969 972 isForkBased := fromFork != "" && sourceBranch != "" ··· 1041 1044 s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?") 1042 1045 return 1043 1046 } 1044 - s.handleBranchBasedPull(w, r, f, user, title, body, targetBranch, sourceBranch, isStacked) 1047 + s.handleBranchBasedPull(w, r, f, userDid, title, body, targetBranch, sourceBranch, isStacked) 1045 1048 } else if isForkBased { 1046 1049 if !caps.PullRequests.ForkSubmissions { 1047 1050 s.pages.Notice(w, "pull", "This knot doesn't support fork-based pull requests. Try another way?") 1048 1051 return 1049 1052 } 1050 - s.handleForkBasedPull(w, r, f, user, fromFork, title, body, targetBranch, sourceBranch, isStacked) 1053 + s.handleForkBasedPull(w, r, f, userDid, fromFork, title, body, targetBranch, sourceBranch, isStacked) 1051 1054 } else if isPatchBased { 1052 1055 if !caps.PullRequests.PatchSubmissions { 1053 1056 s.pages.Notice(w, "pull", "This knot doesn't support patch-based pull requests. Send your patch over email.") 1054 1057 return 1055 1058 } 1056 - s.handlePatchBasedPull(w, r, f, user, title, body, targetBranch, patch, isStacked) 1059 + s.handlePatchBasedPull(w, r, f, userDid, title, body, targetBranch, patch, isStacked) 1057 1060 } 1058 1061 return 1059 1062 } ··· 1063 1066 w http.ResponseWriter, 1064 1067 r *http.Request, 1065 1068 repo *models.Repo, 1066 - user *oauth.MultiAccountUser, 1069 + userDid syntax.DID, 1067 1070 title, 1068 1071 body, 1069 1072 targetBranch, ··· 1117 1120 Sha: comparison.Rev2, 1118 1121 } 1119 1122 1120 - s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked) 1123 + s.createPullRequest(w, r, repo, userDid, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked) 1121 1124 } 1122 1125 1123 - func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.MultiAccountUser, title, body, targetBranch, patch string, isStacked bool) { 1126 + func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, userDid syntax.DID, title, body, targetBranch, patch string, isStacked bool) { 1124 1127 if err := s.validator.ValidatePatch(&patch); err != nil { 1125 1128 s.logger.Error("patch validation failed", "err", err) 1126 1129 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 1127 1130 return 1128 1131 } 1129 1132 1130 - s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, "", "", nil, nil, isStacked) 1133 + s.createPullRequest(w, r, repo, userDid, title, body, targetBranch, patch, "", "", nil, nil, isStacked) 1131 1134 } 1132 1135 1133 - func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.MultiAccountUser, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) { 1136 + func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, userDid syntax.DID, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) { 1134 1137 repoString := strings.SplitN(forkRepo, "/", 2) 1135 1138 forkOwnerDid := repoString[0] 1136 1139 repoName := repoString[1] ··· 1232 1235 Sha: sourceRev, 1233 1236 } 1234 1237 1235 - s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked) 1238 + s.createPullRequest(w, r, repo, userDid, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked) 1236 1239 } 1237 1240 1238 1241 func (s *Pulls) createPullRequest( 1239 1242 w http.ResponseWriter, 1240 1243 r *http.Request, 1241 1244 repo *models.Repo, 1242 - user *oauth.MultiAccountUser, 1245 + userDid syntax.DID, 1243 1246 title, body, targetBranch string, 1244 1247 patch string, 1245 1248 combined string, ··· 1254 1257 w, 1255 1258 r, 1256 1259 repo, 1257 - user, 1260 + userDid, 1258 1261 targetBranch, 1259 1262 patch, 1260 1263 sourceRev, ··· 1311 1314 Title: title, 1312 1315 Body: body, 1313 1316 TargetBranch: targetBranch, 1314 - OwnerDid: user.Active.Did, 1317 + OwnerDid: userDid.String(), 1315 1318 RepoAt: repo.RepoAt(), 1316 1319 Rkey: rkey, 1317 1320 Mentions: mentions, ··· 1343 1346 1344 1347 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1345 1348 Collection: tangled.RepoPullNSID, 1346 - Repo: user.Active.Did, 1349 + Repo: userDid.String(), 1347 1350 Rkey: rkey, 1348 1351 Record: &lexutil.LexiconTypeDecoder{ 1349 1352 Val: &tangled.RepoPull{ ··· 1380 1383 w http.ResponseWriter, 1381 1384 r *http.Request, 1382 1385 repo *models.Repo, 1383 - user *oauth.MultiAccountUser, 1386 + userDid syntax.DID, 1384 1387 targetBranch string, 1385 1388 patch string, 1386 1389 sourceRev string, ··· 1411 1414 1412 1415 // build a stack out of this patch 1413 1416 stackId := uuid.New() 1414 - stack, err := s.newStack(r.Context(), repo, user, targetBranch, patch, pullSource, stackId.String()) 1417 + stack, err := s.newStack(r.Context(), repo, userDid, targetBranch, patch, pullSource, stackId.String()) 1415 1418 if err != nil { 1416 1419 log.Println("failed to create stack", err) 1417 1420 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err)) ··· 1448 1451 }) 1449 1452 } 1450 1453 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 1451 - Repo: user.Active.Did, 1454 + Repo: userDid.String(), 1452 1455 Writes: writes, 1453 1456 }) 1454 1457 if err != nil { ··· 1585 1588 func (s *Pulls) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 1586 1589 user := s.oauth.GetMultiAccountUser(r) 1587 1590 1588 - forks, err := db.GetForksByDid(s.db, user.Active.Did) 1591 + forks, err := db.GetForksByDid(s.db, user.Did) 1589 1592 if err != nil { 1590 1593 log.Println("failed to get forks", err) 1591 1594 return ··· 1732 1735 return 1733 1736 } 1734 1737 1735 - f, err := s.repoResolver.Resolve(r) 1736 - if err != nil { 1737 - log.Println("failed to get repo and knot", err) 1738 + if user == nil || user.Did != pull.OwnerDid { 1739 + log.Println("unauthorized user") 1740 + w.WriteHeader(http.StatusUnauthorized) 1738 1741 return 1739 1742 } 1740 1743 1741 - if user.Active.Did != pull.OwnerDid { 1742 - log.Println("unauthorized user") 1743 - w.WriteHeader(http.StatusUnauthorized) 1744 + f, err := s.repoResolver.Resolve(r) 1745 + if err != nil { 1746 + log.Println("failed to get repo and knot", err) 1744 1747 return 1745 1748 } 1746 1749 1747 1750 patch := r.FormValue("patch") 1748 1751 1749 - s.resubmitPullHelper(w, r, f, user, pull, patch, "", "") 1752 + s.resubmitPullHelper(w, r, f, syntax.DID(user.Did), pull, patch, "", "") 1750 1753 } 1751 1754 1752 1755 func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) { ··· 1759 1762 return 1760 1763 } 1761 1764 1765 + if user == nil || user.Did != pull.OwnerDid { 1766 + log.Println("unauthorized user") 1767 + w.WriteHeader(http.StatusUnauthorized) 1768 + return 1769 + } 1770 + 1762 1771 f, err := s.repoResolver.Resolve(r) 1763 1772 if err != nil { 1764 1773 log.Println("failed to get repo and knot", err) 1765 1774 return 1766 1775 } 1767 1776 1768 - if user.Active.Did != pull.OwnerDid { 1769 - log.Println("unauthorized user") 1770 - w.WriteHeader(http.StatusUnauthorized) 1771 - return 1772 - } 1773 - 1774 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 1777 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 1775 1778 if !roles.IsPushAllowed() { 1776 1779 log.Println("unauthorized user") 1777 1780 w.WriteHeader(http.StatusUnauthorized) ··· 1811 1814 patch := comparison.FormatPatchRaw 1812 1815 combined := comparison.CombinedPatchRaw 1813 1816 1814 - s.resubmitPullHelper(w, r, f, user, pull, patch, combined, sourceRev) 1817 + s.resubmitPullHelper(w, r, f, syntax.DID(user.Did), pull, patch, combined, sourceRev) 1815 1818 } 1816 1819 1817 1820 func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) { ··· 1824 1827 return 1825 1828 } 1826 1829 1830 + if user == nil || user.Did != pull.OwnerDid { 1831 + log.Println("unauthorized user") 1832 + w.WriteHeader(http.StatusUnauthorized) 1833 + return 1834 + } 1835 + 1827 1836 f, err := s.repoResolver.Resolve(r) 1828 1837 if err != nil { 1829 1838 log.Println("failed to get repo and knot", err) 1830 - return 1831 - } 1832 - 1833 - if user.Active.Did != pull.OwnerDid { 1834 - log.Println("unauthorized user") 1835 - w.WriteHeader(http.StatusUnauthorized) 1836 1839 return 1837 1840 } 1838 1841 ··· 1908 1911 patch := comparison.FormatPatchRaw 1909 1912 combined := comparison.CombinedPatchRaw 1910 1913 1911 - s.resubmitPullHelper(w, r, f, user, pull, patch, combined, sourceRev) 1914 + s.resubmitPullHelper(w, r, f, syntax.DID(user.Did), pull, patch, combined, sourceRev) 1912 1915 } 1913 1916 1914 1917 func (s *Pulls) resubmitPullHelper( 1915 1918 w http.ResponseWriter, 1916 1919 r *http.Request, 1917 1920 repo *models.Repo, 1918 - user *oauth.MultiAccountUser, 1921 + userDid syntax.DID, 1919 1922 pull *models.Pull, 1920 1923 patch string, 1921 1924 combined string, ··· 1923 1926 ) { 1924 1927 if pull.IsStacked() { 1925 1928 log.Println("resubmitting stacked PR") 1926 - s.resubmitStackedPullHelper(w, r, repo, user, pull, patch, pull.StackId) 1929 + s.resubmitStackedPullHelper(w, r, repo, userDid, pull, patch, pull.StackId) 1927 1930 return 1928 1931 } 1929 1932 ··· 1971 1974 return 1972 1975 } 1973 1976 1974 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Active.Did, pull.Rkey) 1977 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, userDid.String(), pull.Rkey) 1975 1978 if err != nil { 1976 1979 // failed to get record 1977 1980 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") ··· 1994 1997 1995 1998 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1996 1999 Collection: tangled.RepoPullNSID, 1997 - Repo: user.Active.Did, 2000 + Repo: userDid.String(), 1998 2001 Rkey: pull.Rkey, 1999 2002 SwapRecord: ex.Cid, 2000 2003 Record: &lexutil.LexiconTypeDecoder{ ··· 2021 2024 w http.ResponseWriter, 2022 2025 r *http.Request, 2023 2026 repo *models.Repo, 2024 - user *oauth.MultiAccountUser, 2027 + userDid syntax.DID, 2025 2028 pull *models.Pull, 2026 2029 patch string, 2027 2030 stackId string, ··· 2029 2032 targetBranch := pull.TargetBranch 2030 2033 2031 2034 origStack, _ := r.Context().Value("stack").(models.Stack) 2032 - newStack, err := s.newStack(r.Context(), repo, user, targetBranch, patch, pull.PullSource, stackId) 2035 + newStack, err := s.newStack(r.Context(), repo, userDid, targetBranch, patch, pull.PullSource, stackId) 2033 2036 if err != nil { 2034 2037 log.Println("failed to create resubmitted stack", err) 2035 2038 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 2211 2214 } 2212 2215 2213 2216 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 2214 - Repo: user.Active.Did, 2217 + Repo: userDid.String(), 2215 2218 Writes: writes, 2216 2219 }) 2217 2220 if err != nil { ··· 2336 2339 2337 2340 // notify about the pull merge 2338 2341 for _, p := range pullsToMerge { 2339 - s.notifier.NewPullState(r.Context(), syntax.DID(user.Active.Did), p) 2342 + s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p) 2340 2343 } 2341 2344 2342 2345 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) ··· 2360 2363 } 2361 2364 2362 2365 // auth filter: only owner or collaborators can close 2363 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 2366 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 2364 2367 isOwner := roles.IsOwner() 2365 2368 isCollaborator := roles.IsCollaborator() 2366 - isPullAuthor := user.Active.Did == pull.OwnerDid 2369 + isPullAuthor := user.Did == pull.OwnerDid 2367 2370 isCloseAllowed := isOwner || isCollaborator || isPullAuthor 2368 2371 if !isCloseAllowed { 2369 2372 log.Println("failed to close pull") ··· 2409 2412 } 2410 2413 2411 2414 for _, p := range pullsToClose { 2412 - s.notifier.NewPullState(r.Context(), syntax.DID(user.Active.Did), p) 2415 + s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p) 2413 2416 } 2414 2417 2415 2418 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) ··· 2434 2437 } 2435 2438 2436 2439 // auth filter: only owner or collaborators can close 2437 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 2440 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 2438 2441 isOwner := roles.IsOwner() 2439 2442 isCollaborator := roles.IsCollaborator() 2440 - isPullAuthor := user.Active.Did == pull.OwnerDid 2443 + isPullAuthor := user.Did == pull.OwnerDid 2441 2444 isCloseAllowed := isOwner || isCollaborator || isPullAuthor 2442 2445 if !isCloseAllowed { 2443 2446 log.Println("failed to close pull") ··· 2483 2486 } 2484 2487 2485 2488 for _, p := range pullsToReopen { 2486 - s.notifier.NewPullState(r.Context(), syntax.DID(user.Active.Did), p) 2489 + s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p) 2487 2490 } 2488 2491 2489 2492 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 2490 2493 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 2491 2494 } 2492 2495 2493 - func (s *Pulls) newStack(ctx context.Context, repo *models.Repo, user *oauth.MultiAccountUser, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) { 2496 + func (s *Pulls) newStack(ctx context.Context, repo *models.Repo, userDid syntax.DID, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) { 2494 2497 formatPatches, err := patchutil.ExtractPatches(patch) 2495 2498 if err != nil { 2496 2499 return nil, fmt.Errorf("Failed to extract patches: %v", err) ··· 2526 2529 Title: title, 2527 2530 Body: body, 2528 2531 TargetBranch: targetBranch, 2529 - OwnerDid: user.Active.Did, 2532 + OwnerDid: userDid.String(), 2530 2533 RepoAt: repo.RepoAt(), 2531 2534 Rkey: rkey, 2532 2535 Mentions: mentions,
+4 -4
appview/repo/artifact.go
··· 77 77 78 78 putRecordResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 79 79 Collection: tangled.RepoArtifactNSID, 80 - Repo: user.Active.Did, 80 + Repo: user.Did, 81 81 Rkey: rkey, 82 82 Record: &lexutil.LexiconTypeDecoder{ 83 83 Val: &tangled.RepoArtifact{ ··· 106 106 defer tx.Rollback() 107 107 108 108 artifact := models.Artifact{ 109 - Did: user.Active.Did, 109 + Did: user.Did, 110 110 Rkey: rkey, 111 111 RepoAt: f.RepoAt(), 112 112 Tag: tag.Tag.Hash, ··· 257 257 258 258 artifact := artifacts[0] 259 259 260 - if user.Active.Did != artifact.Did { 260 + if user.Did != artifact.Did { 261 261 l.Error("user not authorized to delete artifact", "err", err) 262 262 rp.pages.Notice(w, "remove", "Unauthorized deletion of artifact.") 263 263 return ··· 265 265 266 266 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 267 267 Collection: tangled.RepoArtifactNSID, 268 - Repo: user.Active.Did, 268 + Repo: user.Did, 269 269 Rkey: artifact.Rkey, 270 270 }) 271 271 if err != nil {
+23 -23
appview/repo/repo.go
··· 32 32 "tangled.org/core/xrpc/serviceauth" 33 33 34 34 comatproto "github.com/bluesky-social/indigo/api/atproto" 35 - atpclient "github.com/bluesky-social/indigo/atproto/client" 35 + "github.com/bluesky-social/indigo/atproto/atclient" 36 36 "github.com/bluesky-social/indigo/atproto/syntax" 37 37 lexutil "github.com/bluesky-social/indigo/lex/util" 38 38 securejoin "github.com/cyphar/filepath-securejoin" ··· 89 89 func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) { 90 90 user := rp.oauth.GetMultiAccountUser(r) 91 91 l := rp.logger.With("handler", "EditSpindle") 92 - l = l.With("did", user.Active.Did) 92 + l = l.With("did", user.Did) 93 93 94 94 errorId := "operation-error" 95 95 fail := func(msg string, err error) { ··· 113 113 114 114 if !removingSpindle { 115 115 // ensure that this is a valid spindle for this user 116 - validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Active.Did) 116 + validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did) 117 117 if err != nil { 118 118 fail("Failed to find spindles. Try again later.", err) 119 119 return ··· 176 176 func (rp *Repo) AddLabelDef(w http.ResponseWriter, r *http.Request) { 177 177 user := rp.oauth.GetMultiAccountUser(r) 178 178 l := rp.logger.With("handler", "AddLabel") 179 - l = l.With("did", user.Active.Did) 179 + l = l.With("did", user.Did) 180 180 181 181 f, err := rp.repoResolver.Resolve(r) 182 182 if err != nil { ··· 222 222 } 223 223 224 224 label := models.LabelDefinition{ 225 - Did: user.Active.Did, 225 + Did: user.Did, 226 226 Rkey: tid.TID(), 227 227 Name: name, 228 228 ValueType: valueType, ··· 335 335 func (rp *Repo) DeleteLabelDef(w http.ResponseWriter, r *http.Request) { 336 336 user := rp.oauth.GetMultiAccountUser(r) 337 337 l := rp.logger.With("handler", "DeleteLabel") 338 - l = l.With("did", user.Active.Did) 338 + l = l.With("did", user.Did) 339 339 340 340 f, err := rp.repoResolver.Resolve(r) 341 341 if err != nil { ··· 443 443 func (rp *Repo) SubscribeLabel(w http.ResponseWriter, r *http.Request) { 444 444 user := rp.oauth.GetMultiAccountUser(r) 445 445 l := rp.logger.With("handler", "SubscribeLabel") 446 - l = l.With("did", user.Active.Did) 446 + l = l.With("did", user.Did) 447 447 448 448 f, err := rp.repoResolver.Resolve(r) 449 449 if err != nil { ··· 529 529 func (rp *Repo) UnsubscribeLabel(w http.ResponseWriter, r *http.Request) { 530 530 user := rp.oauth.GetMultiAccountUser(r) 531 531 l := rp.logger.With("handler", "UnsubscribeLabel") 532 - l = l.With("did", user.Active.Did) 532 + l = l.With("did", user.Did) 533 533 534 534 f, err := rp.repoResolver.Resolve(r) 535 535 if err != nil { ··· 700 700 func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { 701 701 user := rp.oauth.GetMultiAccountUser(r) 702 702 l := rp.logger.With("handler", "AddCollaborator") 703 - l = l.With("did", user.Active.Did) 703 + l = l.With("did", user.Did) 704 704 705 705 f, err := rp.repoResolver.Resolve(r) 706 706 if err != nil { ··· 729 729 return 730 730 } 731 731 732 - if collaboratorIdent.DID.String() == user.Active.Did { 732 + if collaboratorIdent.DID.String() == user.Did { 733 733 fail("You seem to be adding yourself as a collaborator.", nil) 734 734 return 735 735 } ··· 749 749 createdAt := time.Now() 750 750 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 751 751 Collection: tangled.RepoCollaboratorNSID, 752 - Repo: currentUser.Active.Did, 752 + Repo: currentUser.Did, 753 753 Rkey: rkey, 754 754 Record: &lexutil.LexiconTypeDecoder{ 755 755 Val: &tangled.RepoCollaborator{ ··· 798 798 } 799 799 800 800 err = db.AddCollaborator(tx, models.Collaborator{ 801 - Did: syntax.DID(currentUser.Active.Did), 801 + Did: syntax.DID(currentUser.Did), 802 802 Rkey: rkey, 803 803 SubjectDid: collaboratorIdent.DID, 804 804 RepoAt: f.RepoAt(), ··· 846 846 } 847 847 _, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{ 848 848 Collection: tangled.RepoNSID, 849 - Repo: user.Active.Did, 849 + Repo: user.Did, 850 850 Rkey: f.Rkey, 851 851 }) 852 852 if err != nil { ··· 975 975 r.Context(), 976 976 client, 977 977 &tangled.RepoForkSync_Input{ 978 - Did: user.Active.Did, 978 + Did: user.Did, 979 979 Name: f.Name, 980 980 Source: f.Source, 981 981 Branch: ref, ··· 1004 1004 switch r.Method { 1005 1005 case http.MethodGet: 1006 1006 user := rp.oauth.GetMultiAccountUser(r) 1007 - knots, err := rp.enforcer.GetKnotsForUser(user.Active.Did) 1007 + knots, err := rp.enforcer.GetKnotsForUser(user.Did) 1008 1008 if err != nil { 1009 1009 rp.pages.Notice(w, "repo", "Invalid user account.") 1010 1010 return ··· 1026 1026 } 1027 1027 l = l.With("targetKnot", targetKnot) 1028 1028 1029 - ok, err := rp.enforcer.E.Enforce(user.Active.Did, targetKnot, targetKnot, "repo:create") 1029 + ok, err := rp.enforcer.E.Enforce(user.Did, targetKnot, targetKnot, "repo:create") 1030 1030 if err != nil || !ok { 1031 1031 rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 1032 1032 return ··· 1043 1043 // in the user's account. 1044 1044 existingRepo, err := db.GetRepo( 1045 1045 rp.db, 1046 - orm.FilterEq("did", user.Active.Did), 1046 + orm.FilterEq("did", user.Did), 1047 1047 orm.FilterEq("name", forkName), 1048 1048 ) 1049 1049 if err != nil { ··· 1072 1072 // create an atproto record for this fork 1073 1073 rkey := tid.TID() 1074 1074 repo := &models.Repo{ 1075 - Did: user.Active.Did, 1075 + Did: user.Did, 1076 1076 Name: forkName, 1077 1077 Knot: targetKnot, 1078 1078 Rkey: rkey, ··· 1092 1092 1093 1093 atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{ 1094 1094 Collection: tangled.RepoNSID, 1095 - Repo: user.Active.Did, 1095 + Repo: user.Did, 1096 1096 Rkey: rkey, 1097 1097 Record: &lexutil.LexiconTypeDecoder{ 1098 1098 Val: &record, ··· 1171 1171 } 1172 1172 1173 1173 // acls 1174 - p, _ := securejoin.SecureJoin(user.Active.Did, forkName) 1175 - err = rp.enforcer.AddRepo(user.Active.Did, targetKnot, p) 1174 + p, _ := securejoin.SecureJoin(user.Did, forkName) 1175 + err = rp.enforcer.AddRepo(user.Did, targetKnot, p) 1176 1176 if err != nil { 1177 1177 l.Error("failed to add ACLs", "err", err) 1178 1178 rp.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 1197 1197 aturi = "" 1198 1198 1199 1199 rp.notifier.NewRepo(r.Context(), repo) 1200 - rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, forkName)) 1200 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Did, forkName)) 1201 1201 } 1202 1202 } 1203 1203 1204 1204 // this is used to rollback changes made to the PDS 1205 1205 // 1206 1206 // it is a no-op if the provided ATURI is empty 1207 - func rollbackRecord(ctx context.Context, aturi string, client *atpclient.APIClient) error { 1207 + func rollbackRecord(ctx context.Context, aturi string, client *atclient.APIClient) error { 1208 1208 if aturi == "" { 1209 1209 return nil 1210 1210 }
+1 -1
appview/repo/settings.go
··· 74 74 func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) { 75 75 user := rp.oauth.GetMultiAccountUser(r) 76 76 l := rp.logger.With("handler", "Secrets") 77 - l = l.With("did", user.Active.Did) 77 + l = l.With("did", user.Did) 78 78 79 79 f, err := rp.repoResolver.Resolve(r) 80 80 if err != nil {
+3 -3
appview/reporesolver/resolver.go
··· 75 75 repoAt := repo.RepoAt() 76 76 isStarred := false 77 77 roles := repoinfo.RolesInRepo{} 78 - if user != nil && user.Active != nil { 79 - isStarred = db.GetStarStatus(rr.execer, user.Active.Did, repoAt) 80 - roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.DidSlashRepo()) 78 + if user != nil { 79 + isStarred = db.GetStarStatus(rr.execer, user.Did, repoAt) 80 + roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Did, repo.Knot, repo.DidSlashRepo()) 81 81 } 82 82 83 83 stats := repo.RepoStats
+12
appview/service/issue/errors.go
··· 1 + package issue 2 + 3 + import "errors" 4 + 5 + var ( 6 + ErrUnAuthenticated = errors.New("user session missing") 7 + ErrForbidden = errors.New("unauthorized operation") 8 + ErrDatabaseFail = errors.New("db op fail") 9 + ErrPDSFail = errors.New("pds op fail") 10 + ErrIndexerFail = errors.New("indexer fail") 11 + ErrValidationFail = errors.New("issue validation fail") 12 + )
+280
appview/service/issue/issue.go
··· 1 + package issue 2 + 3 + import ( 4 + "context" 5 + "log/slog" 6 + "time" 7 + 8 + "github.com/bluesky-social/indigo/api/atproto" 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + lexutil "github.com/bluesky-social/indigo/lex/util" 11 + "tangled.org/core/api/tangled" 12 + "tangled.org/core/appview/config" 13 + "tangled.org/core/appview/db" 14 + issues_indexer "tangled.org/core/appview/indexer/issues" 15 + "tangled.org/core/appview/mentions" 16 + "tangled.org/core/appview/models" 17 + "tangled.org/core/appview/notify" 18 + "tangled.org/core/appview/session" 19 + "tangled.org/core/appview/validator" 20 + "tangled.org/core/idresolver" 21 + "tangled.org/core/orm" 22 + "tangled.org/core/rbac" 23 + "tangled.org/core/tid" 24 + ) 25 + 26 + type Service struct { 27 + config *config.Config 28 + db *db.DB 29 + enforcer *rbac.Enforcer 30 + indexer *issues_indexer.Indexer 31 + logger *slog.Logger 32 + notifier notify.Notifier 33 + idResolver *idresolver.Resolver 34 + refResolver *mentions.Resolver 35 + validator *validator.Validator 36 + } 37 + 38 + func NewService( 39 + logger *slog.Logger, 40 + config *config.Config, 41 + db *db.DB, 42 + enforcer *rbac.Enforcer, 43 + notifier notify.Notifier, 44 + idResolver *idresolver.Resolver, 45 + refResolver *mentions.Resolver, 46 + indexer *issues_indexer.Indexer, 47 + validator *validator.Validator, 48 + ) Service { 49 + return Service{ 50 + config, 51 + db, 52 + enforcer, 53 + indexer, 54 + logger, 55 + notifier, 56 + idResolver, 57 + refResolver, 58 + validator, 59 + } 60 + } 61 + 62 + func (s *Service) NewIssue(ctx context.Context, repo *models.Repo, title, body string) (*models.Issue, error) { 63 + l := s.logger.With("method", "NewIssue") 64 + sess, ok := session.FromContext(ctx) 65 + if !ok { 66 + l.Error("user session is missing in context") 67 + return nil, ErrForbidden 68 + } 69 + authorDid := syntax.DID(sess.User.Did) 70 + atpclient := sess.AtpClient 71 + l = l.With("did", authorDid) 72 + 73 + mentions, references := s.refResolver.Resolve(ctx, body) 74 + 75 + issue := models.Issue{ 76 + Did: authorDid.String(), 77 + Rkey: tid.TID(), 78 + RepoAt: repo.RepoAt(), 79 + Title: title, 80 + Body: body, 81 + Created: time.Now(), 82 + Mentions: mentions, 83 + References: references, 84 + Open: true, 85 + Repo: repo, 86 + } 87 + 88 + if err := s.validator.ValidateIssue(&issue); err != nil { 89 + l.Error("validation error", "err", err) 90 + return nil, ErrValidationFail 91 + } 92 + 93 + tx, err := s.db.BeginTx(ctx, nil) 94 + if err != nil { 95 + l.Error("db.BeginTx failed", "err", err) 96 + return nil, ErrDatabaseFail 97 + } 98 + defer tx.Rollback() 99 + 100 + if err := db.PutIssue(tx, &issue); err != nil { 101 + l.Error("db.PutIssue failed", "err", err) 102 + return nil, ErrDatabaseFail 103 + } 104 + 105 + record := issue.AsRecord() 106 + _, err = atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{ 107 + Repo: issue.Did, 108 + Collection: tangled.RepoIssueNSID, 109 + Rkey: issue.Rkey, 110 + Record: &lexutil.LexiconTypeDecoder{ 111 + Val: &record, 112 + }, 113 + }) 114 + if err != nil { 115 + l.Error("atproto.RepoPutRecord failed", "err", err) 116 + return nil, ErrPDSFail 117 + } 118 + if err = tx.Commit(); err != nil { 119 + l.Error("tx.Commit failed", "err", err) 120 + return nil, ErrDatabaseFail 121 + } 122 + 123 + s.notifier.NewIssue(ctx, &issue, mentions) 124 + return &issue, nil 125 + } 126 + 127 + func (s *Service) GetIssues(ctx context.Context, repo *models.Repo, searchOpts models.IssueSearchOptions) ([]models.Issue, error) { 128 + l := s.logger.With("method", "GetIssues") 129 + 130 + var issues []models.Issue 131 + var err error 132 + if searchOpts.HasSearchFilters() { 133 + res, err := s.indexer.Search(ctx, searchOpts) 134 + if err != nil { 135 + l.Error("failed to search for issues", "err", err) 136 + return nil, ErrIndexerFail 137 + } 138 + l.Debug("searched issues with indexer", "count", len(res.Hits)) 139 + issues, err = db.GetIssues(s.db, orm.FilterIn("id", res.Hits)) 140 + if err != nil { 141 + l.Error("failed to get issues", "err", err) 142 + return nil, ErrDatabaseFail 143 + } 144 + } else { 145 + filters := []orm.Filter{ 146 + orm.FilterEq("repo_at", repo.RepoAt()), 147 + } 148 + if searchOpts.IsOpen != nil { 149 + openInt := 0 150 + if *searchOpts.IsOpen { 151 + openInt = 1 152 + } 153 + filters = append(filters, orm.FilterEq("open", openInt)) 154 + } 155 + issues, err = db.GetIssuesPaginated( 156 + s.db, 157 + searchOpts.Page, 158 + filters..., 159 + ) 160 + if err != nil { 161 + l.Error("failed to get issues", "err", err) 162 + return nil, ErrDatabaseFail 163 + } 164 + } 165 + 166 + return issues, nil 167 + } 168 + 169 + func (s *Service) EditIssue(ctx context.Context, issue *models.Issue) error { 170 + l := s.logger.With("method", "EditIssue") 171 + sess, ok := session.FromContext(ctx) 172 + if !ok { 173 + l.Error("user session is missing in context") 174 + return ErrForbidden 175 + } 176 + atpclient := sess.AtpClient 177 + l = l.With("did", sess.User.Did) 178 + 179 + mentions, references := s.refResolver.Resolve(ctx, issue.Body) 180 + issue.Mentions = mentions 181 + issue.References = references 182 + 183 + if sess.User.Did != issue.Did { 184 + l.Error("only author can edit the issue") 185 + return ErrForbidden 186 + } 187 + 188 + if err := s.validator.ValidateIssue(issue); err != nil { 189 + l.Error("validation error", "err", err) 190 + return ErrValidationFail 191 + } 192 + 193 + tx, err := s.db.BeginTx(ctx, nil) 194 + if err != nil { 195 + l.Error("db.BeginTx failed", "err", err) 196 + return ErrDatabaseFail 197 + } 198 + defer tx.Rollback() 199 + 200 + if err := db.PutIssue(tx, issue); err != nil { 201 + l.Error("db.PutIssue failed", "err", err) 202 + return ErrDatabaseFail 203 + } 204 + 205 + record := issue.AsRecord() 206 + 207 + ex, err := atproto.RepoGetRecord(ctx, atpclient, "", tangled.RepoIssueNSID, issue.Did, issue.Rkey) 208 + if err != nil { 209 + l.Error("atproto.RepoGetRecord failed", "err", err) 210 + return ErrPDSFail 211 + } 212 + _, err = atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{ 213 + Repo: issue.Did, 214 + Collection: tangled.RepoIssueNSID, 215 + Rkey: issue.Rkey, 216 + SwapRecord: ex.Cid, 217 + Record: &lexutil.LexiconTypeDecoder{ 218 + Val: &record, 219 + }, 220 + }) 221 + if err != nil { 222 + l.Error("atproto.RepoPutRecord failed", "err", err) 223 + return ErrPDSFail 224 + } 225 + 226 + if err = tx.Commit(); err != nil { 227 + l.Error("tx.Commit failed", "err", err) 228 + return ErrDatabaseFail 229 + } 230 + 231 + // TODO: notify EditIssue 232 + 233 + return nil 234 + } 235 + 236 + func (s *Service) DeleteIssue(ctx context.Context, issue *models.Issue) error { 237 + l := s.logger.With("method", "DeleteIssue") 238 + sess, ok := session.FromContext(ctx) 239 + if !ok { 240 + l.Error("user session is missing in context") 241 + return ErrForbidden 242 + } 243 + atpclient := sess.AtpClient 244 + l = l.With("did", sess.User.Did) 245 + 246 + if sess.User.Did != issue.Did { 247 + l.Error("only author can edit the issue") 248 + return ErrForbidden 249 + } 250 + 251 + tx, err := s.db.BeginTx(ctx, nil) 252 + if err != nil { 253 + l.Error("db.BeginTx failed", "err", err) 254 + return ErrDatabaseFail 255 + } 256 + defer tx.Rollback() 257 + 258 + if err := db.DeleteIssues(tx, issue.Did, issue.Rkey); err != nil { 259 + l.Error("db.DeleteIssues failed", "err", err) 260 + return ErrDatabaseFail 261 + } 262 + 263 + _, err = atproto.RepoDeleteRecord(ctx, atpclient, &atproto.RepoDeleteRecord_Input{ 264 + Collection: tangled.RepoIssueNSID, 265 + Repo: issue.Did, 266 + Rkey: issue.Rkey, 267 + }) 268 + if err != nil { 269 + l.Error("atproto.RepoDeleteRecord failed", "err", err) 270 + return ErrPDSFail 271 + } 272 + 273 + if err := tx.Commit(); err != nil { 274 + l.Error("tx.Commit failed", "err", err) 275 + return ErrDatabaseFail 276 + } 277 + 278 + s.notifier.DeleteIssue(ctx, issue) 279 + return nil 280 + }
+84
appview/service/issue/state.go
··· 1 + package issue 2 + 3 + import ( 4 + "context" 5 + 6 + "github.com/bluesky-social/indigo/atproto/syntax" 7 + "tangled.org/core/appview/db" 8 + "tangled.org/core/appview/models" 9 + "tangled.org/core/appview/pages/repoinfo" 10 + "tangled.org/core/appview/session" 11 + "tangled.org/core/orm" 12 + ) 13 + 14 + func (s *Service) CloseIssue(ctx context.Context, issue *models.Issue) error { 15 + l := s.logger.With("method", "CloseIssue") 16 + sess, ok := session.FromContext(ctx) 17 + if !ok { 18 + l.Error("user session is missing in context") 19 + return ErrUnAuthenticated 20 + } 21 + sessDid := syntax.DID(sess.User.Did) 22 + l = l.With("did", sessDid) 23 + 24 + // TODO: make this more granular 25 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(sessDid.String(), issue.Repo.Knot, issue.Repo.DidSlashRepo())} 26 + isRepoOwner := roles.IsOwner() 27 + isCollaborator := roles.IsCollaborator() 28 + isIssueOwner := sessDid == syntax.DID(issue.Did) 29 + if !(isRepoOwner || isCollaborator || isIssueOwner) { 30 + l.Error("user is not authorized") 31 + return ErrForbidden 32 + } 33 + 34 + err := db.CloseIssues( 35 + s.db, 36 + orm.FilterEq("id", issue.Id), 37 + ) 38 + if err != nil { 39 + l.Error("db.CloseIssues failed", "err", err) 40 + return ErrDatabaseFail 41 + } 42 + 43 + // change the issue state (this will pass down to the notifiers) 44 + issue.Open = false 45 + 46 + s.notifier.NewIssueState(ctx, sessDid, issue) 47 + return nil 48 + } 49 + 50 + func (s *Service) ReopenIssue(ctx context.Context, issue *models.Issue) error { 51 + l := s.logger.With("method", "ReopenIssue") 52 + sess, ok := session.FromContext(ctx) 53 + if !ok { 54 + l.Error("user session is missing in context") 55 + return ErrUnAuthenticated 56 + } 57 + sessDid := syntax.DID(sess.User.Did) 58 + l = l.With("did", sessDid) 59 + 60 + // TODO: make this more granular 61 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(sessDid.String(), issue.Repo.Knot, issue.Repo.DidSlashRepo())} 62 + isRepoOwner := roles.IsOwner() 63 + isCollaborator := roles.IsCollaborator() 64 + isIssueOwner := sessDid == syntax.DID(issue.Did) 65 + if !(isRepoOwner || isCollaborator || isIssueOwner) { 66 + l.Error("user is not authorized") 67 + return ErrForbidden 68 + } 69 + 70 + err := db.ReopenIssues( 71 + s.db, 72 + orm.FilterEq("id", issue.Id), 73 + ) 74 + if err != nil { 75 + l.Error("db.ReopenIssues failed", "err", err) 76 + return ErrDatabaseFail 77 + } 78 + 79 + // change the issue state (this will pass down to the notifiers) 80 + issue.Open = true 81 + 82 + s.notifier.NewIssueState(ctx, sessDid, issue) 83 + return nil 84 + }
+11
appview/service/repo/errors.go
··· 1 + package repo 2 + 3 + import "errors" 4 + 5 + var ( 6 + ErrUnAuthenticated = errors.New("user session missing") 7 + ErrForbidden = errors.New("unauthorized operation") 8 + ErrDatabaseFail = errors.New("db op fail") 9 + ErrPDSFail = errors.New("pds op fail") 10 + ErrValidationFail = errors.New("repo validation fail") 11 + )
+94
appview/service/repo/repo.go
··· 1 + package repo 2 + 3 + import ( 4 + "context" 5 + "log/slog" 6 + "time" 7 + 8 + "github.com/bluesky-social/indigo/api/atproto" 9 + lexutil "github.com/bluesky-social/indigo/lex/util" 10 + "tangled.org/core/api/tangled" 11 + "tangled.org/core/appview/config" 12 + "tangled.org/core/appview/db" 13 + "tangled.org/core/appview/models" 14 + "tangled.org/core/appview/session" 15 + "tangled.org/core/rbac" 16 + "tangled.org/core/tid" 17 + ) 18 + 19 + type Service struct { 20 + logger *slog.Logger 21 + config *config.Config 22 + db *db.DB 23 + enforcer *rbac.Enforcer 24 + } 25 + 26 + func NewService( 27 + logger *slog.Logger, 28 + config *config.Config, 29 + db *db.DB, 30 + enforcer *rbac.Enforcer, 31 + ) Service { 32 + return Service{ 33 + logger, 34 + config, 35 + db, 36 + enforcer, 37 + } 38 + } 39 + 40 + // NewRepo creates a repository 41 + // It expects atproto session to be passed in `ctx` 42 + func (s *Service) NewRepo(ctx context.Context, name, description, knot string) (*models.Repo, error) { 43 + l := s.logger.With("method", "NewRepo") 44 + sess, ok := session.FromContext(ctx) 45 + if !ok { 46 + l.Error("user session is missing in context") 47 + return nil, ErrForbidden 48 + } 49 + 50 + atpclient := sess.AtpClient 51 + l = l.With("did", sess.User.Did) 52 + 53 + repo := models.Repo{ 54 + Did: sess.User.Did, 55 + Name: name, 56 + Knot: knot, 57 + Rkey: tid.TID(), 58 + Description: description, 59 + Created: time.Now(), 60 + Labels: s.config.Label.DefaultLabelDefs, 61 + } 62 + l = l.With("aturi", repo.RepoAt()) 63 + 64 + tx, err := s.db.BeginTx(ctx, nil) 65 + if err != nil { 66 + l.Error("db.BeginTx failed", "err", err) 67 + return nil, ErrDatabaseFail 68 + } 69 + defer tx.Rollback() 70 + 71 + if err = db.AddRepo(tx, &repo); err != nil { 72 + l.Error("db.AddRepo failed", "err", err) 73 + return nil, ErrDatabaseFail 74 + } 75 + 76 + record := repo.AsRecord() 77 + _, err = atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{ 78 + Repo: repo.Did, 79 + Collection: tangled.RepoNSID, 80 + Rkey: repo.Rkey, 81 + Record: &lexutil.LexiconTypeDecoder{ 82 + Val: &record, 83 + }, 84 + }) 85 + if err != nil { 86 + l.Error("atproto.RepoPutRecord failed", "err", err) 87 + return nil, ErrPDSFail 88 + } 89 + l.Info("wrote to PDS") 90 + 91 + // knotclient, err := s.oauth.ServiceClient( 92 + // ) 93 + panic("unimplemented") 94 + }
+89
appview/service/repo/repoinfo.go
··· 1 + package repo 2 + 3 + import ( 4 + "context" 5 + 6 + "github.com/bluesky-social/indigo/atproto/identity" 7 + "tangled.org/core/appview/db" 8 + "tangled.org/core/appview/models" 9 + "tangled.org/core/appview/pages/repoinfo" 10 + "tangled.org/core/appview/session" 11 + ) 12 + 13 + // MakeRepoInfo constructs [repoinfo.RepoInfo] object from given [models.Repo]. 14 + // 15 + // NOTE: [repoinfo.RepoInfo] is bad design and should be removed in future. 16 + // Avoid using this method if you can. 17 + func (s *Service) MakeRepoInfo( 18 + ctx context.Context, 19 + ownerId *identity.Identity, 20 + baseRepo *models.Repo, 21 + currentDir, ref string, 22 + ) repoinfo.RepoInfo { 23 + var ( 24 + repoAt = baseRepo.RepoAt() 25 + isStarred = false 26 + roles = repoinfo.RolesInRepo{} 27 + l = s.logger.With("method", "MakeRepoInfo").With("repoAt", repoAt) 28 + ) 29 + sess, ok := session.FromContext(ctx) 30 + if ok { 31 + isStarred = db.GetStarStatus(s.db, sess.User.Did, repoAt) 32 + roles.Roles = s.enforcer.GetPermissionsInRepo(sess.User.Did, baseRepo.Knot, baseRepo.DidSlashRepo()) 33 + } 34 + 35 + stats := baseRepo.RepoStats 36 + if stats == nil { 37 + starCount, err := db.GetStarCount(s.db, repoAt) 38 + if err != nil { 39 + l.Error("failed to get star count", "err", err) 40 + } 41 + issueCount, err := db.GetIssueCount(s.db, repoAt) 42 + if err != nil { 43 + l.Error("failed to get issue count", "err", err) 44 + } 45 + pullCount, err := db.GetPullCount(s.db, repoAt) 46 + if err != nil { 47 + l.Error("failed to get pull count", "err", err) 48 + } 49 + stats = &models.RepoStats{ 50 + StarCount: starCount, 51 + IssueCount: issueCount, 52 + PullCount: pullCount, 53 + } 54 + } 55 + 56 + var sourceRepo *models.Repo 57 + var err error 58 + if baseRepo.Source != "" { 59 + sourceRepo, err = db.GetRepoByAtUri(s.db, baseRepo.Source) 60 + if err != nil { 61 + l.Error("failed to get source repo", "source", baseRepo.Source, "err", err) 62 + } 63 + } 64 + 65 + return repoinfo.RepoInfo{ 66 + // this is basically a models.Repo 67 + OwnerDid: baseRepo.Did, 68 + OwnerHandle: ownerId.Handle.String(), // TODO: shouldn't use 69 + Name: baseRepo.Name, 70 + Rkey: baseRepo.Rkey, 71 + Description: baseRepo.Description, 72 + Website: baseRepo.Website, 73 + Topics: baseRepo.Topics, 74 + Knot: baseRepo.Knot, 75 + Spindle: baseRepo.Spindle, 76 + Stats: *stats, 77 + 78 + // fork repo upstream 79 + Source: sourceRepo, 80 + 81 + // repo path (context) 82 + CurrentDir: currentDir, 83 + Ref: ref, 84 + 85 + // info related to the session 86 + IsStarred: isStarred, 87 + Roles: roles, 88 + } 89 + }
+27
appview/session/context.go
··· 1 + package session 2 + 3 + import ( 4 + "context" 5 + 6 + "tangled.org/core/appview/oauth" 7 + ) 8 + 9 + type ctxKey struct{} 10 + 11 + func IntoContext(ctx context.Context, sess Session) context.Context { 12 + return context.WithValue(ctx, ctxKey{}, &sess) 13 + } 14 + 15 + func FromContext(ctx context.Context) (*Session, bool) { 16 + sess, ok := ctx.Value(ctxKey{}).(*Session) 17 + return sess, ok 18 + } 19 + 20 + // UserFromContext returns optional MultiAccountUser from context. 21 + func UserFromContext(ctx context.Context) *oauth.MultiAccountUser { 22 + sess, ok := ctx.Value(ctxKey{}).(*Session) 23 + if !ok { 24 + return nil 25 + } 26 + return sess.User 27 + }
+11
appview/session/session.go
··· 1 + package session 2 + 3 + import ( 4 + "github.com/bluesky-social/indigo/atproto/atclient" 5 + "tangled.org/core/appview/oauth" 6 + ) 7 + 8 + type Session struct { 9 + User *oauth.MultiAccountUser // TODO: move MultiAccountUser def to here 10 + AtpClient *atclient.APIClient 11 + }
+33 -34
appview/settings/settings.go
··· 23 23 "tangled.org/core/appview/oauth" 24 24 "tangled.org/core/appview/pages" 25 25 "tangled.org/core/appview/sites" 26 + "tangled.org/core/idresolver" 26 27 "tangled.org/core/tid" 27 28 28 29 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 33 34 ) 34 35 35 36 type Settings struct { 36 - Db *db.DB 37 - OAuth *oauth.OAuth 38 - Pages *pages.Pages 39 - Config *config.Config 40 - CfClient *cloudflare.Client 41 - Logger *slog.Logger 37 + Db *db.DB 38 + IdResolver *idresolver.Resolver 39 + OAuth *oauth.OAuth 40 + Pages *pages.Pages 41 + Config *config.Config 42 + CfClient *cloudflare.Client 43 + Logger *slog.Logger 42 44 } 43 45 44 46 func (s *Settings) Router() http.Handler { ··· 81 83 82 84 func (s *Settings) sitesSettings(w http.ResponseWriter, r *http.Request) { 83 85 user := s.OAuth.GetMultiAccountUser(r) 84 - did := s.OAuth.GetDid(r) 85 86 86 - claim, err := db.GetActiveDomainClaimForDid(s.Db, did) 87 + claim, err := db.GetActiveDomainClaimForDid(s.Db, user.Did) 87 88 if err != nil { 88 89 s.Logger.Error("failed to get domain claim", "err", err) 89 90 claim = nil ··· 91 92 92 93 // determine whether the active account has a tngl.sh handle, in which 93 94 // case their sites domain is automatically their handle domain. 94 - pdsDomain := strings.TrimPrefix(s.Config.Pds.Host, "https://") 95 - pdsDomain = strings.TrimPrefix(pdsDomain, "http://") 96 - isTnglHandle := false 97 - for _, acc := range user.Accounts { 98 - if acc.Did == did && strings.HasSuffix(acc.Handle, "."+pdsDomain) { 99 - isTnglHandle = true 100 - break 95 + isTnglHandle := func() bool { 96 + ident, err := s.IdResolver.ResolveIdent(r.Context(), user.Did) 97 + if err != nil { 98 + return false 101 99 } 102 - } 100 + return strings.HasSuffix(ident.Handle.String(), s.Config.Pds.HandleSuffix) 101 + }() 103 102 104 103 s.Pages.UserSiteSettings(w, pages.UserSiteSettingsParams{ 105 104 LoggedInUser: user, ··· 156 155 } 157 156 158 157 func (s *Settings) releaseSitesDomain(w http.ResponseWriter, r *http.Request) { 159 - did := s.OAuth.GetDid(r) 158 + user := s.OAuth.GetMultiAccountUser(r) 160 159 domain := strings.TrimSpace(r.FormValue("domain")) 161 160 162 161 if domain == "" { ··· 164 163 return 165 164 } 166 165 167 - pdsDomain := strings.TrimPrefix(s.Config.Pds.Host, "https://") 168 - pdsDomain = strings.TrimPrefix(pdsDomain, "http://") 169 - user := s.OAuth.GetMultiAccountUser(r) 170 - for _, acc := range user.Accounts { 171 - if acc.Did == did && strings.HasSuffix(acc.Handle, "."+pdsDomain) { 172 - if strings.HasSuffix(domain, "."+pdsDomain) { 173 - s.Pages.Notice(w, "settings-sites-error", "Your tngl.sh domain is tied to your handle and cannot be released here.") 174 - return 175 - } 166 + isTnglHandle := func() bool { 167 + ident, err := s.IdResolver.ResolveIdent(r.Context(), user.Did) 168 + if err != nil { 169 + return false 176 170 } 171 + return strings.HasSuffix(ident.Handle.String(), s.Config.Pds.HandleSuffix) 172 + }() 173 + if isTnglHandle { 174 + s.Pages.Notice(w, "settings-sites-error", "Your tngl.sh domain is tied to your handle and cannot be released here.") 175 + return 177 176 } 178 177 179 - if err := db.ReleaseDomain(s.Db, did, domain); err != nil { 178 + if err := db.ReleaseDomain(s.Db, user.Did, domain); err != nil { 180 179 s.Logger.Error("releasing domain", "err", err) 181 180 s.Pages.Notice(w, "settings-sites-error", "Unable to release domain. Make sure it belongs to your account.") 182 181 return ··· 184 183 185 184 // Clean up all site data for this DID asynchronously. 186 185 if s.CfClient.Enabled() { 187 - siteConfigs, err := db.GetRepoSiteConfigsForDid(s.Db, did) 186 + siteConfigs, err := db.GetRepoSiteConfigsForDid(s.Db, user.Did) 188 187 if err != nil { 189 188 s.Logger.Error("releaseSitesDomain: fetching site configs for cleanup", "err", err) 190 189 } 191 190 192 - if err := db.DeleteRepoSiteConfigsForDid(s.Db, did); err != nil { 191 + if err := db.DeleteRepoSiteConfigsForDid(s.Db, user.Did); err != nil { 193 192 s.Logger.Error("releaseSitesDomain: deleting site configs from db", "err", err) 194 193 } 195 194 ··· 198 197 199 198 // Delete each repo's R2 objects. 200 199 for _, sc := range siteConfigs { 201 - if err := sites.Delete(ctx, s.CfClient, did, sc.RepoName); err != nil { 202 - s.Logger.Error("releaseSitesDomain: R2 delete failed", "did", did, "repo", sc.RepoName, "err", err) 200 + if err := sites.Delete(ctx, s.CfClient, user.Did, sc.RepoName); err != nil { 201 + s.Logger.Error("releaseSitesDomain: R2 delete failed", "did", user.Did, "repo", sc.RepoName, "err", err) 203 202 } 204 203 } 205 204 ··· 233 232 func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) { 234 233 user := s.OAuth.GetMultiAccountUser(r) 235 234 236 - punchcardPreferences, err := db.GetPunchcardPreference(s.Db, user.Did()) 235 + punchcardPreferences, err := db.GetPunchcardPreference(s.Db, user.Did) 237 236 if err != nil { 238 237 log.Printf("failed to get users punchcard preferences: %s", err) 239 238 } ··· 290 289 291 290 func (s *Settings) keysSettings(w http.ResponseWriter, r *http.Request) { 292 291 user := s.OAuth.GetMultiAccountUser(r) 293 - pubKeys, err := db.GetPublicKeysForDid(s.Db, user.Active.Did) 292 + pubKeys, err := db.GetPublicKeysForDid(s.Db, user.Did) 294 293 if err != nil { 295 294 s.Logger.Error("keys settings", "err", err) 296 295 } ··· 303 302 304 303 func (s *Settings) emailsSettings(w http.ResponseWriter, r *http.Request) { 305 304 user := s.OAuth.GetMultiAccountUser(r) 306 - emails, err := db.GetAllEmails(s.Db, user.Active.Did) 305 + emails, err := db.GetAllEmails(s.Db, user.Did) 307 306 if err != nil { 308 307 s.Logger.Error("emails settings", "err", err) 309 308 }
+34 -34
appview/spindles/spindles.go
··· 59 59 user := s.OAuth.GetMultiAccountUser(r) 60 60 all, err := db.GetSpindles( 61 61 s.Db, 62 - orm.FilterEq("owner", user.Active.Did), 62 + orm.FilterEq("owner", user.Did), 63 63 ) 64 64 if err != nil { 65 65 s.Logger.Error("failed to fetch spindles", "err", err) ··· 78 78 l := s.Logger.With("handler", "dashboard") 79 79 80 80 user := s.OAuth.GetMultiAccountUser(r) 81 - l = l.With("user", user.Active.Did) 81 + l = l.With("user", user.Did) 82 82 83 83 instance := chi.URLParam(r, "instance") 84 84 if instance == "" { ··· 89 89 spindles, err := db.GetSpindles( 90 90 s.Db, 91 91 orm.FilterEq("instance", instance), 92 - orm.FilterEq("owner", user.Active.Did), 92 + orm.FilterEq("owner", user.Did), 93 93 orm.FilterIsNot("verified", "null"), 94 94 ) 95 95 if err != nil || len(spindles) != 1 { ··· 161 161 return 162 162 } 163 163 l = l.With("instance", instance) 164 - l = l.With("user", user.Active.Did) 164 + l = l.With("user", user.Did) 165 165 166 166 tx, err := s.Db.Begin() 167 167 if err != nil { ··· 175 175 }() 176 176 177 177 err = db.AddSpindle(tx, models.Spindle{ 178 - Owner: syntax.DID(user.Active.Did), 178 + Owner: syntax.DID(user.Did), 179 179 Instance: instance, 180 180 }) 181 181 if err != nil { ··· 199 199 return 200 200 } 201 201 202 - ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.SpindleNSID, user.Active.Did, instance) 202 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.SpindleNSID, user.Did, instance) 203 203 var exCid *string 204 204 if ex != nil { 205 205 exCid = ex.Cid ··· 208 208 // re-announce by registering under same rkey 209 209 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 210 210 Collection: tangled.SpindleNSID, 211 - Repo: user.Active.Did, 211 + Repo: user.Did, 212 212 Rkey: instance, 213 213 Record: &lexutil.LexiconTypeDecoder{ 214 214 Val: &tangled.Spindle{ ··· 239 239 } 240 240 241 241 // begin verification 242 - err = serververify.RunVerification(r.Context(), instance, user.Active.Did, s.Config.Core.Dev) 242 + err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 243 243 if err != nil { 244 244 l.Error("verification failed", "err", err) 245 245 s.Pages.HxRefresh(w) 246 246 return 247 247 } 248 248 249 - _, err = serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Active.Did) 249 + _, err = serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did) 250 250 if err != nil { 251 251 l.Error("failed to mark verified", "err", err) 252 252 s.Pages.HxRefresh(w) ··· 276 276 277 277 spindles, err := db.GetSpindles( 278 278 s.Db, 279 - orm.FilterEq("owner", user.Active.Did), 279 + orm.FilterEq("owner", user.Did), 280 280 orm.FilterEq("instance", instance), 281 281 ) 282 282 if err != nil || len(spindles) != 1 { ··· 285 285 return 286 286 } 287 287 288 - if string(spindles[0].Owner) != user.Active.Did { 289 - l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner) 288 + if string(spindles[0].Owner) != user.Did { 289 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 290 290 s.Pages.Notice(w, noticeId, "Failed to delete spindle, unauthorized deletion attempt.") 291 291 return 292 292 } ··· 305 305 // remove spindle members first 306 306 err = db.RemoveSpindleMember( 307 307 tx, 308 - orm.FilterEq("did", user.Active.Did), 308 + orm.FilterEq("did", user.Did), 309 309 orm.FilterEq("instance", instance), 310 310 ) 311 311 if err != nil { ··· 316 316 317 317 err = db.DeleteSpindle( 318 318 tx, 319 - orm.FilterEq("owner", user.Active.Did), 319 + orm.FilterEq("owner", user.Did), 320 320 orm.FilterEq("instance", instance), 321 321 ) 322 322 if err != nil { ··· 344 344 345 345 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 346 346 Collection: tangled.SpindleNSID, 347 - Repo: user.Active.Did, 347 + Repo: user.Did, 348 348 Rkey: instance, 349 349 }) 350 350 if err != nil { ··· 392 392 return 393 393 } 394 394 l = l.With("instance", instance) 395 - l = l.With("user", user.Active.Did) 395 + l = l.With("user", user.Did) 396 396 397 397 spindles, err := db.GetSpindles( 398 398 s.Db, 399 - orm.FilterEq("owner", user.Active.Did), 399 + orm.FilterEq("owner", user.Did), 400 400 orm.FilterEq("instance", instance), 401 401 ) 402 402 if err != nil || len(spindles) != 1 { ··· 405 405 return 406 406 } 407 407 408 - if string(spindles[0].Owner) != user.Active.Did { 409 - l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner) 408 + if string(spindles[0].Owner) != user.Did { 409 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 410 410 s.Pages.Notice(w, noticeId, "Failed to verify spindle, unauthorized verification attempt.") 411 411 return 412 412 } 413 413 414 414 // begin verification 415 - err = serververify.RunVerification(r.Context(), instance, user.Active.Did, s.Config.Core.Dev) 415 + err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 416 416 if err != nil { 417 417 l.Error("verification failed", "err", err) 418 418 ··· 430 430 return 431 431 } 432 432 433 - rowId, err := serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Active.Did) 433 + rowId, err := serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did) 434 434 if err != nil { 435 435 l.Error("failed to mark verified", "err", err) 436 436 s.Pages.Notice(w, noticeId, err.Error()) ··· 468 468 return 469 469 } 470 470 l = l.With("instance", instance) 471 - l = l.With("user", user.Active.Did) 471 + l = l.With("user", user.Did) 472 472 473 473 spindles, err := db.GetSpindles( 474 474 s.Db, 475 - orm.FilterEq("owner", user.Active.Did), 475 + orm.FilterEq("owner", user.Did), 476 476 orm.FilterEq("instance", instance), 477 477 ) 478 478 if err != nil || len(spindles) != 1 { ··· 487 487 s.Pages.Notice(w, noticeId, defaultErr) 488 488 } 489 489 490 - if string(spindles[0].Owner) != user.Active.Did { 491 - l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner) 490 + if string(spindles[0].Owner) != user.Did { 491 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 492 492 s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.") 493 493 return 494 494 } ··· 537 537 538 538 // add member to db 539 539 if err = db.AddSpindleMember(tx, models.SpindleMember{ 540 - Did: syntax.DID(user.Active.Did), 540 + Did: syntax.DID(user.Did), 541 541 Rkey: rkey, 542 542 Instance: instance, 543 543 Subject: memberId.DID, ··· 555 555 556 556 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 557 557 Collection: tangled.SpindleMemberNSID, 558 - Repo: user.Active.Did, 558 + Repo: user.Did, 559 559 Rkey: rkey, 560 560 Record: &lexutil.LexiconTypeDecoder{ 561 561 Val: &tangled.SpindleMember{ ··· 604 604 return 605 605 } 606 606 l = l.With("instance", instance) 607 - l = l.With("user", user.Active.Did) 607 + l = l.With("user", user.Did) 608 608 609 609 spindles, err := db.GetSpindles( 610 610 s.Db, 611 - orm.FilterEq("owner", user.Active.Did), 611 + orm.FilterEq("owner", user.Did), 612 612 orm.FilterEq("instance", instance), 613 613 ) 614 614 if err != nil || len(spindles) != 1 { ··· 617 617 return 618 618 } 619 619 620 - if string(spindles[0].Owner) != user.Active.Did { 621 - l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner) 620 + if string(spindles[0].Owner) != user.Did { 621 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 622 622 s.Pages.Notice(w, noticeId, "Failed to remove member, unauthorized attempt.") 623 623 return 624 624 } ··· 653 653 // get the record from the DB first: 654 654 members, err := db.GetSpindleMembers( 655 655 s.Db, 656 - orm.FilterEq("did", user.Active.Did), 656 + orm.FilterEq("did", user.Did), 657 657 orm.FilterEq("instance", instance), 658 658 orm.FilterEq("subject", memberId.DID), 659 659 ) ··· 666 666 // remove from db 667 667 if err = db.RemoveSpindleMember( 668 668 tx, 669 - orm.FilterEq("did", user.Active.Did), 669 + orm.FilterEq("did", user.Did), 670 670 orm.FilterEq("instance", instance), 671 671 orm.FilterEq("subject", memberId.DID), 672 672 ); err != nil { ··· 692 692 // remove from pds 693 693 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 694 694 Collection: tangled.SpindleMemberNSID, 695 - Repo: user.Active.Did, 695 + Repo: user.Did, 696 696 Rkey: members[0].Rkey, 697 697 }) 698 698 if err != nil {
+1 -1
appview/state/accounts.go
··· 41 41 } 42 42 43 43 currentUser := s.oauth.GetMultiAccountUser(r) 44 - isCurrentAccount := currentUser != nil && currentUser.Active.Did == did 44 + isCurrentAccount := currentUser != nil && currentUser.Did == did 45 45 46 46 var remainingAccounts []string 47 47 if currentUser != nil {
+6 -6
appview/state/follow.go
··· 29 29 return 30 30 } 31 31 32 - if currentUser.Active.Did == subjectIdent.DID.String() { 32 + if currentUser.Did == subjectIdent.DID.String() { 33 33 log.Println("cant follow or unfollow yourself") 34 34 return 35 35 } ··· 46 46 rkey := tid.TID() 47 47 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 48 48 Collection: tangled.GraphFollowNSID, 49 - Repo: currentUser.Active.Did, 49 + Repo: currentUser.Did, 50 50 Rkey: rkey, 51 51 Record: &lexutil.LexiconTypeDecoder{ 52 52 Val: &tangled.GraphFollow{ ··· 62 62 log.Println("created atproto record: ", resp.Uri) 63 63 64 64 follow := &models.Follow{ 65 - UserDid: currentUser.Active.Did, 65 + UserDid: currentUser.Did, 66 66 SubjectDid: subjectIdent.DID.String(), 67 67 Rkey: rkey, 68 68 } ··· 89 89 return 90 90 case http.MethodDelete: 91 91 // find the record in the db 92 - follow, err := db.GetFollow(s.db, currentUser.Active.Did, subjectIdent.DID.String()) 92 + follow, err := db.GetFollow(s.db, currentUser.Did, subjectIdent.DID.String()) 93 93 if err != nil { 94 94 log.Println("failed to get follow relationship") 95 95 return ··· 97 97 98 98 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 99 99 Collection: tangled.GraphFollowNSID, 100 - Repo: currentUser.Active.Did, 100 + Repo: currentUser.Did, 101 101 Rkey: follow.Rkey, 102 102 }) 103 103 ··· 106 106 return 107 107 } 108 108 109 - err = db.DeleteFollowByRkey(s.db, currentUser.Active.Did, follow.Rkey) 109 + err = db.DeleteFollowByRkey(s.db, currentUser.Did, follow.Rkey) 110 110 if err != nil { 111 111 log.Println("failed to delete follow from DB") 112 112 // this is not an issue, the firehose event might have already done this
+66
appview/state/legacy_bridge.go
··· 1 + package state 2 + 3 + import ( 4 + "log/slog" 5 + 6 + "tangled.org/core/appview/config" 7 + "tangled.org/core/appview/db" 8 + "tangled.org/core/appview/indexer" 9 + "tangled.org/core/appview/issues" 10 + "tangled.org/core/appview/mentions" 11 + "tangled.org/core/appview/middleware" 12 + "tangled.org/core/appview/notify" 13 + "tangled.org/core/appview/oauth" 14 + "tangled.org/core/appview/pages" 15 + "tangled.org/core/appview/validator" 16 + "tangled.org/core/idresolver" 17 + "tangled.org/core/log" 18 + "tangled.org/core/rbac" 19 + ) 20 + 21 + // Expose exposes private fields in `State`. This is used to bridge between 22 + // legacy web routers and new architecture 23 + func (s *State) Expose() ( 24 + *config.Config, 25 + *db.DB, 26 + *rbac.Enforcer, 27 + *idresolver.Resolver, 28 + *mentions.Resolver, 29 + *indexer.Indexer, 30 + *slog.Logger, 31 + notify.Notifier, 32 + *oauth.OAuth, 33 + *pages.Pages, 34 + *validator.Validator, 35 + ) { 36 + return s.config, s.db, s.enforcer, s.idResolver, s.mentionsResolver, s.indexer, s.logger, s.notifier, s.oauth, s.pages, s.validator 37 + } 38 + 39 + func (s *State) ExposeIssue() *issues.Issues { 40 + return issues.New( 41 + s.oauth, 42 + s.repoResolver, 43 + s.enforcer, 44 + s.pages, 45 + s.idResolver, 46 + s.mentionsResolver, 47 + s.db, 48 + s.config, 49 + s.notifier, 50 + s.validator, 51 + s.indexer.Issues, 52 + log.SubLogger(s.logger, "issues"), 53 + ) 54 + } 55 + 56 + func (s *State) Middleware() *middleware.Middleware { 57 + mw := middleware.New( 58 + s.oauth, 59 + s.db, 60 + s.enforcer, 61 + s.repoResolver, 62 + s.idResolver, 63 + s.pages, 64 + ) 65 + return &mw 66 + }
+8 -19
appview/state/login.go
··· 5 5 "net/http" 6 6 "strings" 7 7 8 - "tangled.org/core/appview/oauth" 9 8 "tangled.org/core/appview/pages" 10 9 ) 11 10 ··· 18 17 errorCode := r.URL.Query().Get("error") 19 18 addAccount := r.URL.Query().Get("mode") == "add_account" 20 19 21 - user := s.oauth.GetMultiAccountUser(r) 22 - if user == nil { 23 - registry := s.oauth.GetAccounts(r) 24 - if len(registry.Accounts) > 0 { 25 - user = &oauth.MultiAccountUser{ 26 - Active: nil, 27 - Accounts: registry.Accounts, 28 - } 29 - } 30 - } 20 + registry := s.oauth.GetAccounts(r) 31 21 s.pages.Login(w, pages.LoginParams{ 32 - ReturnUrl: returnURL, 33 - ErrorCode: errorCode, 34 - AddAccount: addAccount, 35 - LoggedInUser: user, 22 + ReturnUrl: returnURL, 23 + ErrorCode: errorCode, 24 + AddAccount: addAccount, 25 + Accounts: registry.Accounts, 36 26 }) 37 27 case http.MethodPost: 38 28 handle := r.FormValue("handle") 39 29 returnURL := r.FormValue("return_url") 40 - addAccount := r.FormValue("add_account") == "true" 41 30 42 31 // remove spaces around the handle, handles can't have spaces around them 43 32 handle = strings.TrimSpace(handle) ··· 64 53 return 65 54 } 66 55 67 - if err := s.oauth.SetAuthReturn(w, r, returnURL, addAccount); err != nil { 56 + if err := s.oauth.SetAuthReturn(w, r, returnURL); err != nil { 68 57 l.Error("failed to set auth return", "err", err) 69 58 } 70 59 ··· 87 76 l := s.logger.With("handler", "Logout") 88 77 89 78 currentUser := s.oauth.GetMultiAccountUser(r) 90 - if currentUser == nil || currentUser.Active == nil { 79 + if currentUser == nil { 91 80 s.pages.HxRedirect(w, "/login") 92 81 return 93 82 } 94 83 95 - currentDid := currentUser.Active.Did 84 + currentDid := currentUser.Did 96 85 97 86 var remainingAccounts []string 98 87 for _, acc := range currentUser.Accounts {
+27 -31
appview/state/profile.go
··· 86 86 loggedInUser := s.oauth.GetMultiAccountUser(r) 87 87 followStatus := models.IsNotFollowing 88 88 if loggedInUser != nil { 89 - followStatus = db.GetFollowStatus(s.db, loggedInUser.Active.Did, did) 89 + followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did) 90 90 } 91 91 92 - var loggedInDid string 93 - if loggedInUser != nil { 94 - loggedInDid = loggedInUser.Did() 95 - } 96 - showPunchcard := s.shouldShowPunchcard(did, loggedInDid) 92 + showPunchcard := s.shouldShowPunchcard(did, loggedInUser.Did) 97 93 98 94 var punchcard *models.Punchcard 99 95 if showPunchcard { ··· 352 348 353 349 loggedInUserFollowing := make(map[string]struct{}) 354 350 if loggedInUser != nil { 355 - following, err := db.GetFollowing(s.db, loggedInUser.Active.Did) 351 + following, err := db.GetFollowing(s.db, loggedInUser.Did) 356 352 if err != nil { 357 - l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Active.Did) 353 + l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Did) 358 354 return &params, err 359 355 } 360 356 loggedInUserFollowing = make(map[string]struct{}, len(following)) ··· 369 365 followStatus := models.IsNotFollowing 370 366 if _, exists := loggedInUserFollowing[did]; exists { 371 367 followStatus = models.IsFollowing 372 - } else if loggedInUser != nil && loggedInUser.Active.Did == did { 368 + } else if loggedInUser != nil && loggedInUser.Did == did { 373 369 followStatus = models.IsSelf 374 370 } 375 371 ··· 575 571 return 576 572 } 577 573 578 - profile, err := db.GetProfile(s.db, user.Active.Did) 574 + profile, err := db.GetProfile(s.db, user.Did) 579 575 if err != nil { 580 - log.Printf("getting profile data for %s: %s", user.Active.Did, err) 576 + log.Printf("getting profile data for %s: %s", user.Did, err) 581 577 } 582 578 if profile == nil { 583 - profile = &models.Profile{Did: user.Active.Did} 579 + profile = &models.Profile{Did: user.Did} 584 580 } 585 581 586 582 profile.Description = r.FormValue("description") ··· 621 617 return 622 618 } 623 619 624 - profile, err := db.GetProfile(s.db, user.Active.Did) 620 + profile, err := db.GetProfile(s.db, user.Did) 625 621 if err != nil { 626 - log.Printf("getting profile data for %s: %s", user.Active.Did, err) 622 + log.Printf("getting profile data for %s: %s", user.Did, err) 627 623 } 628 624 if profile == nil { 629 - profile = &models.Profile{Did: user.Active.Did} 625 + profile = &models.Profile{Did: user.Did} 630 626 } 631 627 632 628 i := 0 ··· 681 677 vanityStats = append(vanityStats, string(v.Kind)) 682 678 } 683 679 684 - ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Active.Did, "self") 680 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Did, "self") 685 681 var cid *string 686 682 if ex != nil { 687 683 cid = ex.Cid ··· 689 685 690 686 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 691 687 Collection: tangled.ActorProfileNSID, 692 - Repo: user.Active.Did, 688 + Repo: user.Did, 693 689 Rkey: "self", 694 690 Record: &lexutil.LexiconTypeDecoder{ 695 691 Val: &tangled.ActorProfile{ ··· 718 714 719 715 s.notifier.UpdateProfile(r.Context(), profile) 720 716 721 - s.pages.HxRedirect(w, "/"+user.Active.Did) 717 + s.pages.HxRedirect(w, "/"+user.Did) 722 718 } 723 719 724 720 func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) { 725 721 user := s.oauth.GetMultiAccountUser(r) 726 722 727 - profile, err := db.GetProfile(s.db, user.Active.Did) 723 + profile, err := db.GetProfile(s.db, user.Did) 728 724 if err != nil { 729 - log.Printf("getting profile data for %s: %s", user.Active.Did, err) 725 + log.Printf("getting profile data for %s: %s", user.Did, err) 730 726 } 731 727 if profile == nil { 732 - profile = &models.Profile{Did: user.Active.Did} 728 + profile = &models.Profile{Did: user.Did} 733 729 } 734 730 735 731 s.pages.EditBioFragment(w, pages.EditBioParams{ ··· 741 737 func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) { 742 738 user := s.oauth.GetMultiAccountUser(r) 743 739 744 - profile, err := db.GetProfile(s.db, user.Active.Did) 740 + profile, err := db.GetProfile(s.db, user.Did) 745 741 if err != nil { 746 - log.Printf("getting profile data for %s: %s", user.Active.Did, err) 742 + log.Printf("getting profile data for %s: %s", user.Did, err) 747 743 } 748 744 if profile == nil { 749 - profile = &models.Profile{Did: user.Active.Did} 745 + profile = &models.Profile{Did: user.Did} 750 746 } 751 747 752 - repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Active.Did)) 748 + repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Did)) 753 749 if err != nil { 754 - log.Printf("getting repos for %s: %s", user.Active.Did, err) 750 + log.Printf("getting repos for %s: %s", user.Did, err) 755 751 } 756 752 757 - collaboratingRepos, err := db.CollaboratingIn(s.db, user.Active.Did) 753 + collaboratingRepos, err := db.CollaboratingIn(s.db, user.Did) 758 754 if err != nil { 759 - log.Printf("getting collaborating repos for %s: %s", user.Active.Did, err) 755 + log.Printf("getting collaborating repos for %s: %s", user.Did, err) 760 756 } 761 757 762 758 allRepos := []pages.PinnedRepo{} ··· 785 781 786 782 func (s *State) UploadProfileAvatar(w http.ResponseWriter, r *http.Request) { 787 783 l := s.logger.With("handler", "UploadProfileAvatar") 788 - user := s.oauth.GetUser(r) 784 + user := s.oauth.GetMultiAccountUser(r) 789 785 l = l.With("did", user.Did) 790 786 791 787 // Parse multipart form (10MB max) ··· 901 897 902 898 func (s *State) RemoveProfileAvatar(w http.ResponseWriter, r *http.Request) { 903 899 l := s.logger.With("handler", "RemoveProfileAvatar") 904 - user := s.oauth.GetUser(r) 900 + user := s.oauth.GetMultiAccountUser(r) 905 901 l = l.With("did", user.Did) 906 902 907 903 client, err := s.oauth.AuthorizedClient(r) ··· 983 979 log.Println("invalid profile update form", err) 984 980 return 985 981 } 986 - user := s.oauth.GetUser(r) 982 + user := s.oauth.GetMultiAccountUser(r) 987 983 988 984 hideOthers := false 989 985 hideMine := false
+6 -6
appview/state/reaction.go
··· 49 49 rkey := tid.TID() 50 50 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 51 51 Collection: tangled.FeedReactionNSID, 52 - Repo: currentUser.Active.Did, 52 + Repo: currentUser.Did, 53 53 Rkey: rkey, 54 54 Record: &lexutil.LexiconTypeDecoder{ 55 55 Val: &tangled.FeedReaction{ ··· 64 64 return 65 65 } 66 66 67 - err = db.AddReaction(s.db, currentUser.Active.Did, subjectUri, reactionKind, rkey) 67 + err = db.AddReaction(s.db, currentUser.Did, subjectUri, reactionKind, rkey) 68 68 if err != nil { 69 69 log.Println("failed to react", err) 70 70 return ··· 87 87 88 88 return 89 89 case http.MethodDelete: 90 - reaction, err := db.GetReaction(s.db, currentUser.Active.Did, subjectUri, reactionKind) 90 + reaction, err := db.GetReaction(s.db, currentUser.Did, subjectUri, reactionKind) 91 91 if err != nil { 92 - log.Println("failed to get reaction relationship for", currentUser.Active.Did, subjectUri) 92 + log.Println("failed to get reaction relationship for", currentUser.Did, subjectUri) 93 93 return 94 94 } 95 95 96 96 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 97 97 Collection: tangled.FeedReactionNSID, 98 - Repo: currentUser.Active.Did, 98 + Repo: currentUser.Did, 99 99 Rkey: reaction.Rkey, 100 100 }) 101 101 ··· 104 104 return 105 105 } 106 106 107 - err = db.DeleteReactionByRkey(s.db, currentUser.Active.Did, reaction.Rkey) 107 + err = db.DeleteReactionByRkey(s.db, currentUser.Did, reaction.Rkey) 108 108 if err != nil { 109 109 log.Println("failed to delete reaction from DB") 110 110 // this is not an issue, the firehose event might have already done this
+7 -6
appview/state/router.go
··· 210 210 211 211 func (s *State) SettingsRouter() http.Handler { 212 212 settings := &settings.Settings{ 213 - Db: s.db, 214 - OAuth: s.oauth, 215 - Pages: s.pages, 216 - Config: s.config, 217 - CfClient: s.cfClient, 218 - Logger: log.SubLogger(s.logger, "settings"), 213 + Db: s.db, 214 + OAuth: s.oauth, 215 + Pages: s.pages, 216 + Config: s.config, 217 + CfClient: s.cfClient, 218 + Logger: log.SubLogger(s.logger, "settings"), 219 + IdResolver: s.idResolver, 219 220 } 220 221 221 222 return settings.Router()
+5 -5
appview/state/star.go
··· 42 42 rkey := tid.TID() 43 43 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 44 44 Collection: tangled.FeedStarNSID, 45 - Repo: currentUser.Active.Did, 45 + Repo: currentUser.Did, 46 46 Rkey: rkey, 47 47 Record: &lexutil.LexiconTypeDecoder{ 48 48 Val: &tangled.FeedStar{ ··· 57 57 log.Println("created atproto record: ", resp.Uri) 58 58 59 59 star := &models.Star{ 60 - Did: currentUser.Active.Did, 60 + Did: currentUser.Did, 61 61 RepoAt: subjectUri, 62 62 Rkey: rkey, 63 63 } ··· 84 84 return 85 85 case http.MethodDelete: 86 86 // find the record in the db 87 - star, err := db.GetStar(s.db, currentUser.Active.Did, subjectUri) 87 + star, err := db.GetStar(s.db, currentUser.Did, subjectUri) 88 88 if err != nil { 89 89 log.Println("failed to get star relationship") 90 90 return ··· 92 92 93 93 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 94 94 Collection: tangled.FeedStarNSID, 95 - Repo: currentUser.Active.Did, 95 + Repo: currentUser.Did, 96 96 Rkey: star.Rkey, 97 97 }) 98 98 ··· 101 101 return 102 102 } 103 103 104 - err = db.DeleteStarByRkey(s.db, currentUser.Active.Did, star.Rkey) 104 + err = db.DeleteStarByRkey(s.db, currentUser.Did, star.Rkey) 105 105 if err != nil { 106 106 log.Println("failed to delete star from DB") 107 107 // this is not an issue, the firehose event might have already done this
+15 -14
appview/state/state.go
··· 38 38 "tangled.org/core/tid" 39 39 40 40 comatproto "github.com/bluesky-social/indigo/api/atproto" 41 - atpclient "github.com/bluesky-social/indigo/atproto/client" 41 + "github.com/bluesky-social/indigo/atproto/atclient" 42 42 "github.com/bluesky-social/indigo/atproto/syntax" 43 43 lexutil "github.com/bluesky-social/indigo/lex/util" 44 44 "github.com/bluesky-social/indigo/xrpc" ··· 124 124 tangled.StringNSID, 125 125 tangled.RepoIssueNSID, 126 126 tangled.RepoIssueCommentNSID, 127 + tangled.CommentNSID, 127 128 tangled.LabelDefinitionNSID, 128 129 tangled.LabelOpNSID, 129 130 }, ··· 271 272 } 272 273 273 274 l := s.logger.With("handler", "UpgradeBanner") 274 - l = l.With("did", user.Active.Did) 275 + l = l.With("did", user.Did) 275 276 276 277 regs, err := db.GetRegistrations( 277 278 s.db, 278 - orm.FilterEq("did", user.Active.Did), 279 + orm.FilterEq("did", user.Did), 279 280 orm.FilterEq("needs_upgrade", 1), 280 281 ) 281 282 if err != nil { ··· 284 285 285 286 spindles, err := db.GetSpindles( 286 287 s.db, 287 - orm.FilterEq("owner", user.Active.Did), 288 + orm.FilterEq("owner", user.Did), 288 289 orm.FilterEq("needs_upgrade", 1), 289 290 ) 290 291 if err != nil { ··· 374 375 switch r.Method { 375 376 case http.MethodGet: 376 377 user := s.oauth.GetMultiAccountUser(r) 377 - knots, err := s.enforcer.GetKnotsForUser(user.Active.Did) 378 + knots, err := s.enforcer.GetKnotsForUser(user.Did) 378 379 if err != nil { 379 380 s.pages.Notice(w, "repo", "Invalid user account.") 380 381 return ··· 389 390 l := s.logger.With("handler", "NewRepo") 390 391 391 392 user := s.oauth.GetMultiAccountUser(r) 392 - l = l.With("did", user.Active.Did) 393 + l = l.With("did", user.Did) 393 394 394 395 // form validation 395 396 domain := r.FormValue("domain") ··· 425 426 } 426 427 427 428 // ACL validation 428 - ok, err := s.enforcer.E.Enforce(user.Active.Did, domain, domain, "repo:create") 429 + ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 429 430 if err != nil || !ok { 430 431 l.Info("unauthorized") 431 432 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") ··· 435 436 // Check for existing repos 436 437 existingRepo, err := db.GetRepo( 437 438 s.db, 438 - orm.FilterEq("did", user.Active.Did), 439 + orm.FilterEq("did", user.Did), 439 440 orm.FilterEq("name", repoName), 440 441 ) 441 442 if err == nil && existingRepo != nil { ··· 447 448 // create atproto record for this repo 448 449 rkey := tid.TID() 449 450 repo := &models.Repo{ 450 - Did: user.Active.Did, 451 + Did: user.Did, 451 452 Name: repoName, 452 453 Knot: domain, 453 454 Rkey: rkey, ··· 466 467 467 468 atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{ 468 469 Collection: tangled.RepoNSID, 469 - Repo: user.Active.Did, 470 + Repo: user.Did, 470 471 Rkey: rkey, 471 472 Record: &lexutil.LexiconTypeDecoder{ 472 473 Val: &record, ··· 543 544 } 544 545 545 546 // acls 546 - p, _ := securejoin.SecureJoin(user.Active.Did, repoName) 547 - err = s.enforcer.AddRepo(user.Active.Did, domain, p) 547 + p, _ := securejoin.SecureJoin(user.Did, repoName) 548 + err = s.enforcer.AddRepo(user.Did, domain, p) 548 549 if err != nil { 549 550 l.Error("acl setup failed", "err", err) 550 551 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 569 570 aturi = "" 570 571 571 572 s.notifier.NewRepo(r.Context(), repo) 572 - s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, repoName)) 573 + s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Did, repoName)) 573 574 } 574 575 } 575 576 576 577 // this is used to rollback changes made to the PDS 577 578 // 578 579 // it is a no-op if the provided ATURI is empty 579 - func rollbackRecord(ctx context.Context, aturi string, client *atpclient.APIClient) error { 580 + func rollbackRecord(ctx context.Context, aturi string, client *atclient.APIClient) error { 580 581 if aturi == "" { 581 582 return nil 582 583 }
+2 -2
appview/state/timeline.go
··· 47 47 filtered := false 48 48 49 49 var userDid string 50 - if user != nil && user.Active != nil { 51 - userDid = user.Active.Did 50 + if user != nil { 51 + userDid = user.Did 52 52 } 53 53 timeline, err := db.MakeTimeline(s.db, 50, userDid, filtered) 54 54 if err != nil {
+13 -13
appview/strings/strings.go
··· 156 156 user := s.OAuth.GetMultiAccountUser(r) 157 157 isStarred := false 158 158 if user != nil { 159 - isStarred = db.GetStarStatus(s.Db, user.Active.Did, string.AtUri()) 159 + isStarred = db.GetStarStatus(s.Db, user.Did, string.AtUri()) 160 160 } 161 161 162 162 s.Pages.SingleString(w, pages.SingleStringParams{ ··· 216 216 first := all[0] 217 217 218 218 // verify that the logged in user owns this string 219 - if user.Active.Did != id.DID.String() { 220 - l.Error("unauthorized request", "expected", id.DID, "got", user.Active.Did) 219 + if user.Did != id.DID.String() { 220 + l.Error("unauthorized request", "expected", id.DID, "got", user.Did) 221 221 w.WriteHeader(http.StatusUnauthorized) 222 222 return 223 223 } ··· 299 299 s.Notifier.EditString(r.Context(), &entry) 300 300 301 301 // if that went okay, redir to the string 302 - s.Pages.HxRedirect(w, "/strings/"+user.Active.Did+"/"+entry.Rkey) 302 + s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+entry.Rkey) 303 303 } 304 304 305 305 } ··· 335 335 description := r.FormValue("description") 336 336 337 337 string := models.String{ 338 - Did: syntax.DID(user.Active.Did), 338 + Did: syntax.DID(user.Did), 339 339 Rkey: tid.TID(), 340 340 Filename: filename, 341 341 Description: description, ··· 353 353 354 354 resp, err := comatproto.RepoPutRecord(r.Context(), client, &atproto.RepoPutRecord_Input{ 355 355 Collection: tangled.StringNSID, 356 - Repo: user.Active.Did, 356 + Repo: user.Did, 357 357 Rkey: string.Rkey, 358 358 Record: &lexutil.LexiconTypeDecoder{ 359 359 Val: &record, ··· 375 375 s.Notifier.NewString(r.Context(), &string) 376 376 377 377 // successful 378 - s.Pages.HxRedirect(w, "/strings/"+user.Active.Did+"/"+string.Rkey) 378 + s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+string.Rkey) 379 379 } 380 380 } 381 381 ··· 402 402 return 403 403 } 404 404 405 - if user.Active.Did != id.DID.String() { 406 - fail("You cannot delete this string", fmt.Errorf("unauthorized deletion, %s != %s", user.Active.Did, id.DID.String())) 405 + if user.Did != id.DID.String() { 406 + fail("You cannot delete this string", fmt.Errorf("unauthorized deletion, %s != %s", user.Did, id.DID.String())) 407 407 return 408 408 } 409 409 ··· 415 415 416 416 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 417 417 Collection: tangled.StringNSID, 418 - Repo: user.Active.Did, 418 + Repo: user.Did, 419 419 Rkey: rkey, 420 420 }) 421 421 if err != nil { ··· 425 425 426 426 if err := db.DeleteString( 427 427 s.Db, 428 - orm.FilterEq("did", user.Active.Did), 428 + orm.FilterEq("did", user.Did), 429 429 orm.FilterEq("rkey", rkey), 430 430 ); err != nil { 431 431 fail("Failed to delete string.", err) 432 432 return 433 433 } 434 434 435 - s.Notifier.DeleteString(r.Context(), user.Active.Did, rkey) 435 + s.Notifier.DeleteString(r.Context(), user.Did, rkey) 436 436 437 - s.Pages.HxRedirect(w, "/strings/"+user.Active.Did) 437 + s.Pages.HxRedirect(w, "/strings/"+user.Did) 438 438 } 439 439 440 440 func (s *Strings) comment(w http.ResponseWriter, r *http.Request) {
-27
appview/validator/issue.go
··· 4 4 "fmt" 5 5 "strings" 6 6 7 - "tangled.org/core/appview/db" 8 7 "tangled.org/core/appview/models" 9 - "tangled.org/core/orm" 10 8 ) 11 - 12 - func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error { 13 - // if comments have parents, only ingest ones that are 1 level deep 14 - if comment.ReplyTo != nil { 15 - parents, err := db.GetIssueComments(v.db, orm.FilterEq("at_uri", *comment.ReplyTo)) 16 - if err != nil { 17 - return fmt.Errorf("failed to fetch parent comment: %w", err) 18 - } 19 - if len(parents) != 1 { 20 - return fmt.Errorf("incorrect number of parent comments returned: %d", len(parents)) 21 - } 22 - 23 - // depth check 24 - parent := parents[0] 25 - if parent.ReplyTo != nil { 26 - return fmt.Errorf("incorrect depth, this comment is replying at depth >1") 27 - } 28 - } 29 - 30 - if sb := strings.TrimSpace(v.sanitizer.SanitizeDefault(comment.Body)); sb == "" { 31 - return fmt.Errorf("body is empty after HTML sanitization") 32 - } 33 - 34 - return nil 35 - } 36 9 37 10 func (v *Validator) ValidateIssue(issue *models.Issue) error { 38 11 if issue.Title == "" {
+34
appview/web/handler/oauth.go
··· 1 + package handler 2 + 3 + import ( 4 + "encoding/json" 5 + "net/http" 6 + 7 + "tangled.org/core/appview/oauth" 8 + ) 9 + 10 + func OauthClientMetadata(o *oauth.OAuth) http.HandlerFunc { 11 + return func(w http.ResponseWriter, r *http.Request) { 12 + doc := o.ClientApp.Config.ClientMetadata() 13 + doc.JWKSURI = &o.JwksUri 14 + doc.ClientName = &o.ClientName 15 + doc.ClientURI = &o.ClientUri 16 + 17 + w.Header().Set("Content-Type", "application/json") 18 + if err := json.NewEncoder(w).Encode(doc); err != nil { 19 + http.Error(w, err.Error(), http.StatusInternalServerError) 20 + return 21 + } 22 + } 23 + } 24 + 25 + func OauthJwks(o *oauth.OAuth) http.HandlerFunc { 26 + return func(w http.ResponseWriter, r *http.Request) { 27 + w.Header().Set("Content-Type", "application/json") 28 + body := o.ClientApp.Config.PublicJWKS() 29 + if err := json.NewEncoder(w).Encode(body); err != nil { 30 + http.Error(w, err.Error(), http.StatusInternalServerError) 31 + return 32 + } 33 + } 34 + }
+390
appview/web/handler/user_repo_issues.go
··· 1 + package handler 2 + 3 + import ( 4 + "errors" 5 + "fmt" 6 + "net/http" 7 + 8 + "tangled.org/core/api/tangled" 9 + "tangled.org/core/appview/db" 10 + "tangled.org/core/appview/models" 11 + "tangled.org/core/appview/pages" 12 + "tangled.org/core/appview/pagination" 13 + "tangled.org/core/appview/reporesolver" 14 + "tangled.org/core/appview/searchquery" 15 + isvc "tangled.org/core/appview/service/issue" 16 + rsvc "tangled.org/core/appview/service/repo" 17 + "tangled.org/core/appview/session" 18 + "tangled.org/core/appview/web/request" 19 + "tangled.org/core/log" 20 + "tangled.org/core/orm" 21 + ) 22 + 23 + func RepoIssues(is isvc.Service, rs rsvc.Service, p *pages.Pages, d *db.DB) http.HandlerFunc { 24 + return func(w http.ResponseWriter, r *http.Request) { 25 + ctx := r.Context() 26 + l := log.FromContext(ctx).With("handler", "RepoIssues") 27 + repo, ok := request.RepoFromContext(ctx) 28 + if !ok { 29 + l.Error("malformed request") 30 + p.Error503(w) 31 + return 32 + } 33 + repoOwnerId, ok := request.OwnerFromContext(ctx) 34 + if !ok { 35 + l.Error("malformed request") 36 + p.Error503(w) 37 + return 38 + } 39 + 40 + params := r.URL.Query() 41 + page := pagination.FromContext(r.Context()) 42 + 43 + query := searchquery.Parse(params.Get("q")) 44 + 45 + // resolve := func(ctx context.Context, ident string) (string, error) { 46 + // id, err := s.idResolver.ResolveIdent(ctx, ident) 47 + // if err != nil { 48 + // return "", err 49 + // } 50 + // return id.DID.String(), nil 51 + // } 52 + 53 + // authorDid, negatedAuthorDids := searchquery.ResolveAuthor(r.Context(), query, resolve, l) 54 + 55 + labels := query.GetAll("label") 56 + negatedLabels := query.GetAllNegated("label") 57 + labelValues := query.GetDynamicTags() 58 + negatedLabelValues := query.GetNegatedDynamicTags() 59 + 60 + tf := searchquery.ExtractTextFilters(query) 61 + 62 + isOpen := true 63 + 64 + searchOpts := models.IssueSearchOptions{ 65 + Keywords: tf.Keywords, 66 + Phrases: tf.Phrases, 67 + RepoAt: repo.RepoAt().String(), 68 + IsOpen: &isOpen, 69 + AuthorDid: "", 70 + Labels: labels, 71 + LabelValues: labelValues, 72 + NegatedKeywords: tf.NegatedKeywords, 73 + NegatedPhrases: tf.NegatedPhrases, 74 + NegatedLabels: negatedLabels, 75 + NegatedLabelValues: negatedLabelValues, 76 + NegatedAuthorDids: nil, 77 + Page: page, 78 + } 79 + 80 + issues, err := is.GetIssues(ctx, repo, searchOpts) 81 + if err != nil { 82 + l.Error("failed to get issues") 83 + p.Error503(w) 84 + return 85 + } 86 + 87 + // render page 88 + err = func() error { 89 + labelDefs, err := db.GetLabelDefinitions( 90 + d, 91 + orm.FilterIn("at_uri", repo.Labels), 92 + orm.FilterContains("scope", tangled.RepoIssueNSID), 93 + ) 94 + if err != nil { 95 + return err 96 + } 97 + defs := make(map[string]*models.LabelDefinition) 98 + for _, l := range labelDefs { 99 + defs[l.AtUri().String()] = &l 100 + } 101 + return p.RepoIssues(w, pages.RepoIssuesParams{ 102 + LoggedInUser: session.UserFromContext(ctx), 103 + RepoInfo: rs.MakeRepoInfo(ctx, repoOwnerId, repo, "", ""), 104 + 105 + Issues: issues, 106 + LabelDefs: defs, 107 + FilterState: "open", 108 + FilterQuery: query.String(), 109 + Page: searchOpts.Page, 110 + }) 111 + }() 112 + if err != nil { 113 + l.Error("failed to render", "err", err) 114 + p.Error503(w) 115 + return 116 + } 117 + } 118 + } 119 + 120 + func Issue(s isvc.Service, rs rsvc.Service, p *pages.Pages, d *db.DB) http.HandlerFunc { 121 + return func(w http.ResponseWriter, r *http.Request) { 122 + ctx := r.Context() 123 + l := log.FromContext(ctx).With("handler", "Issue") 124 + issue, ok := request.IssueFromContext(ctx) 125 + if !ok { 126 + l.Error("malformed request, failed to get issue") 127 + p.Error503(w) 128 + return 129 + } 130 + repoOwnerId, ok := request.OwnerFromContext(ctx) 131 + if !ok { 132 + l.Error("malformed request") 133 + p.Error503(w) 134 + return 135 + } 136 + 137 + // render 138 + err := func() error { 139 + reactionMap, err := db.GetReactionMap(d, 20, issue.AtUri()) 140 + if err != nil { 141 + l.Error("failed to get issue reactions", "err", err) 142 + return err 143 + } 144 + 145 + userReactions := map[models.ReactionKind]bool{} 146 + if sess, ok := session.FromContext(ctx); ok { 147 + userReactions = db.GetReactionStatusMap(d, sess.User.Did, issue.AtUri()) 148 + } 149 + 150 + backlinks, err := db.GetBacklinks(d, issue.AtUri()) 151 + if err != nil { 152 + l.Error("failed to fetch backlinks", "err", err) 153 + return err 154 + } 155 + 156 + labelDefs, err := db.GetLabelDefinitions( 157 + d, 158 + orm.FilterIn("at_uri", issue.Repo.Labels), 159 + orm.FilterContains("scope", tangled.RepoIssueNSID), 160 + ) 161 + if err != nil { 162 + l.Error("failed to fetch label defs", "err", err) 163 + return err 164 + } 165 + 166 + defs := make(map[string]*models.LabelDefinition) 167 + for _, l := range labelDefs { 168 + defs[l.AtUri().String()] = &l 169 + } 170 + 171 + return p.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 172 + LoggedInUser: session.UserFromContext(ctx), 173 + RepoInfo: rs.MakeRepoInfo(ctx, repoOwnerId, issue.Repo, "", ""), 174 + Issue: issue, 175 + CommentList: issue.CommentList(), 176 + Backlinks: backlinks, 177 + Reactions: reactionMap, 178 + UserReacted: userReactions, 179 + LabelDefs: defs, 180 + }) 181 + }() 182 + if err != nil { 183 + l.Error("failed to render", "err", err) 184 + p.Error503(w) 185 + return 186 + } 187 + } 188 + } 189 + 190 + func NewIssue(rs rsvc.Service, p *pages.Pages) http.HandlerFunc { 191 + return func(w http.ResponseWriter, r *http.Request) { 192 + ctx := r.Context() 193 + l := log.FromContext(ctx).With("handler", "NewIssue") 194 + 195 + // render 196 + err := func() error { 197 + repo, ok := request.RepoFromContext(ctx) 198 + if !ok { 199 + return fmt.Errorf("malformed request") 200 + } 201 + repoOwnerId, ok := request.OwnerFromContext(ctx) 202 + if !ok { 203 + return fmt.Errorf("malformed request") 204 + } 205 + return p.RepoNewIssue(w, pages.RepoNewIssueParams{ 206 + LoggedInUser: session.UserFromContext(ctx), 207 + RepoInfo: rs.MakeRepoInfo(ctx, repoOwnerId, repo, "", ""), 208 + }) 209 + }() 210 + if err != nil { 211 + l.Error("failed to render", "err", err) 212 + p.Error503(w) 213 + return 214 + } 215 + } 216 + } 217 + 218 + func NewIssuePost(is isvc.Service, p *pages.Pages) http.HandlerFunc { 219 + noticeId := "issues" 220 + return func(w http.ResponseWriter, r *http.Request) { 221 + ctx := r.Context() 222 + l := log.FromContext(ctx).With("handler", "NewIssuePost") 223 + repo, ok := request.RepoFromContext(ctx) 224 + if !ok { 225 + l.Error("malformed request, failed to get repo") 226 + // TODO: 503 error with more detailed messages 227 + p.Error503(w) 228 + return 229 + } 230 + var ( 231 + title = r.FormValue("title") 232 + body = r.FormValue("body") 233 + ) 234 + 235 + issue, err := is.NewIssue(ctx, repo, title, body) 236 + if err != nil { 237 + if errors.Is(err, isvc.ErrDatabaseFail) { 238 + p.Notice(w, noticeId, "Failed to create issue.") 239 + } else if errors.Is(err, isvc.ErrPDSFail) { 240 + p.Notice(w, noticeId, "Failed to create issue.") 241 + } else { 242 + p.Notice(w, noticeId, "Failed to create issue.") 243 + } 244 + return 245 + } 246 + ownerSlashRepo := reporesolver.GetBaseRepoPath(r, issue.Repo) 247 + p.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) 248 + } 249 + } 250 + 251 + func IssueEdit(is isvc.Service, rs rsvc.Service, p *pages.Pages) http.HandlerFunc { 252 + return func(w http.ResponseWriter, r *http.Request) { 253 + ctx := r.Context() 254 + l := log.FromContext(ctx).With("handler", "IssueEdit") 255 + issue, ok := request.IssueFromContext(ctx) 256 + if !ok { 257 + l.Error("malformed request, failed to get issue") 258 + p.Error503(w) 259 + return 260 + } 261 + repoOwnerId, ok := request.OwnerFromContext(ctx) 262 + if !ok { 263 + l.Error("malformed request") 264 + p.Error503(w) 265 + return 266 + } 267 + 268 + // render 269 + err := func() error { 270 + return p.EditIssueFragment(w, pages.EditIssueParams{ 271 + LoggedInUser: session.UserFromContext(ctx), 272 + RepoInfo: rs.MakeRepoInfo(ctx, repoOwnerId, issue.Repo, "", ""), 273 + 274 + Issue: issue, 275 + }) 276 + }() 277 + if err != nil { 278 + l.Error("failed to render", "err", err) 279 + p.Error503(w) 280 + return 281 + } 282 + } 283 + } 284 + 285 + func IssueEditPost(is isvc.Service, p *pages.Pages) http.HandlerFunc { 286 + noticeId := "issues" 287 + return func(w http.ResponseWriter, r *http.Request) { 288 + ctx := r.Context() 289 + l := log.FromContext(ctx).With("handler", "IssueEdit") 290 + issue, ok := request.IssueFromContext(ctx) 291 + if !ok { 292 + l.Error("malformed request, failed to get issue") 293 + p.Error503(w) 294 + return 295 + } 296 + 297 + newIssue := *issue 298 + newIssue.Title = r.FormValue("title") 299 + newIssue.Body = r.FormValue("body") 300 + 301 + err := is.EditIssue(ctx, &newIssue) 302 + if err != nil { 303 + if errors.Is(err, isvc.ErrDatabaseFail) { 304 + p.Notice(w, noticeId, "Failed to edit issue.") 305 + } else if errors.Is(err, isvc.ErrPDSFail) { 306 + p.Notice(w, noticeId, "Failed to edit issue.") 307 + } else { 308 + p.Notice(w, noticeId, "Failed to edit issue.") 309 + } 310 + return 311 + } 312 + 313 + p.HxRefresh(w) 314 + } 315 + } 316 + 317 + func CloseIssue(is isvc.Service, p *pages.Pages) http.HandlerFunc { 318 + noticeId := "issue-action" 319 + return func(w http.ResponseWriter, r *http.Request) { 320 + ctx := r.Context() 321 + l := log.FromContext(ctx).With("handler", "CloseIssue") 322 + issue, ok := request.IssueFromContext(ctx) 323 + if !ok { 324 + l.Error("malformed request, failed to get issue") 325 + p.Error503(w) 326 + return 327 + } 328 + 329 + err := is.CloseIssue(ctx, issue) 330 + if err != nil { 331 + if errors.Is(err, isvc.ErrForbidden) { 332 + http.Error(w, "forbidden", http.StatusUnauthorized) 333 + } else { 334 + p.Notice(w, noticeId, "Failed to close issue. Try again later.") 335 + } 336 + return 337 + } 338 + 339 + ownerSlashRepo := reporesolver.GetBaseRepoPath(r, issue.Repo) 340 + p.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) 341 + } 342 + } 343 + 344 + func ReopenIssue(is isvc.Service, p *pages.Pages) http.HandlerFunc { 345 + noticeId := "issue-action" 346 + return func(w http.ResponseWriter, r *http.Request) { 347 + ctx := r.Context() 348 + l := log.FromContext(ctx).With("handler", "ReopenIssue") 349 + issue, ok := request.IssueFromContext(ctx) 350 + if !ok { 351 + l.Error("malformed request, failed to get issue") 352 + p.Error503(w) 353 + return 354 + } 355 + 356 + err := is.ReopenIssue(ctx, issue) 357 + if err != nil { 358 + if errors.Is(err, isvc.ErrForbidden) { 359 + http.Error(w, "forbidden", http.StatusUnauthorized) 360 + } else { 361 + p.Notice(w, noticeId, "Failed to reopen issue. Try again later.") 362 + } 363 + return 364 + } 365 + 366 + ownerSlashRepo := reporesolver.GetBaseRepoPath(r, issue.Repo) 367 + p.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) 368 + } 369 + } 370 + 371 + func IssueDelete(s isvc.Service, p *pages.Pages) http.HandlerFunc { 372 + noticeId := "issue-actions-error" 373 + return func(w http.ResponseWriter, r *http.Request) { 374 + ctx := r.Context() 375 + l := log.FromContext(ctx).With("handler", "IssueDelete") 376 + issue, ok := request.IssueFromContext(ctx) 377 + if !ok { 378 + l.Error("failed to get issue") 379 + // TODO: 503 error with more detailed messages 380 + p.Error503(w) 381 + return 382 + } 383 + err := s.DeleteIssue(ctx, issue) 384 + if err != nil { 385 + p.Notice(w, noticeId, "failed to delete issue") 386 + return 387 + } 388 + p.HxLocation(w, "/") 389 + } 390 + }
+67
appview/web/middleware/auth.go
··· 1 + package middleware 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + "net/url" 7 + 8 + "tangled.org/core/appview/oauth" 9 + "tangled.org/core/appview/session" 10 + "tangled.org/core/log" 11 + ) 12 + 13 + // WithSession resumes atp session from cookie, ensure it's not malformed and 14 + // pass the session through context 15 + func WithSession(o *oauth.OAuth) middlewareFunc { 16 + return func(next http.Handler) http.Handler { 17 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 + atSess, err := o.ResumeSession(r) 19 + if err != nil { 20 + next.ServeHTTP(w, r) 21 + return 22 + } 23 + 24 + registry := o.GetAccounts(r) 25 + sess := session.Session{ 26 + User: &oauth.MultiAccountUser{ 27 + Did: atSess.Data.AccountDID.String(), 28 + Accounts: registry.Accounts, 29 + }, 30 + AtpClient: atSess.APIClient(), 31 + } 32 + ctx := session.IntoContext(r.Context(), sess) 33 + next.ServeHTTP(w, r.WithContext(ctx)) 34 + }) 35 + } 36 + } 37 + 38 + // AuthMiddleware ensures the request is authorized and redirect to login page 39 + // when unauthorized 40 + func AuthMiddleware() middlewareFunc { 41 + return func(next http.Handler) http.Handler { 42 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 43 + ctx := r.Context() 44 + l := log.FromContext(ctx) 45 + 46 + returnURL := "/" 47 + if u, err := url.Parse(r.Header.Get("Referer")); err == nil { 48 + returnURL = u.RequestURI() 49 + } 50 + 51 + loginURL := fmt.Sprintf("/login?return_url=%s", url.QueryEscape(returnURL)) 52 + 53 + if _, ok := session.FromContext(ctx); !ok { 54 + l.Debug("no session, redirecting...") 55 + if r.Header.Get("HX-Request") == "true" { 56 + w.Header().Set("HX-Redirect", loginURL) 57 + w.WriteHeader(http.StatusOK) 58 + } else { 59 + http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect) 60 + } 61 + return 62 + } 63 + 64 + next.ServeHTTP(w, r) 65 + }) 66 + } 67 + }
+27
appview/web/middleware/ensuredidorhandle.go
··· 1 + package middleware 2 + 3 + import ( 4 + "net/http" 5 + 6 + "github.com/go-chi/chi/v5" 7 + "tangled.org/core/appview/pages" 8 + "tangled.org/core/appview/state/userutil" 9 + ) 10 + 11 + // EnsureDidOrHandle ensures the "user" url param is valid did/handle format. 12 + // If not, respond with 404 13 + func EnsureDidOrHandle(p *pages.Pages) middlewareFunc { 14 + return func(next http.Handler) http.Handler { 15 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 + user := chi.URLParam(r, "user") 17 + 18 + // if using a DID or handle, just continue as per usual 19 + if userutil.IsDid(user) || userutil.IsHandle(user) { 20 + next.ServeHTTP(w, r) 21 + return 22 + } 23 + 24 + p.Error404(w) 25 + }) 26 + } 27 + }
+18
appview/web/middleware/log.go
··· 1 + package middleware 2 + 3 + import ( 4 + "log/slog" 5 + "net/http" 6 + 7 + "tangled.org/core/log" 8 + ) 9 + 10 + func WithLogger(l *slog.Logger) middlewareFunc { 11 + return func(next http.Handler) http.Handler { 12 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 + // NOTE: can add some metadata here 14 + ctx := log.IntoContext(r.Context(), l) 15 + next.ServeHTTP(w, r.WithContext(ctx)) 16 + }) 17 + } 18 + }
+7
appview/web/middleware/middleware.go
··· 1 + package middleware 2 + 3 + import ( 4 + "net/http" 5 + ) 6 + 7 + type middlewareFunc func(http.Handler) http.Handler
+49
appview/web/middleware/normalize.go
··· 1 + package middleware 2 + 3 + import ( 4 + "net/http" 5 + "strings" 6 + 7 + "github.com/go-chi/chi/v5" 8 + "tangled.org/core/appview/state/userutil" 9 + ) 10 + 11 + func Normalize() middlewareFunc { 12 + return func(next http.Handler) http.Handler { 13 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 + pat := chi.URLParam(r, "*") 15 + pathParts := strings.SplitN(pat, "/", 2) 16 + if len(pathParts) == 0 { 17 + next.ServeHTTP(w, r) 18 + return 19 + } 20 + 21 + firstPart := pathParts[0] 22 + 23 + // if using a flattened DID (like you would in go modules), unflatten 24 + if userutil.IsFlattenedDid(firstPart) { 25 + unflattenedDid := userutil.UnflattenDid(firstPart) 26 + redirectPath := strings.Join(append([]string{unflattenedDid}, pathParts[1:]...), "/") 27 + 28 + redirectURL := *r.URL 29 + redirectURL.Path = "/" + redirectPath 30 + 31 + http.Redirect(w, r, redirectURL.String(), http.StatusFound) 32 + return 33 + } 34 + 35 + // if using a handle with @, rewrite to work without @ 36 + if normalized := strings.TrimPrefix(firstPart, "@"); userutil.IsHandle(normalized) { 37 + redirectPath := strings.Join(append([]string{normalized}, pathParts[1:]...), "/") 38 + 39 + redirectURL := *r.URL 40 + redirectURL.Path = "/" + redirectPath 41 + 42 + http.Redirect(w, r, redirectURL.String(), http.StatusFound) 43 + return 44 + } 45 + 46 + next.ServeHTTP(w, r) 47 + }) 48 + } 49 + }
+38
appview/web/middleware/paginate.go
··· 1 + package middleware 2 + 3 + import ( 4 + "log" 5 + "net/http" 6 + "strconv" 7 + 8 + "tangled.org/core/appview/pagination" 9 + ) 10 + 11 + func Paginate(next http.Handler) http.Handler { 12 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 + page := pagination.FirstPage() 14 + 15 + offsetVal := r.URL.Query().Get("offset") 16 + if offsetVal != "" { 17 + offset, err := strconv.Atoi(offsetVal) 18 + if err != nil { 19 + log.Println("invalid offset") 20 + } else { 21 + page.Offset = offset 22 + } 23 + } 24 + 25 + limitVal := r.URL.Query().Get("limit") 26 + if limitVal != "" { 27 + limit, err := strconv.Atoi(limitVal) 28 + if err != nil { 29 + log.Println("invalid limit") 30 + } else { 31 + page.Limit = limit 32 + } 33 + } 34 + 35 + ctx := pagination.IntoContext(r.Context(), page) 36 + next.ServeHTTP(w, r.WithContext(ctx)) 37 + }) 38 + }
+121
appview/web/middleware/resolve.go
··· 1 + package middleware 2 + 3 + import ( 4 + "context" 5 + "net/http" 6 + "strconv" 7 + "strings" 8 + 9 + "github.com/go-chi/chi/v5" 10 + "tangled.org/core/appview/db" 11 + "tangled.org/core/appview/pages" 12 + "tangled.org/core/appview/web/request" 13 + "tangled.org/core/idresolver" 14 + "tangled.org/core/log" 15 + "tangled.org/core/orm" 16 + ) 17 + 18 + func ResolveIdent( 19 + idResolver *idresolver.Resolver, 20 + pages *pages.Pages, 21 + ) middlewareFunc { 22 + return func(next http.Handler) http.Handler { 23 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 24 + ctx := r.Context() 25 + l := log.FromContext(ctx) 26 + didOrHandle := chi.URLParam(r, "user") 27 + didOrHandle = strings.TrimPrefix(didOrHandle, "@") 28 + 29 + id, err := idResolver.ResolveIdent(ctx, didOrHandle) 30 + if err != nil { 31 + // invalid did or handle 32 + l.Warn("failed to resolve did/handle", "handle", didOrHandle, "err", err) 33 + pages.Error404(w) 34 + return 35 + } 36 + 37 + ctx = request.WithOwner(ctx, id) 38 + // TODO: reomove this later 39 + ctx = context.WithValue(ctx, "resolvedId", *id) 40 + 41 + next.ServeHTTP(w, r.WithContext(ctx)) 42 + }) 43 + } 44 + } 45 + 46 + func ResolveRepo( 47 + e *db.DB, 48 + pages *pages.Pages, 49 + ) middlewareFunc { 50 + return func(next http.Handler) http.Handler { 51 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 52 + ctx := r.Context() 53 + l := log.FromContext(ctx) 54 + repoName := chi.URLParam(r, "repo") 55 + repoOwner, ok := request.OwnerFromContext(ctx) 56 + if !ok { 57 + l.Error("malformed middleware") 58 + w.WriteHeader(http.StatusInternalServerError) 59 + return 60 + } 61 + 62 + repo, err := db.GetRepo( 63 + e, 64 + orm.FilterEq("did", repoOwner.DID.String()), 65 + orm.FilterEq("name", repoName), 66 + ) 67 + if err != nil { 68 + l.Warn("failed to resolve repo", "err", err) 69 + pages.ErrorKnot404(w) 70 + return 71 + } 72 + 73 + // TODO: pass owner id into repository object 74 + 75 + ctx = request.WithRepo(ctx, repo) 76 + // TODO: reomove this later 77 + ctx = context.WithValue(ctx, "repo", repo) 78 + 79 + next.ServeHTTP(w, r.WithContext(ctx)) 80 + }) 81 + } 82 + } 83 + 84 + func ResolveIssue( 85 + e *db.DB, 86 + pages *pages.Pages, 87 + ) middlewareFunc { 88 + return func(next http.Handler) http.Handler { 89 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 90 + ctx := r.Context() 91 + l := log.FromContext(ctx) 92 + issueIdStr := chi.URLParam(r, "issue") 93 + issueId, err := strconv.Atoi(issueIdStr) 94 + if err != nil { 95 + l.Warn("failed to fully resolve issue ID", "err", err) 96 + pages.Error404(w) 97 + return 98 + } 99 + repo, ok := request.RepoFromContext(ctx) 100 + if !ok { 101 + l.Error("malformed middleware") 102 + w.WriteHeader(http.StatusInternalServerError) 103 + return 104 + } 105 + 106 + issue, err := db.GetIssue(e, repo.RepoAt(), issueId) 107 + if err != nil { 108 + l.Warn("failed to resolve issue", "err", err) 109 + pages.ErrorKnot404(w) 110 + return 111 + } 112 + issue.Repo = repo 113 + 114 + ctx = request.WithIssue(ctx, issue) 115 + // TODO: reomove this later 116 + ctx = context.WithValue(ctx, "issue", issue) 117 + 118 + next.ServeHTTP(w, r.WithContext(ctx)) 119 + }) 120 + } 121 + }
+53
appview/web/readme.md
··· 1 + # appview/web 2 + 3 + ## package structure 4 + 5 + ``` 6 + web/ 7 + |- routes.go 8 + |- handler/ 9 + | |- xrpc/ 10 + |- middleware/ 11 + |- request/ 12 + ``` 13 + 14 + - `web/routes.go` : all possible routes defined in single file 15 + - `web/handler` : general http handlers 16 + - `web/handler/xrpc` : xrpc handlers 17 + - `web/middleware` : all middlwares 18 + - `web/request` : define methods to insert/fetch values from request context. shared between middlewares and handlers. 19 + 20 + ### file name convention on `web/handler` 21 + 22 + - Follow the absolute uri path of the handlers (replace `/` to `_`.) 23 + - Trailing path segments can be omitted. 24 + - Avoid conflicts between prefix and names. 25 + - e.g. using both `user_repo_pulls.go` and `user_repo_pulls_rounds.go` (with `user_repo_pulls_` prefix) 26 + 27 + ### handler-generators instead of raw handler function 28 + 29 + instead of: 30 + ```go 31 + type Handler struct { 32 + is isvc.Service 33 + rs rsvc.Service 34 + } 35 + func (h *Handler) RepoIssues(w http.ResponseWriter, r *http.Request) { 36 + // ... 37 + } 38 + ``` 39 + 40 + prefer: 41 + ```go 42 + func RepoIssues(is isvc.Service, rs rsvc.Service, p *pages.Pages, d *db.DB) http.HandlerFunc { 43 + return func(w http.ResponseWriter, r *http.Request) { 44 + // ... 45 + } 46 + } 47 + ``` 48 + 49 + Pass dependencies to each handler-generators and avoid creating structs with shared dependencies unless it serves somedomain-specific roles like `service/issue.Service`. Same rule applies to middlewares too. 50 + 51 + This pattern is inspired by [the grafana blog post](https://grafana.com/blog/how-i-write-http-services-in-go-after-13-years/#maker-funcs-return-the-handler). 52 + 53 + Function name can be anything as long as it is clear.
+41
appview/web/request/context.go
··· 1 + package request 2 + 3 + import ( 4 + "context" 5 + 6 + "github.com/bluesky-social/indigo/atproto/identity" 7 + "tangled.org/core/appview/models" 8 + ) 9 + 10 + type ( 11 + ctxKeyOwner struct{} 12 + ctxKeyRepo struct{} 13 + ctxKeyIssue struct{} 14 + ) 15 + 16 + func WithOwner(ctx context.Context, owner *identity.Identity) context.Context { 17 + return context.WithValue(ctx, ctxKeyOwner{}, owner) 18 + } 19 + 20 + func OwnerFromContext(ctx context.Context) (*identity.Identity, bool) { 21 + owner, ok := ctx.Value(ctxKeyOwner{}).(*identity.Identity) 22 + return owner, ok 23 + } 24 + 25 + func WithRepo(ctx context.Context, repo *models.Repo) context.Context { 26 + return context.WithValue(ctx, ctxKeyRepo{}, repo) 27 + } 28 + 29 + func RepoFromContext(ctx context.Context) (*models.Repo, bool) { 30 + repo, ok := ctx.Value(ctxKeyRepo{}).(*models.Repo) 31 + return repo, ok 32 + } 33 + 34 + func WithIssue(ctx context.Context, issue *models.Issue) context.Context { 35 + return context.WithValue(ctx, ctxKeyIssue{}, issue) 36 + } 37 + 38 + func IssueFromContext(ctx context.Context) (*models.Issue, bool) { 39 + issue, ok := ctx.Value(ctxKeyIssue{}).(*models.Issue) 40 + return issue, ok 41 + }
+205
appview/web/routes.go
··· 1 + package web 2 + 3 + import ( 4 + "log/slog" 5 + "net/http" 6 + 7 + "github.com/go-chi/chi/v5" 8 + "tangled.org/core/appview/config" 9 + "tangled.org/core/appview/db" 10 + "tangled.org/core/appview/indexer" 11 + "tangled.org/core/appview/mentions" 12 + "tangled.org/core/appview/notify" 13 + "tangled.org/core/appview/oauth" 14 + "tangled.org/core/appview/pages" 15 + isvc "tangled.org/core/appview/service/issue" 16 + rsvc "tangled.org/core/appview/service/repo" 17 + "tangled.org/core/appview/state" 18 + "tangled.org/core/appview/validator" 19 + "tangled.org/core/appview/web/handler" 20 + "tangled.org/core/appview/web/middleware" 21 + "tangled.org/core/idresolver" 22 + "tangled.org/core/rbac" 23 + ) 24 + 25 + // RouterFromState creates a web router from `state.State`. This exist to 26 + // bridge between legacy web routers under `State` and new architecture 27 + func RouterFromState(s *state.State) http.Handler { 28 + config, db, enforcer, idResolver, refResolver, indexer, logger, notifier, oauth, pages, validator := s.Expose() 29 + 30 + return Router( 31 + logger, 32 + config, 33 + db, 34 + enforcer, 35 + idResolver, 36 + refResolver, 37 + indexer, 38 + notifier, 39 + oauth, 40 + pages, 41 + validator, 42 + s, 43 + ) 44 + } 45 + 46 + func Router( 47 + // NOTE: put base dependencies (db, idResolver, oauth etc) 48 + logger *slog.Logger, 49 + config *config.Config, 50 + db *db.DB, 51 + enforcer *rbac.Enforcer, 52 + idResolver *idresolver.Resolver, 53 + mentionsResolver *mentions.Resolver, 54 + indexer *indexer.Indexer, 55 + notifier notify.Notifier, 56 + oauth *oauth.OAuth, 57 + pages *pages.Pages, 58 + validator *validator.Validator, 59 + // to use legacy web handlers. will be removed later 60 + s *state.State, 61 + ) http.Handler { 62 + repo := rsvc.NewService( 63 + logger, 64 + config, 65 + db, 66 + enforcer, 67 + ) 68 + issue := isvc.NewService( 69 + logger, 70 + config, 71 + db, 72 + enforcer, 73 + notifier, 74 + idResolver, 75 + mentionsResolver, 76 + indexer.Issues, 77 + validator, 78 + ) 79 + 80 + i := s.ExposeIssue() 81 + 82 + r := chi.NewRouter() 83 + 84 + mw := s.Middleware() 85 + auth := middleware.AuthMiddleware() 86 + 87 + r.Use(middleware.WithLogger(logger)) 88 + r.Use(middleware.WithSession(oauth)) 89 + 90 + r.Use(middleware.Normalize()) 91 + 92 + r.Get("/pwa-manifest.json", s.WebAppManifest) 93 + r.Get("/robots.txt", s.RobotsTxt) 94 + 95 + r.Handle("/static/*", pages.Static()) 96 + 97 + r.Get("/", s.HomeOrTimeline) 98 + r.Get("/timeline", s.Timeline) 99 + r.Get("/upgradeBanner", s.UpgradeBanner) 100 + 101 + r.Get("/terms", s.TermsOfService) 102 + r.Get("/privacy", s.PrivacyPolicy) 103 + r.Get("/brand", s.Brand) 104 + // special-case handler for serving tangled.org/core 105 + r.Get("/core", s.Core()) 106 + 107 + r.Get("/login", s.Login) 108 + r.Post("/login", s.Login) 109 + r.Post("/logout", s.Logout) 110 + 111 + r.Get("/goodfirstissues", s.GoodFirstIssues) 112 + 113 + r.With(auth).Get("/repo/new", s.NewRepo) 114 + r.With(auth).Post("/repo/new", s.NewRepo) 115 + 116 + r.With(auth).Post("/follow", s.Follow) 117 + r.With(auth).Delete("/follow", s.Follow) 118 + 119 + r.With(auth).Post("/star", s.Star) 120 + r.With(auth).Delete("/star", s.Star) 121 + 122 + r.With(auth).Post("/react", s.React) 123 + r.With(auth).Delete("/react", s.React) 124 + 125 + r.With(auth).Get("/profile/edit-bio", s.EditBioFragment) 126 + r.With(auth).Get("/profile/edit-pins", s.EditPinsFragment) 127 + r.With(auth).Post("/profile/bio", s.UpdateProfileBio) 128 + r.With(auth).Post("/profile/pins", s.UpdateProfilePins) 129 + 130 + r.Mount("/settings", s.SettingsRouter()) 131 + r.Mount("/strings", s.StringsRouter(mw)) 132 + r.Mount("/settings/knots", s.KnotsRouter()) 133 + r.Mount("/settings/spindles", s.SpindlesRouter()) 134 + r.Mount("/notifications", s.NotificationsRouter(mw)) 135 + 136 + r.Mount("/signup", s.SignupRouter()) 137 + r.Get("/oauth/client-metadata.json", handler.OauthClientMetadata(oauth)) 138 + r.Get("/oauth/jwks.json", handler.OauthJwks(oauth)) 139 + r.Get("/oauth/callback", oauth.Callback) 140 + 141 + // special-case handler. should replace with xrpc later 142 + r.Get("/keys/{user}", s.Keys) 143 + 144 + r.HandleFunc("/@*", func(w http.ResponseWriter, r *http.Request) { 145 + http.Redirect(w, r, "/"+chi.URLParam(r, "*"), http.StatusFound) 146 + }) 147 + 148 + r.Route("/{user}", func(r chi.Router) { 149 + r.Use(middleware.EnsureDidOrHandle(pages)) 150 + r.Use(middleware.ResolveIdent(idResolver, pages)) 151 + 152 + r.Get("/", s.Profile) 153 + r.Get("/feed.atom", s.AtomFeedPage) 154 + 155 + r.Route("/{repo}", func(r chi.Router) { 156 + r.Use(middleware.ResolveRepo(db, pages)) 157 + 158 + r.Mount("/", s.RepoRouter(mw)) 159 + 160 + // /{user}/{repo}/issues/* 161 + r.With(middleware.Paginate).Get("/issues", handler.RepoIssues(issue, repo, pages, db)) 162 + r.With(auth).Get("/issues/new", handler.NewIssue(repo, pages)) 163 + r.With(auth).Post("/issues/new", handler.NewIssuePost(issue, pages)) 164 + r.Route("/issues/{issue}", func(r chi.Router) { 165 + r.Use(middleware.ResolveIssue(db, pages)) 166 + 167 + r.Get("/", handler.Issue(issue, repo, pages, db)) 168 + r.Get("/opengraph", i.IssueOpenGraphSummary) 169 + 170 + r.With(auth).Delete("/", handler.IssueDelete(issue, pages)) 171 + 172 + r.With(auth).Get("/edit", handler.IssueEdit(issue, repo, pages)) 173 + r.With(auth).Post("/edit", handler.IssueEditPost(issue, pages)) 174 + 175 + r.With(auth).Post("/close", handler.CloseIssue(issue, pages)) 176 + r.With(auth).Post("/reopen", handler.ReopenIssue(issue, pages)) 177 + 178 + r.With(auth).Post("/comment", i.NewIssueComment) 179 + r.With(auth).Route("/comment/{commentId}/", func(r chi.Router) { 180 + r.Get("/", i.IssueComment) 181 + r.Delete("/", i.DeleteIssueComment) 182 + r.Get("/edit", i.EditIssueComment) 183 + r.Post("/edit", i.EditIssueComment) 184 + r.Get("/reply", i.ReplyIssueComment) 185 + r.Get("/replyPlaceholder", i.ReplyIssueCommentPlaceholder) 186 + }) 187 + }) 188 + 189 + r.Mount("/pulls", s.PullsRouter(mw)) 190 + r.Mount("/pipelines", s.PipelinesRouter(mw)) 191 + r.Mount("/labels", s.LabelsRouter()) 192 + 193 + // These routes get proxied to the knot 194 + r.Get("/info/refs", s.InfoRefs) 195 + r.Post("/git-upload-pack", s.UploadPack) 196 + r.Post("/git-receive-pack", s.ReceivePack) 197 + }) 198 + }) 199 + 200 + r.NotFound(func(w http.ResponseWriter, r *http.Request) { 201 + pages.Error404(w) 202 + }) 203 + 204 + return r 205 + }
+2 -1
cmd/appview/main.go
··· 7 7 8 8 "tangled.org/core/appview/config" 9 9 "tangled.org/core/appview/state" 10 + "tangled.org/core/appview/web" 10 11 tlog "tangled.org/core/log" 11 12 ) 12 13 ··· 35 36 36 37 logger.Info("starting server", "address", c.Core.ListenAddr) 37 38 38 - if err := http.ListenAndServe(c.Core.ListenAddr, state.Router()); err != nil { 39 + if err := http.ListenAndServe(c.Core.ListenAddr, web.RouterFromState(state)); err != nil { 39 40 logger.Error("failed to start appview", "err", err) 40 41 } 41 42 }
+1
cmd/cborgen/cborgen.go
··· 15 15 "api/tangled/cbor_gen.go", 16 16 "tangled", 17 17 tangled.ActorProfile{}, 18 + tangled.Comment{}, 18 19 tangled.FeedReaction{}, 19 20 tangled.FeedStar{}, 20 21 tangled.GitRefUpdate{},
+11
contrib/certs/root.crt
··· 1 + -----BEGIN CERTIFICATE----- 2 + MIIBpDCCAUqgAwIBAgIRAIu1RX0P2Js35XIiiJJgAmgwCgYIKoZIzj0EAwIwMDEu 3 + MCwGA1UEAxMlQ2FkZHkgTG9jYWwgQXV0aG9yaXR5IC0gMjAyNiBFQ0MgUm9vdDAe 4 + Fw0yNjAzMDkxNTU3NTVaFw0zNjAxMTYxNTU3NTVaMDAxLjAsBgNVBAMTJUNhZGR5 5 + IExvY2FsIEF1dGhvcml0eSAtIDIwMjYgRUNDIFJvb3QwWTATBgcqhkjOPQIBBggq 6 + hkjOPQMBBwNCAASazquLyfq/CAPnJUlPhHUIgH4CMqXcKUZ/eLpkVg5HZrqmOhEo 7 + ma0p/EaNJJ1y390TxJ0Z401ZtwsKV3bBvny6o0UwQzAOBgNVHQ8BAf8EBAMCAQYw 8 + EgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUaQbcT77nBTGxgVWPHxoEKa1s 9 + U5MwCgYIKoZIzj0EAwIDSAAwRQIgZVJ5unzemUax0EVHs91KGBInjwrK1B1M46Ji 10 + wzG3Ws8CIQD6251zR7YO/omTeShaXRZ3ctCzsMbW2Ic/tz/aLy6h/A== 11 + -----END CERTIFICATE-----
+31
contrib/example.env
··· 1 + # NOTE: put actual DIDs here 2 + alice_did=did:plc:alice-did 3 + tangled_did=did:plc:tangled-did 4 + 5 + #core 6 + export TANGLED_DEV=true 7 + export TANGLED_APPVIEW_HOST=127.0.0.1:3000 8 + # plc 9 + export TANGLED_PLC_URL=https://plc.tngl.boltless.dev 10 + # jetstream 11 + export TANGLED_JETSTREAM_ENDPOINT=wss://jetstream.tngl.boltless.dev/subscribe 12 + # label 13 + export TANGLED_LABEL_GFI=at://${tangled_did}/sh.tangled.label.definition/good-first-issue 14 + export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_GFI 15 + export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/assignee 16 + export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/documentation 17 + export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/duplicate 18 + export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/wontfix 19 + 20 + # vm settings 21 + export TANGLED_VM_PLC_URL=https://plc.tngl.boltless.dev 22 + export TANGLED_VM_JETSTREAM_ENDPOINT=wss://jetstream.tngl.boltless.dev/subscribe 23 + export TANGLED_VM_KNOT_HOST=knot.tngl.boltless.dev 24 + export TANGLED_VM_KNOT_OWNER=$alice_did 25 + export TANGLED_VM_SPINDLE_HOST=spindle.tngl.boltless.dev 26 + export TANGLED_VM_SPINDLE_OWNER=$alice_did 27 + 28 + if [ -n "${TANGLED_RESEND_API_KEY:-}" ] && [ -n "${TANGLED_RESEND_SENT_FROM:-}" ]; then 29 + export TANGLED_VM_PDS_EMAIL_SMTP_URL=smtps://resend:$TANGLED_RESEND_API_KEY@smtp.resend.com:465/ 30 + export TANGLED_VM_PDS_EMAIL_FROM_ADDRESS=$TANGLED_RESEND_SENT_FROM 31 + fi
+12
contrib/pds.env
··· 1 + LOG_ENABLED=true 2 + 3 + PDS_JWT_SECRET=8cae8bffcc73d9932819650791e4e89a 4 + PDS_ADMIN_PASSWORD=d6a902588cd93bee1af83f924f60cfd3 5 + PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=2e92e336a50a618458e1097d94a1db86ec3fd8829d7735020cbae80625c761d7 6 + 7 + PDS_DATA_DIRECTORY=/pds 8 + PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks 9 + 10 + PDS_DID_PLC_URL=http://localhost:8080 11 + PDS_HOSTNAME=pds.tngl.boltless.dev 12 + PDS_PORT=3000
+25
contrib/readme.md
··· 1 + # how to setup local appview dev environment 2 + 3 + Appview requires several microservices from knot and spindle to entire atproto infra. This test environment is implemented under nixos vm. 4 + 5 + 1. copy `contrib/example.env` to `.env`, fill it and source it 6 + 2. run vm 7 + ```bash 8 + nix run --impure .#vm 9 + ``` 10 + 3. trust the generated cert from host machine 11 + ```bash 12 + # for macos 13 + sudo security add-trusted-cert -d -r trustRoot \ 14 + -k /Library/Keychains/System.keychain \ 15 + ./nix/vm-data/caddy/.local/share/caddy/pki/authorities/local/root.crt 16 + ``` 17 + 4. create test accounts with valid emails (use [`create-test-account.sh`](./scripts/create-test-account.sh)) 18 + 5. create default labels (use [`setup-const-records`](./scripts/setup-const-records.sh)) 19 + 6. restart vm with correct owner-did 20 + 21 + for git-https, you should change your local git config: 22 + ``` 23 + [http "https://knot.tngl.boltless.dev"] 24 + sslCAPath = /Users/boltless/repo/tangled/nix/vm-data/caddy/.local/share/caddy/pki/authorities/local/ 25 + ```
+68
contrib/scripts/create-test-account.sh
··· 1 + #!/bin/bash 2 + set -o errexit 3 + set -o nounset 4 + set -o pipefail 5 + 6 + source "$(dirname "$0")/../pds.env" 7 + 8 + # PDS_HOSTNAME= 9 + # PDS_ADMIN_PASSWORD= 10 + 11 + # curl a URL and fail if the request fails. 12 + function curl_cmd_get { 13 + curl --fail --silent --show-error "$@" 14 + } 15 + 16 + # curl a URL and fail if the request fails. 17 + function curl_cmd_post { 18 + curl --fail --silent --show-error --request POST --header "Content-Type: application/json" "$@" 19 + } 20 + 21 + # curl a URL but do not fail if the request fails. 22 + function curl_cmd_post_nofail { 23 + curl --silent --show-error --request POST --header "Content-Type: application/json" "$@" 24 + } 25 + 26 + USERNAME="${1:-}" 27 + 28 + if [[ "${USERNAME}" == "" ]]; then 29 + read -p "Enter a username: " USERNAME 30 + fi 31 + 32 + if [[ "${USERNAME}" == "" ]]; then 33 + echo "ERROR: missing USERNAME parameter." >/dev/stderr 34 + echo "Usage: $0 ${SUBCOMMAND} <USERNAME>" >/dev/stderr 35 + exit 1 36 + fi 37 + 38 + EMAIL=${USERNAME}@${PDS_HOSTNAME} 39 + 40 + PASSWORD="password" 41 + INVITE_CODE="$(curl_cmd_post \ 42 + --user "admin:${PDS_ADMIN_PASSWORD}" \ 43 + --data '{"useCount": 1}' \ 44 + "https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createInviteCode" | jq --raw-output '.code' 45 + )" 46 + RESULT="$(curl_cmd_post_nofail \ 47 + --data "{\"email\":\"${EMAIL}\", \"handle\":\"${USERNAME}.${PDS_HOSTNAME}\", \"password\":\"${PASSWORD}\", \"inviteCode\":\"${INVITE_CODE}\"}" \ 48 + "https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createAccount" 49 + )" 50 + 51 + DID="$(echo $RESULT | jq --raw-output '.did')" 52 + if [[ "${DID}" != did:* ]]; then 53 + ERR="$(echo ${RESULT} | jq --raw-output '.message')" 54 + echo "ERROR: ${ERR}" >/dev/stderr 55 + echo "Usage: $0 <EMAIL> <HANDLE>" >/dev/stderr 56 + exit 1 57 + fi 58 + 59 + echo 60 + echo "Account created successfully!" 61 + echo "-----------------------------" 62 + echo "Handle : ${USERNAME}.${PDS_HOSTNAME}" 63 + echo "DID : ${DID}" 64 + echo "Password : ${PASSWORD}" 65 + echo "-----------------------------" 66 + echo "This is a test account with an insecure password." 67 + echo "Make sure it's only used for development." 68 + echo
+106
contrib/scripts/setup-const-records.sh
··· 1 + #!/bin/bash 2 + set -o errexit 3 + set -o nounset 4 + set -o pipefail 5 + 6 + source "$(dirname "$0")/../pds.env" 7 + 8 + # PDS_HOSTNAME= 9 + 10 + # curl a URL and fail if the request fails. 11 + function curl_cmd_get { 12 + curl --fail --silent --show-error "$@" 13 + } 14 + 15 + # curl a URL and fail if the request fails. 16 + function curl_cmd_post { 17 + curl --fail --silent --show-error --request POST --header "Content-Type: application/json" "$@" 18 + } 19 + 20 + # curl a URL but do not fail if the request fails. 21 + function curl_cmd_post_nofail { 22 + curl --silent --show-error --request POST --header "Content-Type: application/json" "$@" 23 + } 24 + 25 + USERNAME="${1:-}" 26 + 27 + if [[ "${USERNAME}" == "" ]]; then 28 + read -p "Enter a username: " USERNAME 29 + fi 30 + 31 + if [[ "${USERNAME}" == "" ]]; then 32 + echo "ERROR: missing USERNAME parameter." >/dev/stderr 33 + echo "Usage: $0 ${SUBCOMMAND} <USERNAME>" >/dev/stderr 34 + exit 1 35 + fi 36 + 37 + SESS_RESULT="$(curl_cmd_post \ 38 + --data "$(cat <<EOF 39 + { 40 + "identifier": "$USERNAME", 41 + "password": "password" 42 + } 43 + EOF 44 + )" \ 45 + https://pds.tngl.boltless.dev/xrpc/com.atproto.server.createSession 46 + )" 47 + 48 + echo $SESS_RESULT | jq 49 + 50 + DID="$(echo $SESS_RESULT | jq --raw-output '.did')" 51 + ACCESS_JWT="$(echo $SESS_RESULT | jq --raw-output '.accessJwt')" 52 + 53 + function add_label_def { 54 + local color=$1 55 + local name=$2 56 + echo $color 57 + echo $name 58 + local json_payload=$(cat <<EOF 59 + { 60 + "repo": "$DID", 61 + "collection": "sh.tangled.label.definition", 62 + "rkey": "$name", 63 + "record": { 64 + "name": "$name", 65 + "color": "$color", 66 + "scope": ["sh.tangled.repo.issue"], 67 + "multiple": false, 68 + "createdAt": "2025-09-22T11:14:35+01:00", 69 + "valueType": {"type": "null", "format": "any"} 70 + } 71 + } 72 + EOF 73 + ) 74 + echo $json_payload 75 + echo $json_payload | jq 76 + RESULT="$(curl_cmd_post \ 77 + --data "$json_payload" \ 78 + -H "Authorization: Bearer ${ACCESS_JWT}" \ 79 + "https://${PDS_HOSTNAME}/xrpc/com.atproto.repo.createRecord")" 80 + echo $RESULT | jq 81 + } 82 + 83 + add_label_def '#64748b' 'wontfix' 84 + add_label_def '#8B5CF6' 'good-first-issue' 85 + add_label_def '#ef4444' 'duplicate' 86 + add_label_def '#06b6d4' 'documentation' 87 + json_payload=$(cat <<EOF 88 + { 89 + "repo": "$DID", 90 + "collection": "sh.tangled.label.definition", 91 + "rkey": "assignee", 92 + "record": { 93 + "name": "assignee", 94 + "color": "#10B981", 95 + "scope": ["sh.tangled.repo.issue", "sh.tangled.repo.pull"], 96 + "multiple": false, 97 + "createdAt": "2025-09-22T11:14:35+01:00", 98 + "valueType": {"type": "string", "format": "did"} 99 + } 100 + } 101 + EOF 102 + ) 103 + curl_cmd_post \ 104 + --data "$json_payload" \ 105 + -H "Authorization: Bearer ${ACCESS_JWT}" \ 106 + "https://${PDS_HOSTNAME}/xrpc/com.atproto.repo.createRecord"
+34 -2
flake.nix
··· 106 106 knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {}; 107 107 knot = self.callPackage ./nix/pkgs/knot.nix {}; 108 108 dolly = self.callPackage ./nix/pkgs/dolly.nix {}; 109 + did-method-plc = self.callPackage ./nix/pkgs/did-method-plc.nix {}; 110 + bluesky-jetstream = self.callPackage ./nix/pkgs/bluesky-jetstream.nix {}; 111 + bluesky-relay = self.callPackage ./nix/pkgs/bluesky-relay.nix {}; 112 + tap = self.callPackage ./nix/pkgs/tap.nix {}; 109 113 }); 110 114 in { 111 115 overlays.default = final: prev: { 112 - inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs dolly; 116 + inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs dolly did-method-plc bluesky-jetstream bluesky-relay tap; 113 117 }; 114 118 115 119 packages = forAllSystems (system: let ··· 130 134 sqlite-lib 131 135 docs 132 136 dolly 137 + did-method-plc 138 + bluesky-jetstream 139 + bluesky-relay 140 + tap 133 141 ; 134 142 135 143 pkgsStatic-appview = staticPackages.appview; ··· 282 290 rootDir=$(jj --ignore-working-copy root || git rev-parse --show-toplevel) || (echo "error: can't find repo root?"; exit 1) 283 291 cd "$rootDir" 284 292 285 - mkdir -p nix/vm-data/{knot,repos,spindle,spindle-logs} 293 + mkdir -p nix/vm-data/{caddy,knot,repos,spindle,spindle-logs} 286 294 287 295 export TANGLED_VM_DATA_DIR="$rootDir/nix/vm-data" 288 296 exec ${pkgs.lib.getExe ··· 357 365 imports = [./nix/modules/spindle.nix]; 358 366 359 367 services.tangled.spindle.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.spindle; 368 + }; 369 + nixosModules.did-method-plc = { 370 + lib, 371 + pkgs, 372 + ... 373 + }: { 374 + imports = [./nix/modules/did-method-plc.nix]; 375 + services.did-method-plc.package = lib.mkDefault self.packages.${pkgs.system}.did-method-plc; 376 + }; 377 + nixosModules.bluesky-relay = { 378 + lib, 379 + pkgs, 380 + ... 381 + }: { 382 + imports = [./nix/modules/bluesky-relay.nix]; 383 + services.bluesky-relay.package = lib.mkDefault self.packages.${pkgs.system}.bluesky-relay; 384 + }; 385 + nixosModules.bluesky-jetstream = { 386 + lib, 387 + pkgs, 388 + ... 389 + }: { 390 + imports = [./nix/modules/bluesky-jetstream.nix]; 391 + services.bluesky-jetstream.package = lib.mkDefault self.packages.${pkgs.system}.bluesky-jetstream; 360 392 }; 361 393 }; 362 394 }
+6 -3
go.mod
··· 12 12 github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 13 13 github.com/blevesearch/bleve/v2 v2.5.3 14 14 github.com/bluekeyes/go-gitdiff v0.8.1 15 - github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e 16 - github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 15 + github.com/bluesky-social/indigo v0.0.0-20260220055544-bf41e2ee75ab 16 + github.com/bluesky-social/jetstream v0.0.0-20260226214936-e0274250f654 17 17 github.com/bmatcuk/doublestar/v4 v4.9.1 18 18 github.com/carlmjohnson/versioninfo v0.22.5 19 19 github.com/casbin/casbin/v2 v2.103.0 ··· 45 45 github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c 46 46 github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef 47 47 github.com/stretchr/testify v1.10.0 48 - github.com/urfave/cli/v3 v3.3.3 48 + github.com/urfave/cli/v3 v3.4.1 49 49 github.com/whyrusleeping/cbor-gen v0.3.1 50 50 github.com/yuin/goldmark v1.7.13 51 51 github.com/yuin/goldmark-emoji v1.0.6 ··· 114 114 github.com/dlclark/regexp2 v1.11.5 // indirect 115 115 github.com/docker/go-connections v0.5.0 // indirect 116 116 github.com/docker/go-units v0.5.0 // indirect 117 + github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect 117 118 github.com/emirpasic/gods v1.18.1 // indirect 118 119 github.com/felixge/httpsnoop v1.0.4 // indirect 119 120 github.com/fsnotify/fsnotify v1.6.0 // indirect ··· 242 243 replace github.com/bluekeyes/go-gitdiff => tangled.sh/oppi.li/go-gitdiff v0.8.2 243 244 244 245 replace github.com/alecthomas/chroma/v2 => github.com/oppiliappan/chroma/v2 v2.24.2 246 + 247 + replace github.com/bluesky-social/indigo => github.com/boltlessengineer/indigo v0.0.0-20260302045703-861eeb80e873 245 248 246 249 // from bluesky-social/indigo 247 250 replace github.com/gocql/gocql => github.com/scylladb/gocql v1.14.4
+8 -6
go.sum
··· 91 91 github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= 92 92 github.com/blevesearch/zapx/v16 v16.2.4 h1:tGgfvleXTAkwsD5mEzgM3zCS/7pgocTCnO1oyAUjlww= 93 93 github.com/blevesearch/zapx/v16 v16.2.4/go.mod h1:Rti/REtuuMmzwsI8/C/qIzRaEoSK/wiFYw5e5ctUKKs= 94 - github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e h1:IutKPwmbU0LrYqw03EuwJtMdAe67rDTrL1U8S8dicRU= 95 - github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e/go.mod h1:n6QE1NDPFoi7PRbMUZmc2y7FibCqiVU4ePpsvhHUBR8= 96 - github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 h1:CFvRtYNSnWRAi/98M3O466t9dYuwtesNbu6FVPymRrA= 97 - github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1/go.mod h1:WiYEeyJSdUwqoaZ71KJSpTblemUCpwJfh5oVXplK6T4= 94 + github.com/bluesky-social/jetstream v0.0.0-20260226214936-e0274250f654 h1:OK76FcHhZp8ohjRB0OMWgti0oYAWFlt3KDQcIkH1pfI= 95 + github.com/bluesky-social/jetstream v0.0.0-20260226214936-e0274250f654/go.mod h1:vt8kVRKtvrBspt9G38wDD8+BotjIMO8u8IYoVnyE4zY= 98 96 github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 99 97 github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 100 98 github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= 101 99 github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 100 + github.com/boltlessengineer/indigo v0.0.0-20260302045703-861eeb80e873 h1:dMpEw+TYJMQYAQI+jWw/34+kNqPaOxMR9wxnzuSrJIQ= 101 + github.com/boltlessengineer/indigo v0.0.0-20260302045703-861eeb80e873/go.mod h1:VG/LeqLGNI3Ew7lsYixajnZGFfWPv144qbUddh+Oyag= 102 102 github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 103 103 github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 104 104 github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= ··· 175 175 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 176 176 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 177 177 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 178 + github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg= 179 + github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw= 178 180 github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= 179 181 github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= 180 182 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= ··· 532 534 github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 533 535 github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 534 536 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 535 - github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= 536 - github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 537 + github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM= 538 + github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 537 539 github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI= 538 540 github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q= 539 541 github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
+3 -3
idresolver/resolver.go
··· 60 60 base := BaseDirectory(plcUrl) 61 61 cached := identity.NewCacheDirectory(base, 250_000, time.Hour*24, time.Minute*2, time.Minute*5) 62 62 return &Resolver{ 63 - directory: &cached, 63 + directory: cached, 64 64 } 65 65 } 66 66 ··· 80 80 return nil, err 81 81 } 82 82 83 - return r.directory.Lookup(ctx, *id) 83 + return r.directory.Lookup(ctx, id) 84 84 } 85 85 86 86 func (r *Resolver) ResolveIdents(ctx context.Context, idents []string) []*identity.Identity { ··· 117 117 return err 118 118 } 119 119 120 - return r.directory.Purge(ctx, *id) 120 + return r.directory.Purge(ctx, id) 121 121 } 122 122 123 123 func (r *Resolver) Directory() identity.Directory {
+3
input.css
··· 99 99 border border-gray-300 dark:border-gray-600 100 100 focus:outline-none focus:ring-1 focus:ring-gray-400 dark:focus:ring-gray-500; 101 101 } 102 + textarea { 103 + @apply font-mono; 104 + } 102 105 details summary::-webkit-details-marker { 103 106 display: none; 104 107 }
+2 -1
jetstream/jetstream.go
··· 159 159 j.cancelMu.Unlock() 160 160 161 161 if err := j.client.ConnectAndRead(connCtx, cursor); err != nil { 162 - l.Error("error reading jetstream", "error", err) 162 + l.Error("error reading jetstream, retry in 3s", "error", err) 163 163 cancel() 164 + time.Sleep(3 * time.Second) 164 165 continue 165 166 } 166 167
+51
lexicons/comment/comment.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.comment", 4 + "needsCbor": true, 5 + "needsType": true, 6 + "defs": { 7 + "main": { 8 + "type": "record", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": [ 13 + "subject", 14 + "body", 15 + "createdAt" 16 + ], 17 + "properties": { 18 + "subject": { 19 + "type": "string", 20 + "format": "at-uri" 21 + }, 22 + "body": { 23 + "type": "string" 24 + }, 25 + "createdAt": { 26 + "type": "string", 27 + "format": "datetime" 28 + }, 29 + "replyTo": { 30 + "type": "string", 31 + "format": "at-uri" 32 + }, 33 + "mentions": { 34 + "type": "array", 35 + "items": { 36 + "type": "string", 37 + "format": "did" 38 + } 39 + }, 40 + "references": { 41 + "type": "array", 42 + "items": { 43 + "type": "string", 44 + "format": "at-uri" 45 + } 46 + } 47 + } 48 + } 49 + } 50 + } 51 + }
+10 -6
nix/gomod2nix.toml
··· 103 103 hash = "sha256-GWm5i1ukuBukV0GMF1rffpbOSSXZdfg6/0pABMiGzLQ=" 104 104 replaced = "tangled.sh/oppi.li/go-gitdiff" 105 105 [mod."github.com/bluesky-social/indigo"] 106 - version = "v0.0.0-20251003000214-3259b215110e" 107 - hash = "sha256-qi/GrquJznbLnnHVpd7IqoryCESbi6xE4X1SiEM2qlo=" 106 + version = "v0.0.0-20260302045703-861eeb80e873" 107 + hash = "sha256-WSsFGuNzYFqkU376uInDwQnTqR1lBWFHmzKig6bKZj8=" 108 + replaced = "github.com/boltlessengineer/indigo" 108 109 [mod."github.com/bluesky-social/jetstream"] 109 - version = "v0.0.0-20241210005130-ea96859b93d1" 110 - hash = "sha256-AiapbrkjXboIKc5QNiWH0KyNs0zKnn6UlGwWFlkUfm0=" 110 + version = "v0.0.0-20260226214936-e0274250f654" 111 + hash = "sha256-VE93NvI3PreteLHnlv7WT6GgH2vSjtoFjMygCmrznfg=" 111 112 [mod."github.com/bmatcuk/doublestar/v4"] 112 113 version = "v4.9.1" 113 114 hash = "sha256-0iyHjyTAsfhgYSsE+NKxSNGBuM3Id615VWeQhssTShE=" ··· 189 190 [mod."github.com/dustin/go-humanize"] 190 191 version = "v1.0.1" 191 192 hash = "sha256-yuvxYYngpfVkUg9yAmG99IUVmADTQA0tMbBXe0Fq0Mc=" 193 + [mod."github.com/earthboundkid/versioninfo/v2"] 194 + version = "v2.24.1" 195 + hash = "sha256-nbRdiX9WN2y1aiw1CR/DQ6AYqztow8FazndwY3kByHM=" 192 196 [mod."github.com/emirpasic/gods"] 193 197 version = "v1.18.1" 194 198 hash = "sha256-hGDKddjLj+5dn2woHtXKUdd49/3xdsqnhx7VEdCu1m4=" ··· 510 514 version = "v1.10.0" 511 515 hash = "sha256-fJ4gnPr0vnrOhjQYQwJ3ARDKPsOtA7d4olQmQWR+wpI=" 512 516 [mod."github.com/urfave/cli/v3"] 513 - version = "v3.3.3" 514 - hash = "sha256-FdPiu7koY1qBinkfca4A05zCrX+Vu4eRz8wlRDZJyGg=" 517 + version = "v3.4.1" 518 + hash = "sha256-cDMaQrIVMthUhdyI1mKXzDC5/wIK151073lzRl92RnA=" 515 519 [mod."github.com/vmihailenco/go-tinylfu"] 516 520 version = "v0.2.2" 517 521 hash = "sha256-ZHr4g7DJAV6rLcfrEWZwo9wJSeZcXB9KSP38UIOFfaM="
+64
nix/modules/bluesky-jetstream.nix
··· 1 + { 2 + config, 3 + pkgs, 4 + lib, 5 + ... 6 + }: let 7 + cfg = config.services.bluesky-jetstream; 8 + in 9 + with lib; { 10 + options.services.bluesky-jetstream = { 11 + enable = mkEnableOption "jetstream server"; 12 + package = mkPackageOption pkgs "bluesky-jetstream" {}; 13 + 14 + # dataDir = mkOption { 15 + # type = types.str; 16 + # default = "/var/lib/jetstream"; 17 + # description = "directory to store data (pebbleDB)"; 18 + # }; 19 + livenessTtl = mkOption { 20 + type = types.int; 21 + default = 15; 22 + description = "time to restart when no event detected (seconds)"; 23 + }; 24 + websocketUrl = mkOption { 25 + type = types.str; 26 + default = "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos"; 27 + description = "full websocket path to the ATProto SubscribeRepos XRPC endpoint"; 28 + }; 29 + }; 30 + config = mkIf cfg.enable { 31 + systemd.services.bluesky-jetstream = { 32 + description = "bluesky jetstream"; 33 + after = ["network.target" "pds.service"]; 34 + wantedBy = ["multi-user.target"]; 35 + 36 + serviceConfig = { 37 + User = "jetstream"; 38 + Group = "jetstream"; 39 + StateDirectory = "jetstream"; 40 + StateDirectoryMode = "0755"; 41 + # preStart = '' 42 + # mkdir -p "${cfg.dataDir}" 43 + # chown -R jetstream:jetstream "${cfg.dataDir}" 44 + # ''; 45 + # WorkingDirectory = cfg.dataDir; 46 + Environment = [ 47 + "JETSTREAM_DATA_DIR=/var/lib/jetstream/data" 48 + "JETSTREAM_LIVENESS_TTL=${toString cfg.livenessTtl}s" 49 + "JETSTREAM_WS_URL=${cfg.websocketUrl}" 50 + ]; 51 + ExecStart = getExe cfg.package; 52 + Restart = "always"; 53 + RestartSec = 5; 54 + }; 55 + }; 56 + users = { 57 + users.jetstream = { 58 + group = "jetstream"; 59 + isSystemUser = true; 60 + }; 61 + groups.jetstream = {}; 62 + }; 63 + }; 64 + }
+48
nix/modules/bluesky-relay.nix
··· 1 + { 2 + config, 3 + pkgs, 4 + lib, 5 + ... 6 + }: let 7 + cfg = config.services.bluesky-relay; 8 + in 9 + with lib; { 10 + options.services.bluesky-relay = { 11 + enable = mkEnableOption "relay server"; 12 + package = mkPackageOption pkgs "bluesky-relay" {}; 13 + }; 14 + config = mkIf cfg.enable { 15 + systemd.services.bluesky-relay = { 16 + description = "bluesky relay"; 17 + after = ["network.target" "pds.service"]; 18 + wantedBy = ["multi-user.target"]; 19 + 20 + serviceConfig = { 21 + User = "relay"; 22 + Group = "relay"; 23 + StateDirectory = "relay"; 24 + StateDirectoryMode = "0755"; 25 + Environment = [ 26 + "RELAY_ADMIN_PASSWORD=password" 27 + "RELAY_PLC_HOST=https://plc.tngl.boltless.dev" 28 + "DATABASE_URL=sqlite:///var/lib/relay/relay.sqlite" 29 + "RELAY_IP_BIND=:2470" 30 + "RELAY_PERSIST_DIR=/var/lib/relay" 31 + "RELAY_DISABLE_REQUEST_CRAWL=0" 32 + "RELAY_INITIAL_SEQ_NUMBER=1" 33 + "RELAY_ALLOW_INSECURE_HOSTS=1" 34 + ]; 35 + ExecStart = "${getExe cfg.package} serve"; 36 + Restart = "always"; 37 + RestartSec = 5; 38 + }; 39 + }; 40 + users = { 41 + users.relay = { 42 + group = "relay"; 43 + isSystemUser = true; 44 + }; 45 + groups.relay = {}; 46 + }; 47 + }; 48 + }
+76
nix/modules/did-method-plc.nix
··· 1 + { 2 + config, 3 + pkgs, 4 + lib, 5 + ... 6 + }: let 7 + cfg = config.services.did-method-plc; 8 + in 9 + with lib; { 10 + options.services.did-method-plc = { 11 + enable = mkEnableOption "did-method-plc server"; 12 + package = mkPackageOption pkgs "did-method-plc" {}; 13 + }; 14 + config = mkIf cfg.enable { 15 + services.postgresql = { 16 + enable = true; 17 + package = pkgs.postgresql_14; 18 + ensureDatabases = ["plc"]; 19 + ensureUsers = [ 20 + { 21 + name = "pg"; 22 + # ensurePermissions."DATABASE plc" = "ALL PRIVILEGES"; 23 + } 24 + ]; 25 + authentication = '' 26 + local all all trust 27 + host all all 127.0.0.1/32 trust 28 + ''; 29 + }; 30 + systemd.services.did-method-plc = { 31 + description = "did-method-plc"; 32 + 33 + after = ["postgresql.service"]; 34 + wants = ["postgresql.service"]; 35 + wantedBy = ["multi-user.target"]; 36 + 37 + environment = let 38 + db_creds_json = builtins.toJSON { 39 + username = "pg"; 40 + password = ""; 41 + host = "127.0.0.1"; 42 + port = 5432; 43 + }; 44 + in { 45 + # TODO: inherit from config 46 + DEBUG_MODE = "1"; 47 + LOG_ENABLED = "true"; 48 + LOG_LEVEL = "debug"; 49 + LOG_DESTINATION = "1"; 50 + ENABLE_MIGRATIONS = "true"; 51 + DB_CREDS_JSON = db_creds_json; 52 + DB_MIGRATE_CREDS_JSON = db_creds_json; 53 + PLC_VERSION = "0.0.1"; 54 + PORT = "8080"; 55 + }; 56 + 57 + serviceConfig = { 58 + ExecStart = getExe cfg.package; 59 + User = "plc"; 60 + Group = "plc"; 61 + StateDirectory = "plc"; 62 + StateDirectoryMode = "0755"; 63 + Restart = "always"; 64 + 65 + # Hardening 66 + }; 67 + }; 68 + users = { 69 + users.plc = { 70 + group = "plc"; 71 + isSystemUser = true; 72 + }; 73 + groups.plc = {}; 74 + }; 75 + }; 76 + }
+20
nix/pkgs/bluesky-jetstream.nix
··· 1 + { 2 + buildGoModule, 3 + fetchFromGitHub, 4 + }: 5 + buildGoModule { 6 + pname = "bluesky-jetstream"; 7 + version = "0.1.0"; 8 + src = fetchFromGitHub { 9 + owner = "bluesky-social"; 10 + repo = "jetstream"; 11 + rev = "7d7efa58d7f14101a80ccc4f1085953948b7d5de"; 12 + sha256 = "sha256-1e9SL/8gaDPMA4YZed51ffzgpkptbMd0VTbTTDbPTFw="; 13 + }; 14 + subPackages = ["cmd/jetstream"]; 15 + vendorHash = "sha256-/21XJQH6fo9uPzlABUAbdBwt1O90odmppH6gXu2wkiQ="; 16 + doCheck = false; 17 + meta = { 18 + mainProgram = "jetstream"; 19 + }; 20 + }
+20
nix/pkgs/bluesky-relay.nix
··· 1 + { 2 + buildGoModule, 3 + fetchFromGitHub, 4 + }: 5 + buildGoModule { 6 + pname = "bluesky-relay"; 7 + version = "0.1.0"; 8 + src = fetchFromGitHub { 9 + owner = "boltlessengineer"; 10 + repo = "indigo"; 11 + rev = "7fe70a304d795b998f354d2b7b2050b909709c99"; 12 + sha256 = "sha256-+h34x67cqH5t30+8rua53/ucvbn3BanrmH0Og3moHok="; 13 + }; 14 + subPackages = ["cmd/relay"]; 15 + vendorHash = "sha256-UOedwNYnM8Jx6B7Y9tFcZX8IeUBESAFAPTRYk7n0yo8="; 16 + doCheck = false; 17 + meta = { 18 + mainProgram = "relay"; 19 + }; 20 + }
+65
nix/pkgs/did-method-plc.nix
··· 1 + # inspired by https://github.com/NixOS/nixpkgs/blob/333bfb7c258fab089a834555ea1c435674c459b4/pkgs/by-name/ga/gatsby-cli/package.nix 2 + { 3 + lib, 4 + stdenv, 5 + fetchFromGitHub, 6 + fetchYarnDeps, 7 + yarnConfigHook, 8 + yarnBuildHook, 9 + nodejs, 10 + makeBinaryWrapper, 11 + }: 12 + stdenv.mkDerivation (finalAttrs: { 13 + pname = "did-method-plc"; 14 + version = "0.0.1"; 15 + 16 + src = fetchFromGitHub { 17 + owner = "did-method-plc"; 18 + repo = "did-method-plc"; 19 + rev = "158ba5535ac3da4fd4309954bde41deab0b45972"; 20 + sha256 = "sha256-O5smubbrnTDMCvL6iRyMXkddr5G7YHxkQRVMRULHanQ="; 21 + }; 22 + postPatch = '' 23 + # remove dd-trace dependency 24 + sed -i '3d' packages/server/service/index.js 25 + ''; 26 + 27 + yarnOfflineCache = fetchYarnDeps { 28 + yarnLock = finalAttrs.src + "/yarn.lock"; 29 + hash = "sha256-g8GzaAbWSnWwbQjJMV2DL5/ZlWCCX0sRkjjvX3tqU4Y="; 30 + }; 31 + 32 + nativeBuildInputs = [ 33 + yarnConfigHook 34 + yarnBuildHook 35 + nodejs 36 + makeBinaryWrapper 37 + ]; 38 + yarnBuildScript = "lerna"; 39 + yarnBuildFlags = [ 40 + "run" 41 + "build" 42 + "--scope" 43 + "@did-plc/server" 44 + "--include-dependencies" 45 + ]; 46 + 47 + installPhase = '' 48 + runHook preInstall 49 + 50 + mkdir -p $out/lib/node_modules/ 51 + mv packages/ $out/lib/packages/ 52 + mv node_modules/* $out/lib/node_modules/ 53 + 54 + makeWrapper ${lib.getExe nodejs} $out/bin/plc \ 55 + --add-flags $out/lib/packages/server/service/index.js \ 56 + --add-flags --enable-source-maps \ 57 + --set NODE_PATH $out/lib/node_modules 58 + 59 + runHook postInstall 60 + ''; 61 + 62 + meta = { 63 + mainProgram = "plc"; 64 + }; 65 + })
+20
nix/pkgs/tap.nix
··· 1 + { 2 + buildGoModule, 3 + fetchFromGitHub, 4 + }: 5 + buildGoModule { 6 + pname = "tap"; 7 + version = "0.1.0"; 8 + src = fetchFromGitHub { 9 + owner = "bluesky-social"; 10 + repo = "indigo"; 11 + rev = "498ecb9693e8ae050f73234c86f340f51ad896a9"; 12 + sha256 = "sha256-KASCdwkg/hlKBt7RTW3e3R5J3hqJkphoarFbaMgtN1k="; 13 + }; 14 + subPackages = ["cmd/tap"]; 15 + vendorHash = "sha256-UOedwNYnM8Jx6B7Y9tFcZX8IeUBESAFAPTRYk7n0yo8="; 16 + doCheck = false; 17 + meta = { 18 + mainProgram = "tap"; 19 + }; 20 + }
+122
nix/vm.nix
··· 23 23 nixpkgs.lib.nixosSystem { 24 24 inherit system; 25 25 modules = [ 26 + self.nixosModules.did-method-plc 27 + self.nixosModules.bluesky-jetstream 28 + self.nixosModules.bluesky-relay 26 29 self.nixosModules.knot 27 30 self.nixosModules.spindle 28 31 ({ ··· 39 42 diskSize = 10 * 1024; 40 43 cores = 2; 41 44 forwardPorts = [ 45 + # caddy 46 + { 47 + from = "host"; 48 + host.port = 80; 49 + guest.port = 80; 50 + } 51 + { 52 + from = "host"; 53 + host.port = 443; 54 + guest.port = 443; 55 + } 56 + { 57 + from = "host"; 58 + proto = "udp"; 59 + host.port = 443; 60 + guest.port = 443; 61 + } 42 62 # ssh 43 63 { 44 64 from = "host"; ··· 63 83 # as SQLite is incompatible with them. So instead we 64 84 # mount the shared directories to a different location 65 85 # and copy the contents around on service start/stop. 86 + caddyData = { 87 + source = "$TANGLED_VM_DATA_DIR/caddy"; 88 + target = config.services.caddy.dataDir; 89 + }; 66 90 knotData = { 67 91 source = "$TANGLED_VM_DATA_DIR/knot"; 68 92 target = "/mnt/knot-data"; ··· 79 103 }; 80 104 # This is fine because any and all ports that are forwarded to host are explicitly marked above, we don't need a separate guest firewall 81 105 networking.firewall.enable = false; 106 + # resolve `*.tngl.boltless.dev` to host 107 + services.dnsmasq.enable = true; 108 + services.dnsmasq.settings.address = "/tngl.boltless.dev/10.0.2.2"; 109 + security.pki.certificates = [ 110 + (builtins.readFile ../contrib/certs/root.crt) 111 + ]; 82 112 time.timeZone = "Europe/London"; 113 + services.timesyncd.enable = lib.mkVMOverride true; 83 114 services.getty.autologinUser = "root"; 84 115 environment.systemPackages = with pkgs; [curl vim git sqlite litecli]; 116 + virtualisation.docker.extraOptions = '' 117 + --dns 172.17.0.1 118 + ''; 85 119 services.tangled.knot = { 86 120 enable = true; 87 121 motd = "Welcome to the development knot!\n"; ··· 108 142 provider = "sqlite"; 109 143 }; 110 144 }; 145 + }; 146 + services.did-method-plc.enable = true; 147 + services.bluesky-pds = { 148 + enable = true; 149 + # overriding package version to support emails 150 + package = pkgs.bluesky-pds.overrideAttrs (old: rec { 151 + version = "0.4.188"; 152 + src = pkgs.fetchFromGitHub { 153 + owner = "bluesky-social"; 154 + repo = "pds"; 155 + tag = "v${version}"; 156 + hash = "sha256-t8KdyEygXdbj/5Rhj8W40e1o8mXprELpjsKddHExmo0="; 157 + }; 158 + pnpmDeps = pkgs.fetchPnpmDeps { 159 + inherit version src; 160 + pname = old.pname; 161 + sourceRoot = old.sourceRoot; 162 + fetcherVersion = 2; 163 + hash = "sha256-lQie7f8JbWKSpoavnMjHegBzH3GB9teXsn+S2SLJHHU="; 164 + }; 165 + }); 166 + settings = { 167 + LOG_ENABLED = "true"; 168 + 169 + PDS_JWT_SECRET = "8cae8bffcc73d9932819650791e4e89a"; 170 + PDS_ADMIN_PASSWORD = "d6a902588cd93bee1af83f924f60cfd3"; 171 + PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX = "2e92e336a50a618458e1097d94a1db86ec3fd8829d7735020cbae80625c761d7"; 172 + 173 + PDS_EMAIL_SMTP_URL = envVarOr "TANGLED_VM_PDS_EMAIL_SMTP_URL" null; 174 + PDS_EMAIL_FROM_ADDRESS = envVarOr "TANGLED_VM_PDS_EMAIL_FROM_ADDRESS" null; 175 + 176 + PDS_DID_PLC_URL = "http://localhost:8080"; 177 + PDS_CRAWLERS = "https://relay.tngl.boltless.dev"; 178 + PDS_HOSTNAME = "pds.tngl.boltless.dev"; 179 + PDS_PORT = 3000; 180 + }; 181 + }; 182 + services.bluesky-relay = { 183 + enable = true; 184 + }; 185 + services.bluesky-jetstream = { 186 + enable = true; 187 + livenessTtl = 300; 188 + websocketUrl = "ws://localhost:3000/xrpc/com.atproto.sync.subscribeRepos"; 189 + }; 190 + services.caddy = { 191 + enable = true; 192 + configFile = pkgs.writeText "Caddyfile" '' 193 + { 194 + debug 195 + cert_lifetime 3601d 196 + pki { 197 + ca local { 198 + intermediate_lifetime 3599d 199 + } 200 + } 201 + } 202 + 203 + plc.tngl.boltless.dev { 204 + tls internal 205 + reverse_proxy http://localhost:8080 206 + } 207 + 208 + *.pds.tngl.boltless.dev, pds.tngl.boltless.dev { 209 + tls internal 210 + reverse_proxy http://localhost:3000 211 + } 212 + 213 + jetstream.tngl.boltless.dev { 214 + tls internal 215 + reverse_proxy http://localhost:6008 216 + } 217 + 218 + relay.tngl.boltless.dev { 219 + tls internal 220 + reverse_proxy http://localhost:2470 221 + } 222 + 223 + knot.tngl.boltless.dev { 224 + tls internal 225 + reverse_proxy http://localhost:6444 226 + } 227 + 228 + spindle.tngl.boltless.dev { 229 + tls internal 230 + reverse_proxy http://localhost:6555 231 + } 232 + ''; 111 233 }; 112 234 users = { 113 235 # So we don't have to deal with permission clashing between

History

1 round 1 comment
sign up or login to add to the discussion
boltless.me submitted #0
23 commits
expand
go.mod: bump indigo version
nix: add more nix packages/modules related to atproto
lexicons: add general sh.tangled.comment lexicon
appview: replace PullComment to Comment
appview: replace IssueComment to Comment
contrib,nix: local, sandboxed atmosphere infra
wip: fix jetstream client
private: appview/pages: apply monospace font for all textarea
appview/notify: merge new comment events into one
appview: use explicit TANGLED_PDS_HANDLE_SUFFIX for tngl.sh user check
appview/{oauth,pages}: remove Pds field from oauth.User
appview/{oauth,pages}: cleanup unused codes
appview/{pages,state}: simplify LoginPage params
appview/{oauth,pages,state}: remove unused AddAccount session data
appview/oauth: remove AuthReturnInfo
appview/oauth: remove .Handle from oauth.AccountInfo
appview: wip
appview/pages: don't access MultiAccountUser.Active from template
appview/pulls: remove dependencies to oauth.MultiAccountUser
appview: remove oauth.User type
wip: appview/{service,web}: service layer
air: exclude site dir
appview/db: more verbose error message
3/3 failed
expand
expand 1 comment

oops, wrong one

closed without merging