Blame


1 1081cf75 2024-11-04 o package lemmy
2 1081cf75 2024-11-04 o
3 1081cf75 2024-11-04 o import (
4 1081cf75 2024-11-04 o "bytes"
5 1081cf75 2024-11-04 o "encoding/json"
6 1081cf75 2024-11-04 o "errors"
7 1081cf75 2024-11-04 o "fmt"
8 1081cf75 2024-11-04 o "io"
9 1081cf75 2024-11-04 o "net/http"
10 1081cf75 2024-11-04 o "net/url"
11 1081cf75 2024-11-04 o "os"
12 1081cf75 2024-11-04 o "path"
13 1081cf75 2024-11-04 o "strconv"
14 1081cf75 2024-11-04 o "sync"
15 1081cf75 2024-11-04 o "time"
16 1081cf75 2024-11-04 o )
17 1081cf75 2024-11-04 o
18 1081cf75 2024-11-04 o type Client struct {
19 1081cf75 2024-11-04 o *http.Client
20 1081cf75 2024-11-04 o Address string
21 1081cf75 2024-11-04 o // If true, HTTP request summaries are printed to standard error.
22 1081cf75 2024-11-04 o Debug bool
23 1081cf75 2024-11-04 o authToken string
24 1081cf75 2024-11-04 o instance *url.URL
25 1081cf75 2024-11-04 o cache *cache
26 1081cf75 2024-11-04 o ready bool
27 1081cf75 2024-11-04 o }
28 1081cf75 2024-11-04 o
29 1081cf75 2024-11-04 o type ListMode string
30 1081cf75 2024-11-04 o
31 1081cf75 2024-11-04 o const (
32 1081cf75 2024-11-04 o ListAll ListMode = "All"
33 1081cf75 2024-11-04 o ListLocal = "Local"
34 1081cf75 2024-11-04 o ListSubscribed = "Subscribed"
35 1081cf75 2024-11-04 o )
36 1081cf75 2024-11-04 o
37 1081cf75 2024-11-04 o var ErrNotFound error = errors.New("not found")
38 1081cf75 2024-11-04 o
39 1081cf75 2024-11-04 o func (c *Client) init() error {
40 1081cf75 2024-11-04 o if c.Address == "" {
41 1081cf75 2024-11-04 o c.Address = "127.0.0.1"
42 1081cf75 2024-11-04 o }
43 1081cf75 2024-11-04 o if c.instance == nil {
44 1081cf75 2024-11-04 o u, err := url.Parse("https://" + c.Address + "/api/v3/")
45 1081cf75 2024-11-04 o if err != nil {
46 1081cf75 2024-11-04 o return fmt.Errorf("initialise client: parse instance url: %w", err)
47 1081cf75 2024-11-04 o }
48 1081cf75 2024-11-04 o c.instance = u
49 1081cf75 2024-11-04 o }
50 1081cf75 2024-11-04 o if c.Client == nil {
51 1081cf75 2024-11-04 o c.Client = http.DefaultClient
52 1081cf75 2024-11-04 o }
53 1081cf75 2024-11-04 o if c.cache == nil {
54 1081cf75 2024-11-04 o c.cache = &cache{
55 1081cf75 2024-11-04 o post: make(map[int]entry),
56 1081cf75 2024-11-04 o community: make(map[string]entry),
57 1081cf75 2024-11-04 o mu: &sync.Mutex{},
58 1081cf75 2024-11-04 o }
59 1081cf75 2024-11-04 o }
60 1081cf75 2024-11-04 o c.ready = true
61 1081cf75 2024-11-04 o return nil
62 1081cf75 2024-11-04 o }
63 1081cf75 2024-11-04 o
64 1081cf75 2024-11-04 o func (c *Client) Communities(mode ListMode) ([]Community, error) {
65 1081cf75 2024-11-04 o if !c.ready {
66 1081cf75 2024-11-04 o if err := c.init(); err != nil {
67 1081cf75 2024-11-04 o return nil, err
68 1081cf75 2024-11-04 o }
69 1081cf75 2024-11-04 o }
70 1081cf75 2024-11-04 o
71 1081cf75 2024-11-04 o params := map[string]string{
72 1081cf75 2024-11-04 o "type_": string(mode),
73 1081cf75 2024-11-04 o "limit": "30", // TODO go through pages
74 1081cf75 2024-11-04 o "sort": "New",
75 1081cf75 2024-11-04 o }
76 1081cf75 2024-11-04 o if mode == ListSubscribed {
77 1081cf75 2024-11-04 o if c.authToken == "" {
78 1081cf75 2024-11-04 o return nil, errors.New("not logged in, no subscriptions")
79 1081cf75 2024-11-04 o }
80 1081cf75 2024-11-04 o params["auth"] = c.authToken
81 1081cf75 2024-11-04 o }
82 1081cf75 2024-11-04 o resp, err := c.get("community/list", params)
83 1081cf75 2024-11-04 o if err != nil {
84 1081cf75 2024-11-04 o return nil, err
85 1081cf75 2024-11-04 o }
86 1081cf75 2024-11-04 o defer resp.Body.Close()
87 1081cf75 2024-11-04 o if resp.StatusCode != http.StatusOK {
88 1081cf75 2024-11-04 o return nil, fmt.Errorf("remote status %s: %w", resp.Status, decodeError(resp.Body))
89 1081cf75 2024-11-04 o }
90 1081cf75 2024-11-04 o
91 1081cf75 2024-11-04 o var response struct {
92 1081cf75 2024-11-04 o Communities []struct {
93 1081cf75 2024-11-04 o Community Community
94 1081cf75 2024-11-04 o }
95 1081cf75 2024-11-04 o }
96 1081cf75 2024-11-04 o if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
97 1081cf75 2024-11-04 o return nil, fmt.Errorf("decode community response: %w", err)
98 1081cf75 2024-11-04 o }
99 1081cf75 2024-11-04 o var communities []Community
100 1081cf75 2024-11-04 o for _, c := range response.Communities {
101 1081cf75 2024-11-04 o communities = append(communities, c.Community)
102 1081cf75 2024-11-04 o }
103 1081cf75 2024-11-04 o return communities, nil
104 1081cf75 2024-11-04 o }
105 1081cf75 2024-11-04 o
106 1081cf75 2024-11-04 o func (c *Client) LookupCommunity(name string) (Community, Counts, error) {
107 1081cf75 2024-11-04 o if !c.ready {
108 1081cf75 2024-11-04 o if err := c.init(); err != nil {
109 1081cf75 2024-11-04 o return Community{}, Counts{}, err
110 1081cf75 2024-11-04 o }
111 1081cf75 2024-11-04 o }
112 1081cf75 2024-11-04 o if ent, ok := c.cache.community[name]; ok {
113 1081cf75 2024-11-04 o if time.Now().Before(ent.expiry) {
114 1081cf75 2024-11-04 o return ent.community, Counts{}, nil
115 1081cf75 2024-11-04 o }
116 1081cf75 2024-11-04 o c.cache.delete(ent.post, ent.community)
117 1081cf75 2024-11-04 o }
118 1081cf75 2024-11-04 o
119 1081cf75 2024-11-04 o params := map[string]string{"name": name}
120 1081cf75 2024-11-04 o resp, err := c.get("community", params)
121 1081cf75 2024-11-04 o if err != nil {
122 1081cf75 2024-11-04 o return Community{}, Counts{}, err
123 1081cf75 2024-11-04 o }
124 1081cf75 2024-11-04 o defer resp.Body.Close()
125 1081cf75 2024-11-04 o if resp.StatusCode == http.StatusNotFound {
126 1081cf75 2024-11-04 o return Community{}, Counts{}, ErrNotFound
127 1081cf75 2024-11-04 o } else if resp.StatusCode != http.StatusOK {
128 1081cf75 2024-11-04 o return Community{}, Counts{}, fmt.Errorf("remote status %s: %w", resp.Status, decodeError(resp.Body))
129 1081cf75 2024-11-04 o }
130 1081cf75 2024-11-04 o
131 1081cf75 2024-11-04 o type response struct {
132 1081cf75 2024-11-04 o View struct {
133 1081cf75 2024-11-04 o Community Community
134 1081cf75 2024-11-04 o Counts Counts
135 1081cf75 2024-11-04 o } `json:"community_view"`
136 1081cf75 2024-11-04 o }
137 1081cf75 2024-11-04 o var cres response
138 1081cf75 2024-11-04 o if err := json.NewDecoder(resp.Body).Decode(&cres); err != nil {
139 1081cf75 2024-11-04 o return Community{}, Counts{}, fmt.Errorf("decode community response: %w", err)
140 1081cf75 2024-11-04 o }
141 1081cf75 2024-11-04 o community := cres.View.Community
142 1081cf75 2024-11-04 o age := extractMaxAge(resp.Header)
143 1081cf75 2024-11-04 o if age != "" {
144 1081cf75 2024-11-04 o dur, err := parseMaxAge(age)
145 1081cf75 2024-11-04 o if err != nil {
146 1081cf75 2024-11-04 o return community, Counts{}, fmt.Errorf("parse cache max age from response header: %w", err)
147 1081cf75 2024-11-04 o }
148 1081cf75 2024-11-04 o c.cache.store(Post{}, community, dur)
149 1081cf75 2024-11-04 o }
150 1081cf75 2024-11-04 o return community, cres.View.Counts, nil
151 1081cf75 2024-11-04 o }
152 1081cf75 2024-11-04 o
153 1081cf75 2024-11-04 o func (c *Client) Posts(community string, mode ListMode) ([]Post, error) {
154 1081cf75 2024-11-04 o if !c.ready {
155 1081cf75 2024-11-04 o if err := c.init(); err != nil {
156 1081cf75 2024-11-04 o return nil, err
157 1081cf75 2024-11-04 o }
158 1081cf75 2024-11-04 o }
159 1081cf75 2024-11-04 o
160 1081cf75 2024-11-04 o params := map[string]string{
161 1081cf75 2024-11-04 o "community_name": community,
162 1081cf75 2024-11-04 o // "limit": "30",
163 1081cf75 2024-11-04 o "type_": string(mode),
164 1081cf75 2024-11-04 o "sort": "New",
165 1081cf75 2024-11-04 o }
166 1081cf75 2024-11-04 o resp, err := c.get("post/list", params)
167 1081cf75 2024-11-04 o if err != nil {
168 1081cf75 2024-11-04 o return nil, err
169 1081cf75 2024-11-04 o }
170 1081cf75 2024-11-04 o defer resp.Body.Close()
171 1081cf75 2024-11-04 o if resp.StatusCode != http.StatusOK {
172 1081cf75 2024-11-04 o return nil, fmt.Errorf("remote status %s: %w", resp.Status, decodeError(resp.Body))
173 1081cf75 2024-11-04 o }
174 1081cf75 2024-11-04 o age := extractMaxAge(resp.Header)
175 1081cf75 2024-11-04 o ttl, err := parseMaxAge(age)
176 1081cf75 2024-11-04 o if c.Debug && err != nil {
177 1081cf75 2024-11-04 o fmt.Fprintln(os.Stderr, "parse cache max-age from header:", err)
178 1081cf75 2024-11-04 o }
179 1081cf75 2024-11-04 o
180 1081cf75 2024-11-04 o var jresponse struct {
181 1081cf75 2024-11-04 o Posts []struct {
182 1081cf75 2024-11-04 o Post Post
183 1081cf75 2024-11-04 o Creator Person
184 1081cf75 2024-11-04 o }
185 1081cf75 2024-11-04 o }
186 1081cf75 2024-11-04 o if err := json.NewDecoder(resp.Body).Decode(&jresponse); err != nil {
187 1081cf75 2024-11-04 o return nil, fmt.Errorf("decode posts response: %w", err)
188 1081cf75 2024-11-04 o }
189 1081cf75 2024-11-04 o var posts []Post
190 1081cf75 2024-11-04 o for _, post := range jresponse.Posts {
191 1081cf75 2024-11-04 o post.Post.Creator = post.Creator
192 1081cf75 2024-11-04 o posts = append(posts, post.Post)
193 1081cf75 2024-11-04 o if ttl > 0 {
194 1081cf75 2024-11-04 o c.cache.store(post.Post, Community{}, ttl)
195 1081cf75 2024-11-04 o }
196 1081cf75 2024-11-04 o }
197 1081cf75 2024-11-04 o return posts, nil
198 1081cf75 2024-11-04 o }
199 1081cf75 2024-11-04 o
200 1081cf75 2024-11-04 o func (c *Client) LookupPost(id int) (Post, error) {
201 1081cf75 2024-11-04 o if !c.ready {
202 1081cf75 2024-11-04 o if err := c.init(); err != nil {
203 1081cf75 2024-11-04 o return Post{}, err
204 1081cf75 2024-11-04 o }
205 1081cf75 2024-11-04 o }
206 1081cf75 2024-11-04 o if ent, ok := c.cache.post[id]; ok {
207 1081cf75 2024-11-04 o if time.Now().Before(ent.expiry) {
208 1081cf75 2024-11-04 o return ent.post, nil
209 1081cf75 2024-11-04 o }
210 1081cf75 2024-11-04 o c.cache.delete(ent.post, Community{})
211 1081cf75 2024-11-04 o }
212 1081cf75 2024-11-04 o
213 1081cf75 2024-11-04 o params := map[string]string{"id": strconv.Itoa(id)}
214 1081cf75 2024-11-04 o resp, err := c.get("post", params)
215 1081cf75 2024-11-04 o if err != nil {
216 1081cf75 2024-11-04 o return Post{}, err
217 1081cf75 2024-11-04 o }
218 1081cf75 2024-11-04 o defer resp.Body.Close()
219 1081cf75 2024-11-04 o if resp.StatusCode == http.StatusNotFound {
220 1081cf75 2024-11-04 o return Post{}, ErrNotFound
221 1081cf75 2024-11-04 o } else if resp.StatusCode != http.StatusOK {
222 1081cf75 2024-11-04 o return Post{}, fmt.Errorf("remote status %s: %w", resp.Status, decodeError(resp.Body))
223 1081cf75 2024-11-04 o }
224 1081cf75 2024-11-04 o post, _, _, err := decodePostResponse(resp.Body)
225 1081cf75 2024-11-04 o age := extractMaxAge(resp.Header)
226 1081cf75 2024-11-04 o if age != "" {
227 1081cf75 2024-11-04 o dur, err := parseMaxAge(age)
228 1081cf75 2024-11-04 o if err != nil {
229 1081cf75 2024-11-04 o return post, fmt.Errorf("parse cache max age from response header: %w", err)
230 1081cf75 2024-11-04 o }
231 1081cf75 2024-11-04 o c.cache.store(post, Community{}, dur)
232 1081cf75 2024-11-04 o }
233 1081cf75 2024-11-04 o return post, err
234 1081cf75 2024-11-04 o }
235 1081cf75 2024-11-04 o
236 1081cf75 2024-11-04 o func (c *Client) Comments(post int, mode ListMode) ([]Comment, error) {
237 1081cf75 2024-11-04 o if !c.ready {
238 1081cf75 2024-11-04 o if err := c.init(); err != nil {
239 1081cf75 2024-11-04 o return nil, err
240 1081cf75 2024-11-04 o }
241 1081cf75 2024-11-04 o }
242 1081cf75 2024-11-04 o
243 1081cf75 2024-11-04 o params := map[string]string{
244 1081cf75 2024-11-04 o "post_id": strconv.Itoa(post),
245 1081cf75 2024-11-04 o "type_": string(mode),
246 1081cf75 2024-11-04 o "limit": "30",
247 1081cf75 2024-11-04 o "sort": "New",
248 1081cf75 2024-11-04 o }
249 1081cf75 2024-11-04 o resp, err := c.get("comment/list", params)
250 1081cf75 2024-11-04 o if err != nil {
251 1081cf75 2024-11-04 o return nil, err
252 1081cf75 2024-11-04 o }
253 1081cf75 2024-11-04 o defer resp.Body.Close()
254 1081cf75 2024-11-04 o if resp.StatusCode != http.StatusOK {
255 1081cf75 2024-11-04 o return nil, fmt.Errorf("remote status %s: %w", resp.Status, decodeError(resp.Body))
256 1081cf75 2024-11-04 o }
257 1081cf75 2024-11-04 o
258 1081cf75 2024-11-04 o var jresponse struct {
259 1081cf75 2024-11-04 o Comments []struct {
260 1081cf75 2024-11-04 o Comment Comment
261 1081cf75 2024-11-04 o Creator Person
262 1081cf75 2024-11-04 o }
263 1081cf75 2024-11-04 o }
264 1081cf75 2024-11-04 o if err := json.NewDecoder(resp.Body).Decode(&jresponse); err != nil {
265 1081cf75 2024-11-04 o return nil, fmt.Errorf("decode comments: %w", err)
266 1081cf75 2024-11-04 o }
267 1081cf75 2024-11-04 o var comments []Comment
268 1081cf75 2024-11-04 o for _, comment := range jresponse.Comments {
269 1081cf75 2024-11-04 o comment.Comment.Creator = comment.Creator
270 1081cf75 2024-11-04 o comments = append(comments, comment.Comment)
271 1081cf75 2024-11-04 o }
272 1081cf75 2024-11-04 o return comments, nil
273 1081cf75 2024-11-04 o }
274 1081cf75 2024-11-04 o
275 1081cf75 2024-11-04 o func (c *Client) LookupComment(id int) (Comment, error) {
276 1081cf75 2024-11-04 o if !c.ready {
277 1081cf75 2024-11-04 o if err := c.init(); err != nil {
278 1081cf75 2024-11-04 o return Comment{}, err
279 1081cf75 2024-11-04 o }
280 1081cf75 2024-11-04 o }
281 1081cf75 2024-11-04 o
282 1081cf75 2024-11-04 o params := map[string]string{"id": strconv.Itoa(id)}
283 1081cf75 2024-11-04 o resp, err := c.get("comment", params)
284 1081cf75 2024-11-04 o if err != nil {
285 1081cf75 2024-11-04 o return Comment{}, err
286 1081cf75 2024-11-04 o }
287 1081cf75 2024-11-04 o defer resp.Body.Close()
288 1081cf75 2024-11-04 o if resp.StatusCode != http.StatusOK {
289 1081cf75 2024-11-04 o return Comment{}, fmt.Errorf("remote status %s: %w", resp.Status, decodeError(resp.Body))
290 1081cf75 2024-11-04 o }
291 1081cf75 2024-11-04 o
292 1081cf75 2024-11-04 o type jresponse struct {
293 1081cf75 2024-11-04 o CommentView struct {
294 1081cf75 2024-11-04 o Comment Comment
295 1081cf75 2024-11-04 o } `json:"comment_view"`
296 1081cf75 2024-11-04 o }
297 1081cf75 2024-11-04 o var jresp jresponse
298 1081cf75 2024-11-04 o if err := json.NewDecoder(resp.Body).Decode(&jresp); err != nil {
299 1081cf75 2024-11-04 o return Comment{}, fmt.Errorf("decode comment: %w", err)
300 1081cf75 2024-11-04 o }
301 1081cf75 2024-11-04 o return jresp.CommentView.Comment, nil
302 1081cf75 2024-11-04 o }
303 1081cf75 2024-11-04 o
304 1081cf75 2024-11-04 o func (c *Client) Reply(post int, parent int, msg string) error {
305 1081cf75 2024-11-04 o if c.authToken == "" {
306 1081cf75 2024-11-04 o return errors.New("not logged in")
307 1081cf75 2024-11-04 o }
308 1081cf75 2024-11-04 o
309 1081cf75 2024-11-04 o params := map[string]interface{}{
310 1081cf75 2024-11-04 o "post_id": post,
311 1081cf75 2024-11-04 o "content": msg,
312 1081cf75 2024-11-04 o "auth": c.authToken,
313 1081cf75 2024-11-04 o }
314 1081cf75 2024-11-04 o if parent > 0 {
315 1081cf75 2024-11-04 o params["parent_id"] = strconv.Itoa(parent)
316 1081cf75 2024-11-04 o }
317 1081cf75 2024-11-04 o resp, err := c.post("/comment", params)
318 1081cf75 2024-11-04 o if err != nil {
319 1081cf75 2024-11-04 o return err
320 1081cf75 2024-11-04 o }
321 1081cf75 2024-11-04 o defer resp.Body.Close()
322 1081cf75 2024-11-04 o if resp.StatusCode != http.StatusOK {
323 1081cf75 2024-11-04 o return fmt.Errorf("remote status %s: %w", resp.Status, decodeError(resp.Body))
324 1081cf75 2024-11-04 o }
325 1081cf75 2024-11-04 o return nil
326 1081cf75 2024-11-04 o }
327 1081cf75 2024-11-04 o
328 1081cf75 2024-11-04 o func (c *Client) post(pathname string, params map[string]interface{}) (*http.Response, error) {
329 1081cf75 2024-11-04 o u := *c.instance
330 1081cf75 2024-11-04 o u.Path = path.Join(u.Path, pathname)
331 1081cf75 2024-11-04 o
332 1081cf75 2024-11-04 o b, err := json.Marshal(params)
333 1081cf75 2024-11-04 o if err != nil {
334 1081cf75 2024-11-04 o return nil, fmt.Errorf("encode body: %w", err)
335 1081cf75 2024-11-04 o }
336 1081cf75 2024-11-04 o req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(b))
337 1081cf75 2024-11-04 o if err != nil {
338 1081cf75 2024-11-04 o return nil, err
339 1081cf75 2024-11-04 o }
340 1081cf75 2024-11-04 o req.Header.Set("Content-Type", "application/json")
341 1081cf75 2024-11-04 o req.Header.Set("Accept", "application/json")
342 1081cf75 2024-11-04 o return c.Do(req)
343 1081cf75 2024-11-04 o }
344 1081cf75 2024-11-04 o
345 1081cf75 2024-11-04 o func (c *Client) get(pathname string, params map[string]string) (*http.Response, error) {
346 1081cf75 2024-11-04 o u := *c.instance
347 1081cf75 2024-11-04 o u.Path = path.Join(u.Path, pathname)
348 1081cf75 2024-11-04 o vals := make(url.Values)
349 1081cf75 2024-11-04 o for k, v := range params {
350 1081cf75 2024-11-04 o vals.Set(k, v)
351 1081cf75 2024-11-04 o }
352 1081cf75 2024-11-04 o u.RawQuery = vals.Encode()
353 1081cf75 2024-11-04 o req, err := http.NewRequest(http.MethodGet, u.String(), nil)
354 1081cf75 2024-11-04 o if err != nil {
355 1081cf75 2024-11-04 o return nil, err
356 1081cf75 2024-11-04 o }
357 1081cf75 2024-11-04 o req.Header.Set("Accept", "application/json")
358 1081cf75 2024-11-04 o if c.Debug {
359 1081cf75 2024-11-04 o fmt.Fprintf(os.Stderr, "%s %s\n", req.Method, req.URL)
360 1081cf75 2024-11-04 o }
361 1081cf75 2024-11-04 o resp, err := c.Do(req)
362 1081cf75 2024-11-04 o if err != nil {
363 1081cf75 2024-11-04 o return resp, err
364 1081cf75 2024-11-04 o }
365 1081cf75 2024-11-04 o if resp.StatusCode == http.StatusServiceUnavailable {
366 1081cf75 2024-11-04 o time.Sleep(2 * time.Second)
367 1081cf75 2024-11-04 o resp, err = c.get(pathname, params)
368 1081cf75 2024-11-04 o }
369 1081cf75 2024-11-04 o return resp, err
370 1081cf75 2024-11-04 o }
371 1081cf75 2024-11-04 o
372 1081cf75 2024-11-04 o type jError struct {
373 1081cf75 2024-11-04 o Err string `json:"error"`
374 1081cf75 2024-11-04 o }
375 1081cf75 2024-11-04 o
376 1081cf75 2024-11-04 o func (err jError) Error() string { return err.Err }
377 1081cf75 2024-11-04 o
378 1081cf75 2024-11-04 o func decodeError(r io.Reader) error {
379 1081cf75 2024-11-04 o var jerr jError
380 1081cf75 2024-11-04 o if err := json.NewDecoder(r).Decode(&jerr); err != nil {
381 1081cf75 2024-11-04 o return fmt.Errorf("decode error message: %v", err)
382 1081cf75 2024-11-04 o }
383 1081cf75 2024-11-04 o return jerr
384 1081cf75 2024-11-04 o }
385 1081cf75 2024-11-04 o
386 1081cf75 2024-11-04 o type Counts struct {
387 1081cf75 2024-11-04 o Posts int
388 1081cf75 2024-11-04 o Comments int
389 1081cf75 2024-11-04 o CommunityID int `json:"community_id"`
390 1081cf75 2024-11-04 o PostID int `json:"post_id"`
391 1081cf75 2024-11-04 o }