bluesky viewer in the terminal
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}