Monorepo for Tangled tangled.org
5

Configure Feed

Select the types of activity you want to include in your feed.

1package models 2 3import ( 4 "errors" 5 "fmt" 6 "time" 7 "unicode/utf8" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 lexutil "github.com/bluesky-social/indigo/lex/util" 11 "tangled.org/core/api/tangled" 12) 13 14type String struct { 15 Did syntax.DID 16 Rkey syntax.RecordKey 17 Cid *syntax.CID 18 19 // String_File will remain, and we will still use it. 20 // We just use `FileContent` when fetching the file. 21 22 // after that, change lexicon, start migrating to blobs 23 // when string is migrated, clear FileName and FileContent. 24 // when they are cleared, fetch the blob on page load. 25 26 Title *string 27 Description *string 28 Files []String_File 29 Created time.Time 30 Edited *time.Time 31 32 // legacy string data 33 FileName string 34 FileContent string 35 36 // optionally, populate this when querying for reverse mappings 37 Stats *StringStats 38} 39 40// String_File is [tangled.String_File] with optional prefetched & decompressed text blob content 41type String_File struct { 42 Name string 43 Content lexutil.LexBlob 44} 45 46func (s *String) AtUri() syntax.ATURI { 47 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", s.Did, tangled.StringNSID, s.Rkey)) 48} 49 50func (s *String) AsRecord() *tangled.String { 51 var files []*tangled.String_File 52 for _, f := range s.Files { 53 files = append(files, &tangled.String_File{ 54 Name: f.Name, 55 Content: &f.Content, 56 }) 57 } 58 return &tangled.String{ 59 Title: s.Title, 60 Description: s.Description, 61 Files: files, 62 CreatedAt: s.Created.Format(time.RFC3339), 63 } 64} 65 66func (s *String) Validate() error { 67 var err error 68 if s.FileName == "" && len(s.Files) == 0 { 69 err = errors.Join(err, fmt.Errorf("string should have more than one files")) 70 } 71 // legacy record check 72 if utf8.RuneCountInString(s.FileName) > 140 { 73 err = errors.Join(err, fmt.Errorf("filename too long")) 74 } 75 for i, file := range s.Files { 76 if utf8.RuneCountInString(file.Name) > 140 { 77 err = errors.Join(err, fmt.Errorf("filename too long at files[%d]", i)) 78 } 79 } 80 if s.Title != nil { 81 if utf8.RuneCountInString(*s.Title) > 140 { 82 err = errors.Join(err, fmt.Errorf("title too long")) 83 } 84 } 85 if s.Description != nil { 86 if utf8.RuneCountInString(*s.Description) > 280 { 87 err = errors.Join(err, fmt.Errorf("description too long")) 88 } 89 } 90 return err 91} 92 93func (s String) RenderTitle() string { 94 if s.Title != nil { 95 return *s.Title 96 } 97 if len(s.Files) > 0 { 98 return s.Files[0].Name 99 } 100 return s.FileName 101} 102 103// FileByName returns first item in files with given filename 104func (s *String) FileByName(name string) (String_File, bool) { 105 for _, file := range s.Files { 106 if file.Name == name { 107 return file, true 108 } 109 } 110 return String_File{}, false 111} 112 113func (s String) IsLegacySingleFile() bool { 114 return len(s.Files) == 0 115} 116 117// StringFromRecord creates [String] from [tangled.String]. 118// NOTE: This won't prefetch blobs 119func StringFromRecord(did syntax.DID, rkey syntax.RecordKey, cid syntax.CID, record tangled.String) (String, error) { 120 created, err := time.Parse(time.RFC3339, record.CreatedAt) 121 if err != nil { 122 return String{}, fmt.Errorf("invalid createdAt: %w", err) 123 } 124 var files []String_File 125 for _, f := range record.Files { 126 files = append(files, String_File{ 127 Name: f.Name, 128 Content: *f.Content, 129 }) 130 } 131 return String{ 132 Did: did, 133 Rkey: rkey, 134 Cid: &cid, 135 Title: record.Title, 136 Description: record.Description, 137 Files: files, 138 Created: created, 139 FileName: stringPtr(record.Filename), 140 FileContent: stringPtr(record.Contents), 141 }, nil 142} 143 144type StringStats struct { 145 StarCount int 146 // CommentCount int 147} 148 149type StringFileStats struct { 150 LineCount int 151 ByteCount int 152} 153 154func stringPtr(s *string) string { 155 if s == nil { 156 return "" 157 } 158 return *s 159}