bluesky viewer in the terminal
at main 6.3 kB view raw
1package export 2 3import ( 4 "encoding/csv" 5 "encoding/json" 6 "fmt" 7 "os" 8 "strings" 9 "time" 10 11 "github.com/stormlightlabs/skypanel/cli/internal/store" 12) 13 14// ExportPost represents a post structure for export operations 15type ExportPost struct { 16 ID string `json:"id"` 17 URI string `json:"uri"` 18 AuthorDID string `json:"author_did"` 19 Text string `json:"text"` 20 FeedID string `json:"feed_id"` 21 IndexedAt time.Time `json:"indexed_at"` 22 CreatedAt time.Time `json:"created_at"` 23} 24 25// ToJSON exports posts to JSON format with pretty printing 26func ToJSON(filename string, posts []*store.PostModel) error { 27 file, err := os.Create(filename) 28 if err != nil { 29 return fmt.Errorf("failed to create file: %w", err) 30 } 31 defer file.Close() 32 33 encoder := json.NewEncoder(file) 34 encoder.SetIndent("", " ") 35 36 exportPosts := convertPosts(posts) 37 if err := encoder.Encode(exportPosts); err != nil { 38 return fmt.Errorf("failed to encode JSON: %w", err) 39 } 40 41 return nil 42} 43 44// ToCSV exports posts to CSV format with headers 45func ToCSV(filename string, posts []*store.PostModel) error { 46 file, err := os.Create(filename) 47 if err != nil { 48 return fmt.Errorf("failed to create file: %w", err) 49 } 50 defer file.Close() 51 52 writer := csv.NewWriter(file) 53 defer writer.Flush() 54 55 // Write header 56 if err := writer.Write([]string{"ID", "URI", "AuthorDID", "Text", "FeedID", "IndexedAt", "CreatedAt"}); err != nil { 57 return fmt.Errorf("failed to write CSV header: %w", err) 58 } 59 60 // Write rows 61 for _, post := range posts { 62 record := []string{ 63 post.ID(), 64 post.URI, 65 post.AuthorDID, 66 post.Text, 67 post.FeedID, 68 post.IndexedAt.Format(time.RFC3339), 69 post.CreatedAt().Format(time.RFC3339), 70 } 71 if err := writer.Write(record); err != nil { 72 return fmt.Errorf("failed to write CSV record: %w", err) 73 } 74 } 75 76 return nil 77} 78 79// ToTXT exports posts to plain text format with readable formatting 80func ToTXT(filename string, posts []*store.PostModel) error { 81 file, err := os.Create(filename) 82 if err != nil { 83 return fmt.Errorf("failed to create file: %w", err) 84 } 85 defer file.Close() 86 87 for i, post := range posts { 88 fmt.Fprintf(file, "Post #%d\n", i+1) 89 fmt.Fprintf(file, "ID: %s\n", post.ID()) 90 fmt.Fprintf(file, "URI: %s\n", post.URI) 91 fmt.Fprintf(file, "Author DID: %s\n", post.AuthorDID) 92 fmt.Fprintf(file, "Feed ID: %s\n", post.FeedID) 93 fmt.Fprintf(file, "Indexed At: %s\n", post.IndexedAt.Format(time.RFC3339)) 94 fmt.Fprintf(file, "Created At: %s\n", post.CreatedAt().Format(time.RFC3339)) 95 fmt.Fprintf(file, "\nText:\n%s\n", post.Text) 96 fmt.Fprintf(file, "\n%s\n\n", strings.Repeat("-", 80)) 97 } 98 99 return nil 100} 101 102// convertPosts transforms PostModel slice to ExportPost slice 103func convertPosts(posts []*store.PostModel) []ExportPost { 104 exportPosts := make([]ExportPost, len(posts)) 105 for i, post := range posts { 106 exportPosts[i] = ExportPost{ 107 ID: post.ID(), 108 URI: post.URI, 109 AuthorDID: post.AuthorDID, 110 Text: post.Text, 111 FeedID: post.FeedID, 112 IndexedAt: post.IndexedAt, 113 CreatedAt: post.CreatedAt(), 114 } 115 } 116 return exportPosts 117} 118 119// ProfileToJSON exports an ActorProfile to JSON format 120func ProfileToJSON(filename string, profile *store.ActorProfile) error { 121 file, err := os.Create(filename) 122 if err != nil { 123 return fmt.Errorf("failed to create file: %w", err) 124 } 125 defer file.Close() 126 127 encoder := json.NewEncoder(file) 128 encoder.SetIndent("", " ") 129 130 if err := encoder.Encode(profile); err != nil { 131 return fmt.Errorf("failed to encode JSON: %w", err) 132 } 133 134 return nil 135} 136 137// ProfileToTXT exports an ActorProfile to plain text format 138func ProfileToTXT(filename string, profile *store.ActorProfile) error { 139 file, err := os.Create(filename) 140 if err != nil { 141 return fmt.Errorf("failed to create file: %w", err) 142 } 143 defer file.Close() 144 145 fmt.Fprintf(file, "Profile: @%s\n", profile.Handle) 146 fmt.Fprintf(file, "%s\n\n", strings.Repeat("=", 80)) 147 148 if profile.DisplayName != "" { 149 fmt.Fprintf(file, "Display Name: %s\n", profile.DisplayName) 150 } 151 fmt.Fprintf(file, "DID: %s\n", profile.Did) 152 fmt.Fprintf(file, "Handle: @%s\n", profile.Handle) 153 154 if profile.Description != "" { 155 fmt.Fprintf(file, "\nDescription:\n%s\n", profile.Description) 156 } 157 158 fmt.Fprintf(file, "\nStats:\n") 159 fmt.Fprintf(file, " Followers: %d\n", profile.FollowersCount) 160 fmt.Fprintf(file, " Following: %d\n", profile.FollowsCount) 161 fmt.Fprintf(file, " Posts: %d\n", profile.PostsCount) 162 163 if profile.CreatedAt != "" { 164 fmt.Fprintf(file, "\nCreated: %s\n", profile.CreatedAt) 165 } 166 167 return nil 168} 169 170// FeedViewPostToJSON exports a single FeedViewPost to JSON format 171func FeedViewPostToJSON(filename string, post *store.FeedViewPost) error { 172 file, err := os.Create(filename) 173 if err != nil { 174 return fmt.Errorf("failed to create file: %w", err) 175 } 176 defer file.Close() 177 178 encoder := json.NewEncoder(file) 179 encoder.SetIndent("", " ") 180 181 if err := encoder.Encode(post); err != nil { 182 return fmt.Errorf("failed to encode JSON: %w", err) 183 } 184 185 return nil 186} 187 188// FeedViewPostToTXT exports a single FeedViewPost to plain text format 189func FeedViewPostToTXT(filename string, post *store.FeedViewPost) error { 190 file, err := os.Create(filename) 191 if err != nil { 192 return fmt.Errorf("failed to create file: %w", err) 193 } 194 defer file.Close() 195 196 if post.Post != nil { 197 p := post.Post 198 fmt.Fprintf(file, "Post by @%s\n", p.Author.Handle) 199 fmt.Fprintf(file, "%s\n\n", strings.Repeat("=", 80)) 200 201 fmt.Fprintf(file, "URI: %s\n", p.Uri) 202 fmt.Fprintf(file, "CID: %s\n", p.Cid) 203 204 if p.Author.DisplayName != "" { 205 fmt.Fprintf(file, "Author: %s (@%s)\n", p.Author.DisplayName, p.Author.Handle) 206 } else { 207 fmt.Fprintf(file, "Author: @%s\n", p.Author.Handle) 208 } 209 210 // Extract text from record 211 if recordMap, ok := p.Record.(map[string]any); ok { 212 if text, ok := recordMap["text"].(string); ok { 213 fmt.Fprintf(file, "\nText:\n%s\n", text) 214 } 215 } 216 217 fmt.Fprintf(file, "\nEngagement:\n") 218 fmt.Fprintf(file, " Likes: %d\n", p.LikeCount) 219 fmt.Fprintf(file, " Reposts: %d\n", p.RepostCount) 220 fmt.Fprintf(file, " Replies: %d\n", p.ReplyCount) 221 fmt.Fprintf(file, " Quotes: %d\n", p.QuoteCount) 222 223 fmt.Fprintf(file, "\nIndexed: %s\n", p.IndexedAt) 224 225 if post.Reason != nil && post.Reason.By != nil { 226 fmt.Fprintf(file, "\nReposted by: @%s\n", post.Reason.By.Handle) 227 } 228 } 229 230 return nil 231}