Monorepo for Tangled
tangled.org
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 Gzip *String_GzipInfo
45}
46type String_GzipInfo struct {
47 tangled.String_File_Gzip
48 // Optional uncompressed content.
49 // Populated when the content is first requested.
50 Content string
51}
52
53func (s *String) AtUri() syntax.ATURI {
54 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", s.Did, tangled.StringNSID, s.Rkey))
55}
56
57func (s *String) AsRecord() *tangled.String {
58 var files []*tangled.String_File
59 for _, f := range s.Files {
60 var gzip *tangled.String_File_Gzip
61 if f.Gzip != nil {
62 gzip = &tangled.String_File_Gzip{
63 RealSize: f.Gzip.RealSize,
64 RealMime: f.Gzip.RealMime,
65 }
66 }
67 files = append(files, &tangled.String_File{
68 Name: f.Name,
69 Content: &f.Content,
70 Gzip: gzip,
71 })
72 }
73 return &tangled.String{
74 Title: s.Title,
75 Description: s.Description,
76 Files: files,
77 CreatedAt: s.Created.Format(time.RFC3339),
78 }
79}
80
81func (s *String) Validate() error {
82 var err error
83 if s.FileName == "" && len(s.Files) == 0 {
84 err = errors.Join(err, fmt.Errorf("string should have more than one files"))
85 }
86 // legacy record check
87 if utf8.RuneCountInString(s.FileName) > 140 {
88 err = errors.Join(err, fmt.Errorf("filename too long"))
89 }
90 for i, file := range s.Files {
91 if utf8.RuneCountInString(file.Name) > 140 {
92 err = errors.Join(err, fmt.Errorf("filename too long at files[%d]", i))
93 }
94 }
95 if s.Title != nil {
96 if utf8.RuneCountInString(*s.Title) > 140 {
97 err = errors.Join(err, fmt.Errorf("title too long"))
98 }
99 }
100 if s.Description != nil {
101 if utf8.RuneCountInString(*s.Description) > 280 {
102 err = errors.Join(err, fmt.Errorf("description too long"))
103 }
104 }
105 return err
106}
107
108func (s String) RenderTitle() string {
109 if s.Title != nil {
110 return *s.Title
111 }
112 if len(s.Files) > 0 {
113 return s.Files[0].Name
114 }
115 return s.FileName
116}
117
118// FileByName returns first item in files with given filename
119func (s *String) FileByName(name string) (String_File, bool) {
120 for _, file := range s.Files {
121 if file.Name == name {
122 return file, true
123 }
124 }
125 return String_File{}, false
126}
127
128func (s String) IsLegacySingleFile() bool {
129 return len(s.Files) == 0
130}
131
132// StringFromRecord creates [String] from [tangled.String].
133// NOTE: This won't prefetch blobs
134func StringFromRecord(did syntax.DID, rkey syntax.RecordKey, cid syntax.CID, record tangled.String) (String, error) {
135 created, err := time.Parse(time.RFC3339, record.CreatedAt)
136 if err != nil {
137 return String{}, fmt.Errorf("invalid createdAt: %w", err)
138 }
139 var files []String_File
140 for _, f := range record.Files {
141 var gzip *String_GzipInfo
142 if f.Gzip != nil {
143 gzip = &String_GzipInfo{String_File_Gzip: *f.Gzip}
144 }
145 files = append(files, String_File{
146 Name: f.Name,
147 Content: *f.Content,
148 Gzip: gzip,
149 })
150 }
151 return String{
152 Did: did,
153 Rkey: rkey,
154 Cid: &cid,
155 Title: record.Title,
156 Description: record.Description,
157 Files: files,
158 Created: created,
159 FileName: stringPtr(record.Filename),
160 FileContent: stringPtr(record.Contents),
161 }, nil
162}
163
164type StringStats struct {
165 StarCount int
166 // CommentCount int
167}
168
169type StringFileStats struct {
170 LineCount int
171 ByteCount int
172}
173
174func stringPtr(s *string) string {
175 if s == nil {
176 return ""
177 }
178 return *s
179}