18 1081cf75 2024-11-04 o type Client struct {
21 1081cf75 2024-11-04 o // If true, HTTP request summaries are printed to standard error.
23 1081cf75 2024-11-04 o authToken string
24 1081cf75 2024-11-04 o instance *url.URL
29 1081cf75 2024-11-04 o type ListMode string
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"
37 1081cf75 2024-11-04 o var ErrNotFound error = errors.New("not found")
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"
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)
50 1081cf75 2024-11-04 o if c.Client == nil {
51 1081cf75 2024-11-04 o c.Client = http.DefaultClient
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{},
64 1081cf75 2024-11-04 o func (c *Client) Communities(mode ListMode) ([]Community, error) {
66 1081cf75 2024-11-04 o if err := c.init(); err != nil {
67 1081cf75 2024-11-04 o return nil, err
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
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")
80 1081cf75 2024-11-04 o params["auth"] = c.authToken
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
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))
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
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)
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)
103 1081cf75 2024-11-04 o return communities, nil
106 1081cf75 2024-11-04 o func (c *Client) LookupCommunity(name string) (Community, Counts, error) {
108 1081cf75 2024-11-04 o if err := c.init(); err != nil {
109 1081cf75 2024-11-04 o return Community{}, Counts{}, err
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
116 1081cf75 2024-11-04 o c.cache.delete(ent.post, ent.community)
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
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))
131 1081cf75 2024-11-04 o type response struct {
133 1081cf75 2024-11-04 o Community Community
135 1081cf75 2024-11-04 o } `json:"community_view"`
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)
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)
148 1081cf75 2024-11-04 o c.cache.store(Post{}, community, dur)
150 1081cf75 2024-11-04 o return community, cres.View.Counts, nil
153 1081cf75 2024-11-04 o func (c *Client) Posts(community string, mode ListMode) ([]Post, error) {
155 1081cf75 2024-11-04 o if err := c.init(); err != nil {
156 1081cf75 2024-11-04 o return nil, err
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",
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
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))
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)
180 1081cf75 2024-11-04 o var jresponse struct {
181 1081cf75 2024-11-04 o Posts []struct {
183 1081cf75 2024-11-04 o Creator Person
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)
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)
194 1081cf75 2024-11-04 o c.cache.store(post.Post, Community{}, ttl)
197 1081cf75 2024-11-04 o return posts, nil
200 1081cf75 2024-11-04 o func (c *Client) LookupPost(id int) (Post, error) {
202 1081cf75 2024-11-04 o if err := c.init(); err != nil {
203 1081cf75 2024-11-04 o return Post{}, err
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
210 1081cf75 2024-11-04 o c.cache.delete(ent.post, Community{})
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
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))
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)
231 1081cf75 2024-11-04 o c.cache.store(post, Community{}, dur)
233 1081cf75 2024-11-04 o return post, err
236 1081cf75 2024-11-04 o func (c *Client) Comments(post int, mode ListMode) ([]Comment, error) {
238 1081cf75 2024-11-04 o if err := c.init(); err != nil {
239 1081cf75 2024-11-04 o return nil, err
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",
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
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))
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
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)
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)
272 1081cf75 2024-11-04 o return comments, nil
275 1081cf75 2024-11-04 o func (c *Client) LookupComment(id int) (Comment, error) {
277 1081cf75 2024-11-04 o if err := c.init(); err != nil {
278 1081cf75 2024-11-04 o return Comment{}, err
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
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))
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"`
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)
301 1081cf75 2024-11-04 o return jresp.CommentView.Comment, nil
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")
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,
314 1081cf75 2024-11-04 o if parent > 0 {
315 1081cf75 2024-11-04 o params["parent_id"] = strconv.Itoa(parent)
317 1081cf75 2024-11-04 o resp, err := c.post("/comment", params)
318 1081cf75 2024-11-04 o if err != nil {
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))
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)
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)
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
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)
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)
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
357 1081cf75 2024-11-04 o req.Header.Set("Accept", "application/json")
359 1081cf75 2024-11-04 o fmt.Fprintf(os.Stderr, "%s %s\n", req.Method, req.URL)
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
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)
369 1081cf75 2024-11-04 o return resp, err
372 1081cf75 2024-11-04 o type jError struct {
373 1081cf75 2024-11-04 o Err string `json:"error"`
376 1081cf75 2024-11-04 o func (err jError) Error() string { return err.Err }
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)
386 1081cf75 2024-11-04 o type Counts struct {
389 1081cf75 2024-11-04 o CommunityID int `json:"community_id"`
390 1081cf75 2024-11-04 o PostID int `json:"post_id"`