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}
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}