Yōten: A social tracker for your language learning journey built on the atproto.

feat: add comment lexicon and db struct

brookjeynes.dev 4639bdba 6ccbe4bd

verified
Changed files
+703 -161
api
cmd
internal
lexicons
+376
api/yoten/cbor_gen.go
··· 648 648 649 649 return nil 650 650 } 651 + func (t *FeedComment) MarshalCBOR(w io.Writer) error { 652 + if t == nil { 653 + _, err := w.Write(cbg.CborNull) 654 + return err 655 + } 656 + 657 + cw := cbg.NewCborWriter(w) 658 + fieldCount := 5 659 + 660 + if t.Reply == nil { 661 + fieldCount-- 662 + } 663 + 664 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 665 + return err 666 + } 667 + 668 + // t.Body (string) (string) 669 + if len("body") > 1000000 { 670 + return xerrors.Errorf("Value in field \"body\" was too long") 671 + } 672 + 673 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil { 674 + return err 675 + } 676 + if _, err := cw.WriteString(string("body")); err != nil { 677 + return err 678 + } 679 + 680 + if len(t.Body) > 1000000 { 681 + return xerrors.Errorf("Value in field t.Body was too long") 682 + } 683 + 684 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Body))); err != nil { 685 + return err 686 + } 687 + if _, err := cw.WriteString(string(t.Body)); err != nil { 688 + return err 689 + } 690 + 691 + // t.LexiconTypeID (string) (string) 692 + if len("$type") > 1000000 { 693 + return xerrors.Errorf("Value in field \"$type\" was too long") 694 + } 695 + 696 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 697 + return err 698 + } 699 + if _, err := cw.WriteString(string("$type")); err != nil { 700 + return err 701 + } 702 + 703 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("app.yoten.feed.comment"))); err != nil { 704 + return err 705 + } 706 + if _, err := cw.WriteString(string("app.yoten.feed.comment")); err != nil { 707 + return err 708 + } 709 + 710 + // t.Reply (yoten.FeedComment_Reply) (struct) 711 + if t.Reply != nil { 712 + 713 + if len("reply") > 1000000 { 714 + return xerrors.Errorf("Value in field \"reply\" was too long") 715 + } 716 + 717 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("reply"))); err != nil { 718 + return err 719 + } 720 + if _, err := cw.WriteString(string("reply")); err != nil { 721 + return err 722 + } 723 + 724 + if err := t.Reply.MarshalCBOR(cw); err != nil { 725 + return err 726 + } 727 + } 728 + 729 + // t.Subject (string) (string) 730 + if len("subject") > 1000000 { 731 + return xerrors.Errorf("Value in field \"subject\" was too long") 732 + } 733 + 734 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil { 735 + return err 736 + } 737 + if _, err := cw.WriteString(string("subject")); err != nil { 738 + return err 739 + } 740 + 741 + if len(t.Subject) > 1000000 { 742 + return xerrors.Errorf("Value in field t.Subject was too long") 743 + } 744 + 745 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Subject))); err != nil { 746 + return err 747 + } 748 + if _, err := cw.WriteString(string(t.Subject)); err != nil { 749 + return err 750 + } 751 + 752 + // t.CreatedAt (string) (string) 753 + if len("createdAt") > 1000000 { 754 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 755 + } 756 + 757 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 758 + return err 759 + } 760 + if _, err := cw.WriteString(string("createdAt")); err != nil { 761 + return err 762 + } 763 + 764 + if len(t.CreatedAt) > 1000000 { 765 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 766 + } 767 + 768 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 769 + return err 770 + } 771 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 772 + return err 773 + } 774 + return nil 775 + } 776 + 777 + func (t *FeedComment) UnmarshalCBOR(r io.Reader) (err error) { 778 + *t = FeedComment{} 779 + 780 + cr := cbg.NewCborReader(r) 781 + 782 + maj, extra, err := cr.ReadHeader() 783 + if err != nil { 784 + return err 785 + } 786 + defer func() { 787 + if err == io.EOF { 788 + err = io.ErrUnexpectedEOF 789 + } 790 + }() 791 + 792 + if maj != cbg.MajMap { 793 + return fmt.Errorf("cbor input should be of type map") 794 + } 795 + 796 + if extra > cbg.MaxLength { 797 + return fmt.Errorf("FeedComment: map struct too large (%d)", extra) 798 + } 799 + 800 + n := extra 801 + 802 + nameBuf := make([]byte, 9) 803 + for i := uint64(0); i < n; i++ { 804 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 805 + if err != nil { 806 + return err 807 + } 808 + 809 + if !ok { 810 + // Field doesn't exist on this type, so ignore it 811 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 812 + return err 813 + } 814 + continue 815 + } 816 + 817 + switch string(nameBuf[:nameLen]) { 818 + // t.Body (string) (string) 819 + case "body": 820 + 821 + { 822 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 823 + if err != nil { 824 + return err 825 + } 826 + 827 + t.Body = string(sval) 828 + } 829 + // t.LexiconTypeID (string) (string) 830 + case "$type": 831 + 832 + { 833 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 834 + if err != nil { 835 + return err 836 + } 837 + 838 + t.LexiconTypeID = string(sval) 839 + } 840 + // t.Reply (yoten.FeedComment_Reply) (struct) 841 + case "reply": 842 + 843 + { 844 + 845 + b, err := cr.ReadByte() 846 + if err != nil { 847 + return err 848 + } 849 + if b != cbg.CborNull[0] { 850 + if err := cr.UnreadByte(); err != nil { 851 + return err 852 + } 853 + t.Reply = new(FeedComment_Reply) 854 + if err := t.Reply.UnmarshalCBOR(cr); err != nil { 855 + return xerrors.Errorf("unmarshaling t.Reply pointer: %w", err) 856 + } 857 + } 858 + 859 + } 860 + // t.Subject (string) (string) 861 + case "subject": 862 + 863 + { 864 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 865 + if err != nil { 866 + return err 867 + } 868 + 869 + t.Subject = string(sval) 870 + } 871 + // t.CreatedAt (string) (string) 872 + case "createdAt": 873 + 874 + { 875 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 876 + if err != nil { 877 + return err 878 + } 879 + 880 + t.CreatedAt = string(sval) 881 + } 882 + 883 + default: 884 + // Field doesn't exist on this type, so ignore it 885 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 886 + return err 887 + } 888 + } 889 + } 890 + 891 + return nil 892 + } 893 + func (t *FeedComment_Reply) MarshalCBOR(w io.Writer) error { 894 + if t == nil { 895 + _, err := w.Write(cbg.CborNull) 896 + return err 897 + } 898 + 899 + cw := cbg.NewCborWriter(w) 900 + 901 + if _, err := cw.Write([]byte{162}); err != nil { 902 + return err 903 + } 904 + 905 + // t.Root (string) (string) 906 + if len("root") > 1000000 { 907 + return xerrors.Errorf("Value in field \"root\" was too long") 908 + } 909 + 910 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("root"))); err != nil { 911 + return err 912 + } 913 + if _, err := cw.WriteString(string("root")); err != nil { 914 + return err 915 + } 916 + 917 + if len(t.Root) > 1000000 { 918 + return xerrors.Errorf("Value in field t.Root was too long") 919 + } 920 + 921 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Root))); err != nil { 922 + return err 923 + } 924 + if _, err := cw.WriteString(string(t.Root)); err != nil { 925 + return err 926 + } 927 + 928 + // t.Parent (string) (string) 929 + if len("parent") > 1000000 { 930 + return xerrors.Errorf("Value in field \"parent\" was too long") 931 + } 932 + 933 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("parent"))); err != nil { 934 + return err 935 + } 936 + if _, err := cw.WriteString(string("parent")); err != nil { 937 + return err 938 + } 939 + 940 + if len(t.Parent) > 1000000 { 941 + return xerrors.Errorf("Value in field t.Parent was too long") 942 + } 943 + 944 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Parent))); err != nil { 945 + return err 946 + } 947 + if _, err := cw.WriteString(string(t.Parent)); err != nil { 948 + return err 949 + } 950 + return nil 951 + } 952 + 953 + func (t *FeedComment_Reply) UnmarshalCBOR(r io.Reader) (err error) { 954 + *t = FeedComment_Reply{} 955 + 956 + cr := cbg.NewCborReader(r) 957 + 958 + maj, extra, err := cr.ReadHeader() 959 + if err != nil { 960 + return err 961 + } 962 + defer func() { 963 + if err == io.EOF { 964 + err = io.ErrUnexpectedEOF 965 + } 966 + }() 967 + 968 + if maj != cbg.MajMap { 969 + return fmt.Errorf("cbor input should be of type map") 970 + } 971 + 972 + if extra > cbg.MaxLength { 973 + return fmt.Errorf("FeedComment_Reply: map struct too large (%d)", extra) 974 + } 975 + 976 + n := extra 977 + 978 + nameBuf := make([]byte, 6) 979 + for i := uint64(0); i < n; i++ { 980 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 981 + if err != nil { 982 + return err 983 + } 984 + 985 + if !ok { 986 + // Field doesn't exist on this type, so ignore it 987 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 988 + return err 989 + } 990 + continue 991 + } 992 + 993 + switch string(nameBuf[:nameLen]) { 994 + // t.Root (string) (string) 995 + case "root": 996 + 997 + { 998 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 999 + if err != nil { 1000 + return err 1001 + } 1002 + 1003 + t.Root = string(sval) 1004 + } 1005 + // t.Parent (string) (string) 1006 + case "parent": 1007 + 1008 + { 1009 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1010 + if err != nil { 1011 + return err 1012 + } 1013 + 1014 + t.Parent = string(sval) 1015 + } 1016 + 1017 + default: 1018 + // Field doesn't exist on this type, so ignore it 1019 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 1020 + return err 1021 + } 1022 + } 1023 + } 1024 + 1025 + return nil 1026 + } 651 1027 func (t *FeedReaction) MarshalCBOR(w io.Writer) error { 652 1028 if t == nil { 653 1029 _, err := w.Write(cbg.CborNull)
+35
api/yoten/feedcomment.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package yoten 4 + 5 + // schema: app.yoten.feed.comment 6 + 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 11 + const ( 12 + FeedCommentNSID = "app.yoten.feed.comment" 13 + ) 14 + 15 + func init() { 16 + util.RegisterType("app.yoten.feed.comment", &FeedComment{}) 17 + } // 18 + // RECORDTYPE: FeedComment 19 + type FeedComment struct { 20 + LexiconTypeID string `json:"$type,const=app.yoten.feed.comment" cborgen:"$type,const=app.yoten.feed.comment"` 21 + Body string `json:"body" cborgen:"body"` 22 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 + // reply: Indicates that this comment is a reply to another comment. 24 + Reply *FeedComment_Reply `json:"reply,omitempty" cborgen:"reply,omitempty"` 25 + // subject: A reference to the study session being commented on. 26 + Subject string `json:"subject" cborgen:"subject"` 27 + } 28 + 29 + // Indicates that this comment is a reply to another comment. 30 + type FeedComment_Reply struct { 31 + // parent: A reference to the specific comment being replied to. 32 + Parent string `json:"parent" cborgen:"parent"` 33 + // root: A reference to the original study session (the root of the conversation). 34 + Root string `json:"root" cborgen:"root"` 35 + }
+2
cmd/gen.go
··· 28 28 yoten.ActivityDef{}, 29 29 yoten.GraphFollow{}, 30 30 yoten.FeedReaction{}, 31 + yoten.FeedComment{}, 32 + yoten.FeedComment_Reply{}, 31 33 } 32 34 33 35 for name, rt := range AllLexTypes() {
+62
internal/db/comment.go
··· 1 + package db 2 + 3 + import ( 4 + "fmt" 5 + "time" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + "yoten.app/api/yoten" 9 + "yoten.app/internal/types" 10 + ) 11 + 12 + type CommentWithBskyProfile struct { 13 + Comment 14 + BskyProfile types.BskyProfile 15 + } 16 + 17 + type Comment struct { 18 + ID int 19 + Did string 20 + Rkey string 21 + StudySessionUri syntax.ATURI 22 + ParentCommentUri *syntax.ATURI 23 + Body string 24 + IsDeleted bool 25 + CreatedAt time.Time 26 + } 27 + 28 + func (c Comment) CommentAt() syntax.ATURI { 29 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", c.Did, yoten.FeedCommentNSID, c.Rkey)) 30 + } 31 + 32 + func UpsertComment(e Execer, comment Comment) error { 33 + _, err := e.Exec(` 34 + insert into study_sessions ( 35 + id, 36 + did, 37 + rkey, 38 + study_session_uri, 39 + parent_comment_uri, 40 + body, 41 + is_deleted, 42 + created_at 43 + ) 44 + values () 45 + on conflict(did, rkey) do update set 46 + is_deleted = excluded.is_deleted, 47 + body = excluded.body`, 48 + comment.ID, 49 + comment.Did, 50 + comment.Rkey, 51 + comment.StudySessionUri.String(), 52 + comment.ParentCommentUri.String(), 53 + comment.Body, 54 + comment.IsDeleted, 55 + comment.CreatedAt.Format(time.RFC3339), 56 + ) 57 + if err != nil { 58 + return fmt.Errorf("failed to insert or update comment: %w", err) 59 + } 60 + 61 + return nil 62 + }
+177 -161
internal/db/db.go
··· 29 29 return nil, fmt.Errorf("failed to open db: %w", err) 30 30 } 31 31 _, err = db.Exec(` 32 - pragma journal_mode = WAL; 33 - pragma synchronous = normal; 34 - pragma foreign_keys = on; 35 - pragma temp_store = memory; 36 - pragma mmap_size = 30000000000; 37 - pragma page_size = 32768; 38 - pragma auto_vacuum = incremental; 39 - pragma busy_timeout = 5000; 32 + pragma journal_mode = WAL; 33 + pragma synchronous = normal; 34 + pragma foreign_keys = on; 35 + pragma temp_store = memory; 36 + pragma mmap_size = 30000000000; 37 + pragma page_size = 32768; 38 + pragma auto_vacuum = incremental; 39 + pragma busy_timeout = 5000; 40 40 41 - create table if not exists oauth_requests ( 42 - id integer primary key autoincrement, 43 - auth_server_iss text not null, 44 - state text not null, 45 - did text not null, 46 - handle text not null, 47 - pds_url text not null, 48 - pkce_verifier text not null, 49 - dpop_auth_server_nonce text not null, 50 - dpop_private_jwk text not null 51 - ); 41 + create table if not exists oauth_requests ( 42 + id integer primary key autoincrement, 43 + auth_server_iss text not null, 44 + state text not null, 45 + did text not null, 46 + handle text not null, 47 + pds_url text not null, 48 + pkce_verifier text not null, 49 + dpop_auth_server_nonce text not null, 50 + dpop_private_jwk text not null 51 + ); 52 52 53 - create table if not exists oauth_sessions ( 54 - id integer primary key autoincrement, 55 - did text not null, 56 - handle text not null, 57 - pds_url text not null, 58 - auth_server_iss text not null, 59 - access_jwt text not null, 60 - refresh_jwt text not null, 61 - dpop_pds_nonce text, 62 - dpop_auth_server_nonce text not null, 63 - dpop_private_jwk text not null, 64 - expiry text not null 65 - ); 53 + create table if not exists oauth_sessions ( 54 + id integer primary key autoincrement, 55 + did text not null, 56 + handle text not null, 57 + pds_url text not null, 58 + auth_server_iss text not null, 59 + access_jwt text not null, 60 + refresh_jwt text not null, 61 + dpop_pds_nonce text, 62 + dpop_auth_server_nonce text not null, 63 + dpop_private_jwk text not null, 64 + expiry text not null 65 + ); 66 66 67 - create table if not exists profiles ( 68 - -- id 69 - id integer primary key autoincrement, 70 - did text not null, 67 + create table if not exists profiles ( 68 + -- id 69 + id integer primary key autoincrement, 70 + did text not null, 71 71 72 - -- data 73 - display_name text not null, 74 - description text, 75 - location text, 76 - xp integer not null default 0, -- total accumulated xp 77 - level integer not null default 0, 78 - created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 72 + -- data 73 + display_name text not null, 74 + description text, 75 + location text, 76 + xp integer not null default 0, -- total accumulated xp 77 + level integer not null default 0, 78 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 79 79 80 - -- constraints 81 - unique(did) 82 - ); 80 + -- constraints 81 + unique(did) 82 + ); 83 83 84 - create table if not exists profile_languages ( 85 - -- id 86 - did text not null, 84 + create table if not exists profile_languages ( 85 + -- id 86 + did text not null, 87 87 88 - -- data 89 - language_code text not null, 88 + -- data 89 + language_code text not null, 90 90 91 - -- constraints 92 - primary key (did, language_code), 93 - check (length(language_code) = 2), 94 - foreign key (did) references profiles(did) on delete cascade 95 - ); 91 + -- constraints 92 + primary key (did, language_code), 93 + check (length(language_code) = 2), 94 + foreign key (did) references profiles(did) on delete cascade 95 + ); 96 96 97 - create table if not exists study_sessions ( 98 - -- id 99 - did text not null, 100 - rkey text not null, 97 + create table if not exists study_sessions ( 98 + -- id 99 + did text not null, 100 + rkey text not null, 101 101 102 - -- data 103 - activity_id integer not null, 104 - resource_id integer, 105 - description text, 106 - duration integer not null, 107 - language_code text not null, 108 - date text not null, 109 - created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 102 + -- data 103 + activity_id integer not null, 104 + resource_id integer, 105 + description text, 106 + duration integer not null, 107 + language_code text not null, 108 + date text not null, 109 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 110 110 111 - -- constraints 112 - check (length(language_code) = 2), 113 - foreign key (did) references profiles(did) on delete cascade, 114 - foreign key (activity_id) references activities(id) on delete restrict, 115 - foreign key (resource_id) references resources(id) on delete set null, 116 - primary key (did, rkey) 117 - ); 111 + -- constraints 112 + check (length(language_code) = 2), 113 + foreign key (did) references profiles(did) on delete cascade, 114 + foreign key (activity_id) references activities(id) on delete restrict, 115 + foreign key (resource_id) references resources(id) on delete set null, 116 + primary key (did, rkey) 117 + ); 118 118 119 - create table if not exists categories ( 120 - id integer primary key, -- Matches StudySessionCategory iota 121 - name text not null unique 122 - ); 119 + create table if not exists categories ( 120 + id integer primary key, -- Matches StudySessionCategory iota 121 + name text not null unique 122 + ); 123 123 124 - create table if not exists activities ( 125 - id integer primary key autoincrement, 124 + create table if not exists activities ( 125 + id integer primary key autoincrement, 126 126 127 - did text, 128 - rkey text, 127 + did text, 128 + rkey text, 129 129 130 - name text not null, 131 - description text, 132 - status integer not null default 0 check(status in (0, 1)), 133 - created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 130 + name text not null, 131 + description text, 132 + status integer not null default 0 check(status in (0, 1)), 133 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 134 + 135 + foreign key (did) references profiles(did) on delete cascade, 136 + unique (did, rkey) 137 + ); 138 + 139 + create table if not exists activity_categories ( 140 + activity_id integer not null, 141 + category_id integer not null, 142 + 143 + foreign key (activity_id) references activities(id) on delete cascade, 144 + foreign key (category_id) references categories(id) on delete cascade, 145 + primary key (activity_id, category_id) 146 + ); 147 + 148 + create table if not exists follows ( 149 + user_did text not null, 150 + subject_did text not null, 134 151 135 - foreign key (did) references profiles(did) on delete cascade, 136 - unique (did, rkey) 137 - ); 152 + rkey text not null, 153 + followed_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 138 154 139 - create table if not exists activity_categories ( 140 - activity_id integer not null, 141 - category_id integer not null, 155 + primary key (user_did, subject_did), 156 + check (user_did <> subject_did) 157 + ); 142 158 143 - foreign key (activity_id) references activities(id) on delete cascade, 144 - foreign key (category_id) references categories(id) on delete cascade, 145 - primary key (activity_id, category_id) 146 - ); 159 + create table if not exists xp_events ( 160 + id integer primary key autoincrement, 147 161 148 - create table if not exists follows ( 149 - user_did text not null, 150 - subject_did text not null, 162 + did text not null, 163 + session_rkey text not null, 164 + xp_gained integer not null, 165 + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 151 166 152 - rkey text not null, 153 - followed_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 167 + foreign key (did) references profiles (did), 168 + foreign key (did, session_rkey) references study_sessions (did, rkey), 169 + unique (did, session_rkey) 170 + ); 154 171 155 - primary key (user_did, subject_did), 156 - check (user_did <> subject_did) 157 - ); 172 + create table if not exists study_session_reactions ( 173 + id integer primary key autoincrement, 158 174 159 - create table if not exists xp_events ( 160 - id integer primary key autoincrement, 175 + did text not null, 176 + rkey text not null, 161 177 162 - did text not null, 163 - session_rkey text not null, 164 - xp_gained integer not null, 165 - created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 178 + session_did text not null, 179 + session_rkey text not null, 180 + reaction_id integer not null, 181 + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 166 182 167 - foreign key (did) references profiles (did), 168 - foreign key (did, session_rkey) references study_sessions (did, rkey), 169 - unique (did, session_rkey) 170 - ); 183 + foreign key (did) references profiles (did), 184 + foreign key (session_did, session_rkey) references study_sessions (did, rkey), 185 + unique (did, session_did, session_rkey, reaction_id) 186 + ); 171 187 172 - create table if not exists study_session_reactions ( 173 - id integer primary key autoincrement, 188 + create table if not exists notifications ( 189 + id integer primary key autoincrement, 174 190 175 - did text not null, 176 - rkey text not null, 191 + recipient_did text not null, 192 + actor_did text not null, 193 + subject_uri text not null, 177 194 178 - session_did text not null, 179 - session_rkey text not null, 180 - reaction_id integer not null, 181 - created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 195 + state text not null default 'unread' check(state in ('unread', 'read')), 196 + type text not null check(type in ('follow', 'reaction')), 182 197 183 - foreign key (did) references profiles (did), 184 - foreign key (session_did, session_rkey) references study_sessions (did, rkey), 185 - unique (did, session_did, session_rkey, reaction_id) 186 - ); 198 + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 187 199 188 - create table if not exists notifications ( 189 - id integer primary key autoincrement, 200 + foreign key (recipient_did) references profiles(did) on delete cascade, 201 + foreign key (actor_did) references profiles(did) on delete cascade 202 + ); 190 203 191 - recipient_did text not null, 192 - actor_did text not null, 193 - subject_uri text not null, 204 + create table if not exists resources ( 205 + id integer primary key autoincrement, 194 206 195 - state text not null default 'unread' check(state in ('unread', 'read')), 196 - type text not null check(type in ('follow', 'reaction')), 207 + did text not null, 208 + rkey text not null, 197 209 198 - created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 210 + title text not null, 211 + type text not null, 212 + author text not null, 213 + link text, 214 + description text not null, 215 + status integer not null default 0 check(status in (0, 1)), 216 + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 199 217 200 - foreign key (recipient_did) references profiles(did) on delete cascade, 201 - foreign key (actor_did) references profiles(did) on delete cascade 202 - ); 218 + foreign key (did) references profiles (did) on delete cascade, 219 + unique (did, rkey) 220 + ); 203 221 204 - create table if not exists resources ( 205 - id integer primary key autoincrement, 222 + create table if not exists comments ( 223 + id integer primary key autoincrement, 206 224 207 - did text not null, 208 - rkey text not null, 225 + did text not null, 226 + rkey text not null, 209 227 210 - title text not null, 211 - type text not null, 212 - author text not null, 213 - link text, 214 - description text not null, 215 - status integer not null default 0 check(status in (0, 1)), 216 - created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 228 + study_session_uri text not null, 229 + parent_comment_uri text, 230 + body text not null, 231 + is_deleted boolean not null default false, 232 + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 217 233 218 - foreign key (did) references profiles (did) on delete cascade, 219 - unique (did, rkey) 220 - ); 234 + foreign key (did) references profiles(did) on delete cascade 235 + unique (did, rkey) 236 + ); 221 237 222 - create table if not exists _jetstream ( 223 - id integer primary key autoincrement, 224 - last_time_us integer not null 225 - ); 238 + create table if not exists _jetstream ( 239 + id integer primary key autoincrement, 240 + last_time_us integer not null 241 + ); 226 242 227 - create table if not exists migrations ( 228 - id integer primary key autoincrement, 229 - name text unique 230 - ); 231 - `) 243 + create table if not exists migrations ( 244 + id integer primary key autoincrement, 245 + name text unique 246 + ); 247 + `) 232 248 if err != nil { 233 249 return nil, fmt.Errorf("failed to execute db create statement: %w", err) 234 250 }
+1
internal/server/views/new-study-session.templ
··· 500 500 stopAndLog() { 501 501 this.pause(); 502 502 const form = this.$root; 503 + this.timerState = 'stopped'; 503 504 504 505 let durationSeconds = form.querySelector('input[name="duration_seconds"]'); 505 506 if (!durationSeconds) {
+50
lexicons/feed/comment.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.yoten.feed.comment", 4 + "needsCbor": true, 5 + "needsType": true, 6 + "defs": { 7 + "main": { 8 + "type": "record", 9 + "description": "A declaration of a Yōten comment.", 10 + "key": "tid", 11 + "record": { 12 + "type": "object", 13 + "required": ["subject", "body", "createdAt"], 14 + "properties": { 15 + "body": { 16 + "type": "string", 17 + "minLength": 1, 18 + "maxLength": 256 19 + }, 20 + "subject": { 21 + "type": "string", 22 + "format": "at-uri", 23 + "description": "A reference to the study session being commented on." 24 + }, 25 + "reply": { 26 + "type": "object", 27 + "description": "Indicates that this comment is a reply to another comment.", 28 + "required": ["root", "parent"], 29 + "properties": { 30 + "root": { 31 + "type": "string", 32 + "format": "at-uri", 33 + "description": "A reference to the original study session (the root of the conversation)." 34 + }, 35 + "parent": { 36 + "type": "string", 37 + "format": "at-uri", 38 + "description": "A reference to the specific comment being replied to." 39 + } 40 + } 41 + }, 42 + "createdAt": { 43 + "type": "string", 44 + "format": "datetime" 45 + } 46 + } 47 + } 48 + } 49 + } 50 + }