package models import ( "errors" "fmt" "time" "unicode/utf8" "github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util" "tangled.org/core/api/tangled" ) type String struct { Did syntax.DID Rkey syntax.RecordKey Cid *syntax.CID // String_File will remain, and we will still use it. // We just use `FileContent` when fetching the file. // after that, change lexicon, start migrating to blobs // when string is migrated, clear FileName and FileContent. // when they are cleared, fetch the blob on page load. Title *string Description *string Files []String_File Created time.Time Edited *time.Time // legacy string data FileName string FileContent string // optionally, populate this when querying for reverse mappings Stats *StringStats } // String_File is [tangled.String_File] with optional prefetched & decompressed text blob content type String_File struct { Name string Content lexutil.LexBlob } func (s *String) AtUri() syntax.ATURI { return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", s.Did, tangled.StringNSID, s.Rkey)) } func (s *String) AsRecord() *tangled.String { var files []*tangled.String_File for _, f := range s.Files { files = append(files, &tangled.String_File{ Name: f.Name, Content: &f.Content, }) } return &tangled.String{ Title: s.Title, Description: s.Description, Files: files, CreatedAt: s.Created.Format(time.RFC3339), } } func (s *String) Validate() error { var err error if s.FileName == "" && len(s.Files) == 0 { err = errors.Join(err, fmt.Errorf("string should have more than one files")) } // legacy record check if utf8.RuneCountInString(s.FileName) > 140 { err = errors.Join(err, fmt.Errorf("filename too long")) } for i, file := range s.Files { if utf8.RuneCountInString(file.Name) > 140 { err = errors.Join(err, fmt.Errorf("filename too long at files[%d]", i)) } } if s.Title != nil { if utf8.RuneCountInString(*s.Title) > 140 { err = errors.Join(err, fmt.Errorf("title too long")) } } if s.Description != nil { if utf8.RuneCountInString(*s.Description) > 280 { err = errors.Join(err, fmt.Errorf("description too long")) } } return err } func (s String) RenderTitle() string { if s.Title != nil { return *s.Title } if len(s.Files) > 0 { return s.Files[0].Name } return s.FileName } // FileByName returns first item in files with given filename func (s *String) FileByName(name string) (String_File, bool) { for _, file := range s.Files { if file.Name == name { return file, true } } return String_File{}, false } func (s String) IsLegacySingleFile() bool { return len(s.Files) == 0 } // StringFromRecord creates [String] from [tangled.String]. // NOTE: This won't prefetch blobs func StringFromRecord(did syntax.DID, rkey syntax.RecordKey, cid syntax.CID, record tangled.String) (String, error) { created, err := time.Parse(time.RFC3339, record.CreatedAt) if err != nil { return String{}, fmt.Errorf("invalid createdAt: %w", err) } var files []String_File for _, f := range record.Files { files = append(files, String_File{ Name: f.Name, Content: *f.Content, }) } return String{ Did: did, Rkey: rkey, Cid: &cid, Title: record.Title, Description: record.Description, Files: files, Created: created, FileName: stringPtr(record.Filename), FileContent: stringPtr(record.Contents), }, nil } type StringStats struct { StarCount int // CommentCount int } type StringFileStats struct { LineCount int ByteCount int } func stringPtr(s *string) string { if s == nil { return "" } return *s }