Monorepo for Tangled

MM: service layer

boltless.me da016d82 b6fe3ea8

verified
+1982 -715
+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 + }
+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
··· 1181 1181 return err 1182 1182 }) 1183 1183 1184 + orm.RunMigration(conn, logger, "add-comments-table", func(tx *sql.Tx) error { 1185 + _, err := tx.Exec(` 1186 + drop table if exists comments; 1187 + 1188 + create table comments ( 1189 + -- identifiers 1190 + id integer primary key autoincrement, 1191 + did text not null, 1192 + collection text not null default 'sh.tangled.comment', 1193 + rkey text not null, 1194 + at_uri text generated always as ('at://' || did || '/' || collection || '/' || rkey) stored, 1195 + 1196 + -- at identifiers 1197 + subject_at text not null, 1198 + reply_to text, -- at_uri of parent comment 1199 + 1200 + pull_submission_id integer, -- dirty fix until we atprotate the pull-rounds 1201 + 1202 + -- content 1203 + body text not null, 1204 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1205 + edited text, 1206 + deleted text, 1207 + 1208 + -- constraints 1209 + unique(did, collection, rkey) 1210 + ); 1211 + 1212 + insert into comments ( 1213 + did, 1214 + collection, 1215 + rkey, 1216 + subject_at, 1217 + reply_to, 1218 + body, 1219 + created, 1220 + edited, 1221 + deleted 1222 + ) 1223 + select 1224 + did, 1225 + 'sh.tangled.repo.issue.comment', 1226 + rkey, 1227 + issue_at, 1228 + reply_to, 1229 + body, 1230 + created, 1231 + edited, 1232 + deleted 1233 + from issue_comments 1234 + where rkey is not null; 1235 + 1236 + insert into comments ( 1237 + did, 1238 + collection, 1239 + rkey, 1240 + subject_at, 1241 + pull_submission_id, 1242 + body, 1243 + created 1244 + ) 1245 + select 1246 + c.owner_did, 1247 + 'sh.tangled.repo.pull.comment', 1248 + substr( 1249 + substr(c.comment_at, 6 + instr(substr(c.comment_at, 6), '/')), -- nsid/rkey 1250 + instr( 1251 + substr(c.comment_at, 6 + instr(substr(c.comment_at, 6), '/')), -- nsid/rkey 1252 + '/' 1253 + ) + 1 1254 + ), -- rkey 1255 + p.at_uri, 1256 + c.submission_id, 1257 + c.body, 1258 + c.created 1259 + from pull_comments c 1260 + join pulls p on c.repo_at = p.repo_at and c.pull_id = p.pull_id; 1261 + `) 1262 + return err 1263 + }) 1264 + 1184 1265 return &DB{ 1185 1266 db, 1186 1267 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 ),
+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
+39 -37
appview/issues/issues.go
··· 402 402 403 403 body := r.FormValue("body") 404 404 if body == "" { 405 - rp.pages.Notice(w, "issue", "Body is required") 405 + rp.pages.Notice(w, "issue-comment", "Body is required") 406 406 return 407 407 } 408 408 409 - replyToUri := r.FormValue("reply-to") 410 - var replyTo *string 411 - if replyToUri != "" { 412 - replyTo = &replyToUri 409 + var replyTo *syntax.ATURI 410 + replyToRaw := r.FormValue("reply-to") 411 + if replyToRaw != "" { 412 + aturi, err := syntax.ParseATURI(replyToRaw) 413 + if err != nil { 414 + rp.pages.Notice(w, "issue-comment", "reply-to should be valid AT-URI") 415 + return 416 + } 417 + replyTo = &aturi 413 418 } 414 419 415 420 mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 416 421 417 - comment := models.IssueComment{ 418 - Did: user.Did, 422 + comment := models.Comment{ 423 + Did: syntax.DID(user.Did), 424 + Collection: tangled.CommentNSID, 419 425 Rkey: tid.TID(), 420 - IssueAt: issue.AtUri().String(), 426 + Subject: issue.AtUri(), 421 427 ReplyTo: replyTo, 422 428 Body: body, 423 429 Created: time.Now(), 424 430 Mentions: mentions, 425 431 References: references, 426 432 } 427 - if err = rp.validator.ValidateIssueComment(&comment); err != nil { 433 + if err = comment.Validate(); err != nil { 428 434 l.Error("failed to validate comment", "err", err) 429 435 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 430 436 return 431 437 } 432 - record := comment.AsRecord() 433 438 434 439 client, err := rp.oauth.AuthorizedClient(r) 435 440 if err != nil { ··· 440 445 441 446 // create a record first 442 447 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 443 - Collection: tangled.RepoIssueCommentNSID, 444 - Repo: comment.Did, 448 + Collection: comment.Collection.String(), 449 + Repo: comment.Did.String(), 445 450 Rkey: comment.Rkey, 446 451 Record: &lexutil.LexiconTypeDecoder{ 447 - Val: &record, 452 + Val: comment.AsRecord(), 448 453 }, 449 454 }) 450 455 if err != nil { ··· 467 472 } 468 473 defer tx.Rollback() 469 474 470 - commentId, err := db.AddIssueComment(tx, comment) 475 + err = db.PutComment(tx, &comment) 471 476 if err != nil { 472 477 l.Error("failed to create comment", "err", err) 473 478 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") ··· 483 488 // reset atUri to make rollback a no-op 484 489 atUri = "" 485 490 486 - // notify about the new comment 487 - comment.Id = commentId 488 - 489 - rp.notifier.NewIssueComment(r.Context(), &comment, mentions) 491 + rp.notifier.NewComment(r.Context(), &comment) 490 492 491 493 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 492 - rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId)) 494 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, comment.Id)) 493 495 } 494 496 495 497 func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { ··· 504 506 } 505 507 506 508 commentId := chi.URLParam(r, "commentId") 507 - comments, err := db.GetIssueComments( 509 + comments, err := db.GetComments( 508 510 rp.db, 509 511 orm.FilterEq("id", commentId), 510 512 ) ··· 540 542 } 541 543 542 544 commentId := chi.URLParam(r, "commentId") 543 - comments, err := db.GetIssueComments( 545 + comments, err := db.GetComments( 544 546 rp.db, 545 547 orm.FilterEq("id", commentId), 546 548 ) ··· 556 558 } 557 559 comment := comments[0] 558 560 559 - if comment.Did != user.Did { 561 + if comment.Did.String() != user.Did { 560 562 l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did) 561 563 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 562 564 return ··· 585 587 newComment.Body = newBody 586 588 newComment.Edited = &now 587 589 newComment.Mentions, newComment.References = rp.mentionsResolver.Resolve(r.Context(), newBody) 588 - 589 - record := newComment.AsRecord() 590 590 591 591 tx, err := rp.db.Begin() 592 592 if err != nil { ··· 596 596 } 597 597 defer tx.Rollback() 598 598 599 - _, err = db.AddIssueComment(tx, newComment) 599 + err = db.PutComment(tx, &newComment) 600 600 if err != nil { 601 601 l.Error("failed to perferom update-description query", "err", err) 602 602 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") ··· 606 606 607 607 // rkey is optional, it was introduced later 608 608 if newComment.Rkey != "" { 609 + // TODO: update correct comment 610 + 609 611 // update the record on pds 610 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey) 612 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", newComment.Collection.String(), newComment.Did.String(), newComment.Rkey) 611 613 if err != nil { 612 614 l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 613 - rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") 615 + rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update comment, no record found on PDS.") 614 616 return 615 617 } 616 618 617 619 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 618 - Collection: tangled.RepoIssueCommentNSID, 619 - Repo: user.Did, 620 + Collection: newComment.Collection.String(), 621 + Repo: newComment.Did.String(), 620 622 Rkey: newComment.Rkey, 621 623 SwapRecord: ex.Cid, 622 624 Record: &lexutil.LexiconTypeDecoder{ 623 - Val: &record, 625 + Val: newComment.AsRecord(), 624 626 }, 625 627 }) 626 628 if err != nil { ··· 650 652 } 651 653 652 654 commentId := chi.URLParam(r, "commentId") 653 - comments, err := db.GetIssueComments( 655 + comments, err := db.GetComments( 654 656 rp.db, 655 657 orm.FilterEq("id", commentId), 656 658 ) ··· 686 688 } 687 689 688 690 commentId := chi.URLParam(r, "commentId") 689 - comments, err := db.GetIssueComments( 691 + comments, err := db.GetComments( 690 692 rp.db, 691 693 orm.FilterEq("id", commentId), 692 694 ) ··· 722 724 } 723 725 724 726 commentId := chi.URLParam(r, "commentId") 725 - comments, err := db.GetIssueComments( 727 + comments, err := db.GetComments( 726 728 rp.db, 727 729 orm.FilterEq("id", commentId), 728 730 ) ··· 738 740 } 739 741 comment := comments[0] 740 742 741 - if comment.Did != user.Did { 743 + if comment.Did.String() != user.Did { 742 744 l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did) 743 745 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 744 746 return ··· 751 753 752 754 // optimistic deletion 753 755 deleted := time.Now() 754 - err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id)) 756 + err = db.DeleteComments(rp.db, orm.FilterEq("id", comment.Id)) 755 757 if err != nil { 756 758 l.Error("failed to delete comment", "err", err) 757 759 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") ··· 767 769 return 768 770 } 769 771 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 770 - Collection: tangled.RepoIssueCommentNSID, 771 - Repo: user.Did, 772 + Collection: comment.Collection.String(), 773 + Repo: comment.Did.String(), 772 774 Rkey: comment.Rkey, 773 775 }) 774 776 if err != nil {
+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
+110 -113
appview/notify/db/db.go
··· 74 74 // no-op 75 75 } 76 76 77 - func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 78 - collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 77 + func (n *databaseNotifier) NewComment(ctx context.Context, comment *models.Comment) { 78 + var ( 79 + // built the recipients list: 80 + // - the owner of the repo 81 + // - | if the comment is a reply -> everybody on that thread 82 + // | if the comment is a top level -> just the issue owner 83 + // - remove mentioned users from the recipients list 84 + recipients = sets.New[syntax.DID]() 85 + entityType string 86 + entityId string 87 + repoId *int64 88 + issueId *int64 89 + pullId *int64 90 + ) 91 + 92 + subjectDid, err := comment.Subject.Authority().AsDID() 79 93 if err != nil { 80 - log.Printf("failed to fetch collaborators: %v", err) 94 + log.Printf("NewComment: expected did based at-uri for comment.subject") 81 95 return 82 96 } 97 + switch comment.Subject.Collection() { 98 + case tangled.RepoIssueNSID: 99 + issues, err := db.GetIssues( 100 + n.db, 101 + orm.FilterEq("did", subjectDid), 102 + orm.FilterEq("rkey", comment.Subject.RecordKey()), 103 + ) 104 + if err != nil { 105 + log.Printf("NewComment: failed to get issues: %v", err) 106 + return 107 + } 108 + if len(issues) == 0 { 109 + log.Printf("NewComment: no issue found for %s", comment.Subject) 110 + return 111 + } 112 + issue := issues[0] 83 113 84 - // build the recipients list 85 - // - owner of the repo 86 - // - collaborators in the repo 87 - // - remove users already mentioned 88 - recipients := sets.Singleton(syntax.DID(issue.Repo.Did)) 89 - for _, c := range collaborators { 90 - recipients.Insert(c.SubjectDid) 114 + recipients.Insert(syntax.DID(issue.Repo.Did)) 115 + if comment.IsReply() { 116 + // if this comment is a reply, then notify everybody in that thread 117 + parentAtUri := *comment.ReplyTo 118 + 119 + // find the parent thread, and add all DIDs from here to the recipient list 120 + for _, t := range issue.CommentList() { 121 + if t.Self.AtUri() == parentAtUri { 122 + for _, p := range t.Participants() { 123 + recipients.Insert(p) 124 + } 125 + } 126 + } 127 + } else { 128 + // not a reply, notify just the issue author 129 + recipients.Insert(syntax.DID(issue.Did)) 130 + } 131 + 132 + entityType = "issue" 133 + entityId = issue.AtUri().String() 134 + repoId = &issue.Repo.Id 135 + issueId = &issue.Id 136 + case tangled.RepoPullNSID: 137 + pulls, err := db.GetPulls( 138 + n.db, 139 + orm.FilterEq("owner_did", subjectDid), 140 + orm.FilterEq("rkey", comment.Subject.RecordKey()), 141 + ) 142 + if err != nil { 143 + log.Printf("NewComment: failed to get pulls: %v", err) 144 + return 145 + } 146 + if len(pulls) == 0 { 147 + log.Printf("NewComment: no pull found for %s", comment.Subject) 148 + return 149 + } 150 + pull := pulls[0] 151 + 152 + pull.Repo, err = db.GetRepo(n.db, orm.FilterEq("at_uri", pull.RepoAt)) 153 + if err != nil { 154 + log.Printf("NewComment: failed to get repos: %v", err) 155 + return 156 + } 157 + 158 + recipients.Insert(syntax.DID(pull.Repo.Did)) 159 + for _, p := range pull.Participants() { 160 + recipients.Insert(syntax.DID(p)) 161 + } 162 + 163 + entityType = "pull" 164 + entityId = pull.AtUri().String() 165 + repoId = &pull.Repo.Id 166 + p := int64(pull.ID) 167 + pullId = &p 168 + default: 169 + return // no-op 91 170 } 92 - for _, m := range mentions { 171 + 172 + for _, m := range comment.Mentions { 93 173 recipients.Remove(m) 94 174 } 95 175 96 - actorDid := syntax.DID(issue.Did) 97 - entityType := "issue" 98 - entityId := issue.AtUri().String() 99 - repoId := &issue.Repo.Id 100 - issueId := &issue.Id 101 - var pullId *int64 102 - 103 176 n.notifyEvent( 104 - actorDid, 177 + comment.Did, 105 178 recipients, 106 - models.NotificationTypeIssueCreated, 179 + models.NotificationTypeIssueCommented, 107 180 entityType, 108 181 entityId, 109 182 repoId, ··· 111 184 pullId, 112 185 ) 113 186 n.notifyEvent( 114 - actorDid, 115 - sets.Collect(slices.Values(mentions)), 187 + comment.Did, 188 + sets.Collect(slices.Values(comment.Mentions)), 116 189 models.NotificationTypeUserMentioned, 117 190 entityType, 118 191 entityId, ··· 122 195 ) 123 196 } 124 197 125 - func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 126 - issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt)) 198 + func (n *databaseNotifier) DeleteComment(ctx context.Context, comment *models.Comment) { 199 + // no-op 200 + } 201 + 202 + func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 203 + collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 127 204 if err != nil { 128 - log.Printf("NewIssueComment: failed to get issues: %v", err) 205 + log.Printf("failed to fetch collaborators: %v", err) 129 206 return 130 207 } 131 - if len(issues) == 0 { 132 - log.Printf("NewIssueComment: no issue found for %s", comment.IssueAt) 133 - return 134 - } 135 - issue := issues[0] 136 208 137 - // built the recipients list: 138 - // - the owner of the repo 139 - // - | if the comment is a reply -> everybody on that thread 140 - // | if the comment is a top level -> just the issue owner 141 - // - remove mentioned users from the recipients list 209 + // build the recipients list 210 + // - owner of the repo 211 + // - collaborators in the repo 212 + // - remove users already mentioned 142 213 recipients := sets.Singleton(syntax.DID(issue.Repo.Did)) 143 - 144 - if comment.IsReply() { 145 - // if this comment is a reply, then notify everybody in that thread 146 - parentAtUri := *comment.ReplyTo 147 - 148 - // find the parent thread, and add all DIDs from here to the recipient list 149 - for _, t := range issue.CommentList() { 150 - if t.Self.AtUri().String() == parentAtUri { 151 - for _, p := range t.Participants() { 152 - recipients.Insert(p) 153 - } 154 - } 155 - } 156 - } else { 157 - // not a reply, notify just the issue author 158 - recipients.Insert(syntax.DID(issue.Did)) 214 + for _, c := range collaborators { 215 + recipients.Insert(c.SubjectDid) 159 216 } 160 - 161 217 for _, m := range mentions { 162 218 recipients.Remove(m) 163 219 } 164 220 165 - actorDid := syntax.DID(comment.Did) 221 + actorDid := syntax.DID(issue.Did) 166 222 entityType := "issue" 167 223 entityId := issue.AtUri().String() 168 224 repoId := &issue.Repo.Id ··· 172 228 n.notifyEvent( 173 229 actorDid, 174 230 recipients, 175 - models.NotificationTypeIssueCommented, 231 + models.NotificationTypeIssueCreated, 176 232 entityType, 177 233 entityId, 178 234 repoId, ··· 252 308 actorDid, 253 309 recipients, 254 310 eventType, 255 - entityType, 256 - entityId, 257 - repoId, 258 - issueId, 259 - pullId, 260 - ) 261 - } 262 - 263 - func (n *databaseNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) { 264 - pull, err := db.GetPull(n.db, 265 - syntax.ATURI(comment.RepoAt), 266 - comment.PullId, 267 - ) 268 - if err != nil { 269 - log.Printf("NewPullComment: failed to get pulls: %v", err) 270 - return 271 - } 272 - 273 - repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", comment.RepoAt)) 274 - if err != nil { 275 - log.Printf("NewPullComment: failed to get repos: %v", err) 276 - return 277 - } 278 - 279 - // build up the recipients list: 280 - // - repo owner 281 - // - all pull participants 282 - // - remove those already mentioned 283 - recipients := sets.Singleton(syntax.DID(repo.Did)) 284 - for _, p := range pull.Participants() { 285 - recipients.Insert(syntax.DID(p)) 286 - } 287 - for _, m := range mentions { 288 - recipients.Remove(m) 289 - } 290 - 291 - actorDid := syntax.DID(comment.OwnerDid) 292 - eventType := models.NotificationTypePullCommented 293 - entityType := "pull" 294 - entityId := pull.AtUri().String() 295 - repoId := &repo.Id 296 - var issueId *int64 297 - p := int64(pull.ID) 298 - pullId := &p 299 - 300 - n.notifyEvent( 301 - actorDid, 302 - recipients, 303 - eventType, 304 - entityType, 305 - entityId, 306 - repoId, 307 - issueId, 308 - pullId, 309 - ) 310 - n.notifyEvent( 311 - actorDid, 312 - sets.Collect(slices.Values(mentions)), 313 - models.NotificationTypeUserMentioned, 314 311 entityType, 315 312 entityId, 316 313 repoId,
+8 -8
appview/notify/merged_notifier.go
··· 53 53 m.fanout("DeleteStar", ctx, star) 54 54 } 55 55 56 - func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 57 - m.fanout("NewIssue", ctx, issue, mentions) 56 + func (m *mergedNotifier) NewComment(ctx context.Context, comment *models.Comment) { 57 + m.fanout("NewComment", ctx, comment) 58 58 } 59 59 60 - func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 61 - m.fanout("NewIssueComment", ctx, comment, mentions) 60 + func (m *mergedNotifier) DeleteComment(ctx context.Context, comment *models.Comment) { 61 + m.fanout("DeleteComment", ctx, comment) 62 + } 63 + 64 + func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 65 + m.fanout("NewIssue", ctx, issue, mentions) 62 66 } 63 67 64 68 func (m *mergedNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) { ··· 79 83 80 84 func (m *mergedNotifier) NewPull(ctx context.Context, pull *models.Pull) { 81 85 m.fanout("NewPull", ctx, pull) 82 - } 83 - 84 - func (m *mergedNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) { 85 - m.fanout("NewPullComment", ctx, comment, mentions) 86 86 } 87 87 88 88 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 UpdateProfile(ctx context.Context, profile *models.Profile) ··· 42 43 func (m *BaseNotifier) NewStar(ctx context.Context, star *models.Star) {} 43 44 func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {} 44 45 46 + func (m *BaseNotifier) NewComment(ctx context.Context, comment *models.Comment) {} 47 + func (m *BaseNotifier) DeleteComment(ctx context.Context, comment *models.Comment) {} 48 + 45 49 func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {} 46 - func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 47 - } 48 50 func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {} 49 51 func (m *BaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {} 50 52 51 53 func (m *BaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {} 52 54 func (m *BaseNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {} 53 55 54 - func (m *BaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {} 55 - func (m *BaseNotifier) NewPullComment(ctx context.Context, models *models.PullComment, mentions []syntax.DID) { 56 - } 56 + func (m *BaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {} 57 57 func (m *BaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {} 58 58 59 59 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 {
+4 -4
appview/pages/pages.go
··· 1004 1004 LoggedInUser *oauth.MultiAccountUser 1005 1005 RepoInfo repoinfo.RepoInfo 1006 1006 Issue *models.Issue 1007 - Comment *models.IssueComment 1007 + Comment *models.Comment 1008 1008 } 1009 1009 1010 1010 func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { ··· 1015 1015 LoggedInUser *oauth.MultiAccountUser 1016 1016 RepoInfo repoinfo.RepoInfo 1017 1017 Issue *models.Issue 1018 - Comment *models.IssueComment 1018 + Comment *models.Comment 1019 1019 } 1020 1020 1021 1021 func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { ··· 1026 1026 LoggedInUser *oauth.MultiAccountUser 1027 1027 RepoInfo repoinfo.RepoInfo 1028 1028 Issue *models.Issue 1029 - Comment *models.IssueComment 1029 + Comment *models.Comment 1030 1030 } 1031 1031 1032 1032 func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { ··· 1037 1037 LoggedInUser *oauth.MultiAccountUser 1038 1038 RepoInfo repoinfo.RepoInfo 1039 1039 Issue *models.Issue 1040 - Comment *models.IssueComment 1040 + Comment *models.Comment 1041 1041 } 1042 1042 1043 1043 func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
+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" . }}
+5 -4
appview/pages/templates/repo/pulls/pull.html
··· 561 561 {{ end }} 562 562 563 563 {{ define "submissionComment" }} 564 - <div id="comment-{{.ID}}" class="flex gap-2 -ml-4 py-4 w-full mx-auto"> 564 + <div id="comment-{{.Id}}" class="flex gap-2 -ml-4 py-4 w-full mx-auto"> 565 565 <!-- left column: profile picture --> 566 566 <div class="flex-shrink-0 h-fit relative"> 567 - {{ template "user/fragments/picLink" (list .OwnerDid "size-8") }} 567 + {{ template "user/fragments/picLink" (list .Did.String "size-8") }} 568 568 </div> 569 569 <!-- right column: name and body in two rows --> 570 570 <div class="flex-1 min-w-0"> 571 571 <!-- Row 1: Author and timestamp --> 572 572 <div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1"> 573 - {{ $handle := resolve .OwnerDid }} 573 + {{ $handle := resolve .Did.String }} 574 574 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="/{{ $handle }}">{{ $handle }}</a> 575 575 <span class="before:content-['·']"></span> 576 - <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}"> 576 + <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.Id}}"> 577 + {{ template "repo/fragments/time" .Created }} 577 578 {{ template "repo/fragments/shortTime" .Created }} 578 579 </a> 579 580 </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!"
+1 -1
appview/pulls/opengraph.go
··· 277 277 } 278 278 279 279 // Get comment count from database 280 - comments, err := db.GetPullComments(s.db, orm.FilterEq("pull_id", pull.ID)) 280 + comments, err := db.GetComments(s.db, orm.FilterEq("subject_at", pull.AtUri())) 281 281 if err != nil { 282 282 log.Printf("failed to get pull comments: %v", err) 283 283 }
+26 -24
appview/pulls/pulls.go
··· 727 727 } 728 728 defer tx.Rollback() 729 729 730 - createdAt := time.Now().Format(time.RFC3339) 730 + comment := models.Comment{ 731 + Did: syntax.DID(user.Did), 732 + Collection: tangled.CommentNSID, 733 + Rkey: tid.TID(), 734 + Subject: pull.AtUri(), 735 + ReplyTo: nil, 736 + Body: body, 737 + Created: time.Now(), 738 + Mentions: mentions, 739 + References: references, 740 + PullSubmissionId: &pull.Submissions[roundNumber].ID, 741 + } 742 + if err = comment.Validate(); err != nil { 743 + log.Println("failed to validate comment", err) 744 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 745 + return 746 + } 731 747 732 748 client, err := s.oauth.AuthorizedClient(r) 733 749 if err != nil { ··· 735 751 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 736 752 return 737 753 } 738 - atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 739 - Collection: tangled.RepoPullCommentNSID, 740 - Repo: user.Did, 741 - Rkey: tid.TID(), 754 + 755 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 756 + Collection: comment.Collection.String(), 757 + Repo: comment.Did.String(), 758 + Rkey: comment.Rkey, 742 759 Record: &lexutil.LexiconTypeDecoder{ 743 - Val: &tangled.RepoPullComment{ 744 - Pull: pull.AtUri().String(), 745 - Body: body, 746 - CreatedAt: createdAt, 747 - }, 760 + Val: comment.AsRecord(), 748 761 }, 749 762 }) 750 763 if err != nil { ··· 753 766 return 754 767 } 755 768 756 - comment := &models.PullComment{ 757 - OwnerDid: user.Did, 758 - RepoAt: f.RepoAt().String(), 759 - PullId: pull.PullId, 760 - Body: body, 761 - CommentAt: atResp.Uri, 762 - SubmissionId: pull.Submissions[roundNumber].ID, 763 - Mentions: mentions, 764 - References: references, 765 - } 766 - 767 769 // Create the pull comment in the database with the commentAt field 768 - commentId, err := db.NewPullComment(tx, comment) 770 + err = db.PutComment(tx, &comment) 769 771 if err != nil { 770 772 log.Println("failed to create pull comment", err) 771 773 s.pages.Notice(w, "pull-comment", "Failed to create comment.") ··· 779 781 return 780 782 } 781 783 782 - s.notifier.NewPullComment(r.Context(), comment, mentions) 784 + s.notifier.NewComment(r.Context(), &comment) 783 785 784 786 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 785 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, commentId)) 787 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, comment.Id)) 786 788 return 787 789 } 788 790 }
+1
appview/state/state.go
··· 118 118 tangled.StringNSID, 119 119 tangled.RepoIssueNSID, 120 120 tangled.RepoIssueCommentNSID, 121 + tangled.CommentNSID, 121 122 tangled.LabelDefinitionNSID, 122 123 tangled.LabelOpNSID, 123 124 },
-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 == "" {
+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 + MIIBozCCAUmgAwIBAgIQRnYoKs3BuihlLFeydgURVzAKBggqhkjOPQQDAjAwMS4w 3 + LAYDVQQDEyVDYWRkeSBMb2NhbCBBdXRob3JpdHkgLSAyMDI2IEVDQyBSb290MB4X 4 + DTI2MDEwODEzNTk1MloXDTM1MTExNzEzNTk1MlowMDEuMCwGA1UEAxMlQ2FkZHkg 5 + TG9jYWwgQXV0aG9yaXR5IC0gMjAyNiBFQ0MgUm9vdDBZMBMGByqGSM49AgEGCCqG 6 + SM49AwEHA0IABCQlYShhxLaX8/ZP7rcBtD5xL4u3wYMe77JS/lRFjjpAUGmJPxUE 7 + ctsNvukG1hU4MeLMSqAEIqFWjs8dQBxLjGSjRTBDMA4GA1UdDwEB/wQEAwIBBjAS 8 + BgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBQ7Mt/6izTOOXCSWDS6HrwrqMDB 9 + vzAKBggqhkjOPQQDAgNIADBFAiEA9QAYIuHR5qsGJ1JMZnuAAQpEwaqewhUICsKO 10 + e2fWj4ACICPgj9Kh9++8FH5eVyDI1AD/BLwmMmiaqs1ojZT7QJqb 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
··· 95 95 knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {}; 96 96 knot = self.callPackage ./nix/pkgs/knot.nix {}; 97 97 dolly = self.callPackage ./nix/pkgs/dolly.nix {}; 98 + did-method-plc = self.callPackage ./nix/pkgs/did-method-plc.nix {}; 99 + bluesky-jetstream = self.callPackage ./nix/pkgs/bluesky-jetstream.nix {}; 100 + bluesky-relay = self.callPackage ./nix/pkgs/bluesky-relay.nix {}; 101 + tap = self.callPackage ./nix/pkgs/tap.nix {}; 98 102 }); 99 103 in { 100 104 overlays.default = final: prev: { 101 - inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs dolly; 105 + inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs dolly did-method-plc bluesky-jetstream bluesky-relay tap; 102 106 }; 103 107 104 108 packages = forAllSystems (system: let ··· 119 123 sqlite-lib 120 124 docs 121 125 dolly 126 + did-method-plc 127 + bluesky-jetstream 128 + bluesky-relay 129 + tap 122 130 ; 123 131 124 132 pkgsStatic-appview = staticPackages.appview; ··· 248 256 rootDir=$(jj --ignore-working-copy root || git rev-parse --show-toplevel) || (echo "error: can't find repo root?"; exit 1) 249 257 cd "$rootDir" 250 258 251 - mkdir -p nix/vm-data/{knot,repos,spindle,spindle-logs} 259 + mkdir -p nix/vm-data/{caddy,knot,repos,spindle,spindle-logs} 252 260 253 261 export TANGLED_VM_DATA_DIR="$rootDir/nix/vm-data" 254 262 exec ${pkgs.lib.getExe ··· 323 331 imports = [./nix/modules/spindle.nix]; 324 332 325 333 services.tangled.spindle.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.spindle; 334 + }; 335 + nixosModules.did-method-plc = { 336 + lib, 337 + pkgs, 338 + ... 339 + }: { 340 + imports = [./nix/modules/did-method-plc.nix]; 341 + services.did-method-plc.package = lib.mkDefault self.packages.${pkgs.system}.did-method-plc; 342 + }; 343 + nixosModules.bluesky-relay = { 344 + lib, 345 + pkgs, 346 + ... 347 + }: { 348 + imports = [./nix/modules/bluesky-relay.nix]; 349 + services.bluesky-relay.package = lib.mkDefault self.packages.${pkgs.system}.bluesky-relay; 350 + }; 351 + nixosModules.bluesky-jetstream = { 352 + lib, 353 + pkgs, 354 + ... 355 + }: { 356 + imports = [./nix/modules/bluesky-jetstream.nix]; 357 + services.bluesky-jetstream.package = lib.mkDefault self.packages.${pkgs.system}.bluesky-jetstream; 326 358 }; 327 359 }; 328 360 }
+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 + }
+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