package client import ( "encoding/json" "fmt" "io/ioutil" "log" "net/http" "net/http/httputil" "net/url" "strings" "p83.nl/go/ekster/pkg/microsub" "p83.nl/go/ekster/pkg/sse" ) // Client is a HTTP client for Microsub type Client struct { Me *url.URL MicrosubEndpoint *url.URL Token string Logging bool } func (c *Client) microsubGetRequest(action string, args map[string]string) (*http.Response, error) { client := http.Client{} u := *c.MicrosubEndpoint q := u.Query() q.Add("action", action) for k, v := range args { q.Add(k, v) } u.RawQuery = q.Encode() req, err := http.NewRequest(http.MethodGet, u.String(), nil) if err != nil { return nil, err } req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.Token)) if c.Logging { x, _ := httputil.DumpRequestOut(req, true) log.Printf("REQUEST:\n\n%s\n\n", x) } res, err := client.Do(req) if c.Logging { x, _ := httputil.DumpResponse(res, true) log.Printf("RESPONSE:\n\n%s\n\n", x) } return res, err } func (c *Client) microsubPostRequest(action string, args map[string]string) (*http.Response, error) { client := http.Client{} u := *c.MicrosubEndpoint q := u.Query() q.Add("action", action) for k, v := range args { q.Add(k, v) } u.RawQuery = q.Encode() req, err := http.NewRequest(http.MethodPost, u.String(), nil) if err != nil { return nil, err } req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.Token)) if c.Logging { x, _ := httputil.DumpRequestOut(req, true) log.Printf("REQUEST:\n\n%s\n\n", x) } res, err := client.Do(req) if c.Logging { x, _ := httputil.DumpResponse(res, true) log.Printf("RESPONSE:\n\n%s\n\n", x) } if res.StatusCode != 200 { msg, _ := ioutil.ReadAll(res.Body) return nil, fmt.Errorf("unsuccessful response: %d: %q", res.StatusCode, strings.TrimSpace(string(msg))) } return res, err } func (c *Client) microsubPostFormRequest(action string, args map[string]string, data url.Values) (*http.Response, error) { client := http.Client{} u := *c.MicrosubEndpoint q := u.Query() q.Add("action", action) for k, v := range args { q.Add(k, v) } u.RawQuery = q.Encode() req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(data.Encode())) if err != nil { return nil, err } req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.Token)) res, err := client.Do(req) if res.StatusCode != 200 { msg, _ := ioutil.ReadAll(res.Body) return nil, fmt.Errorf("unsuccessful response: %d: %q", res.StatusCode, strings.TrimSpace(string(msg))) } return res, err } // ChannelsGetList gets the channels from a Microsub server func (c *Client) ChannelsGetList() ([]microsub.Channel, error) { args := make(map[string]string) res, err := c.microsubGetRequest("channels", args) if err != nil { return []microsub.Channel{}, err } defer res.Body.Close() if res.StatusCode != 200 { body, err := ioutil.ReadAll(res.Body) if err != nil { return []microsub.Channel{}, fmt.Errorf("HTTP Status is not 200, but %d, error while reading body", res.StatusCode) } return []microsub.Channel{}, fmt.Errorf("HTTP Status is not 200, but %d: %s", res.StatusCode, body) } type channelsResponse struct { Channels []microsub.Channel `json:"channels"` } dec := json.NewDecoder(res.Body) var channels channelsResponse err = dec.Decode(&channels) return channels.Channels, err } // TimelineGet gets a timeline from a Microsub server func (c *Client) TimelineGet(before, after, channel string) (microsub.Timeline, error) { args := make(map[string]string) args["after"] = after args["before"] = before args["channel"] = channel res, err := c.microsubGetRequest("timeline", args) if err != nil { return microsub.Timeline{}, err } defer res.Body.Close() if res.StatusCode != 200 { body, err := ioutil.ReadAll(res.Body) if err != nil { return microsub.Timeline{}, fmt.Errorf("HTTP Status is not 200, but %d, error while reading body", res.StatusCode) } return microsub.Timeline{}, fmt.Errorf("HTTP Status is not 200, but %d: %s", res.StatusCode, body) } dec := json.NewDecoder(res.Body) var timeline microsub.Timeline err = dec.Decode(&timeline) if err != nil { return microsub.Timeline{}, err } return timeline, nil } // PreviewURL gets a Timeline for a url from a Microsub server func (c *Client) PreviewURL(url string) (microsub.Timeline, error) { args := make(map[string]string) args["url"] = url res, err := c.microsubPostRequest("preview", args) if err != nil { return microsub.Timeline{}, err } defer res.Body.Close() var timeline microsub.Timeline if res.StatusCode != 200 { body, err := ioutil.ReadAll(res.Body) if err != nil { return timeline, fmt.Errorf("HTTP Status is not 200, but %d, error while reading body", res.StatusCode) } return timeline, fmt.Errorf("HTTP Status is not 200, but %d: %s", res.StatusCode, body) } dec := json.NewDecoder(res.Body) err = dec.Decode(&timeline) if err != nil { return microsub.Timeline{}, err } return timeline, nil } // FollowGetList gets the list of followed feeds. func (c *Client) FollowGetList(channel string) ([]microsub.Feed, error) { args := make(map[string]string) args["channel"] = channel res, err := c.microsubGetRequest("follow", args) if err != nil { return []microsub.Feed{}, nil } defer res.Body.Close() if res.StatusCode != 200 { body, err := ioutil.ReadAll(res.Body) if err != nil { return []microsub.Feed{}, fmt.Errorf("HTTP Status is not 200, but %d, error while reading body", res.StatusCode) } return []microsub.Feed{}, fmt.Errorf("HTTP Status is not 200, but %d: %s", res.StatusCode, body) } dec := json.NewDecoder(res.Body) type followResponse struct { Items []microsub.Feed `json:"items"` } var response followResponse err = dec.Decode(&response) if err != nil { return []microsub.Feed{}, nil } return response.Items, nil } // ChannelsCreate creates and new channel on a microsub server. func (c *Client) ChannelsCreate(name string) (microsub.Channel, error) { args := make(map[string]string) args["name"] = name res, err := c.microsubPostRequest("channels", args) if err != nil { return microsub.Channel{}, nil } defer res.Body.Close() var channel microsub.Channel dec := json.NewDecoder(res.Body) err = dec.Decode(&channel) if err != nil { return microsub.Channel{}, nil } return channel, nil } // ChannelsUpdate updates a channel. func (c *Client) ChannelsUpdate(uid, name string) (microsub.Channel, error) { args := make(map[string]string) args["name"] = name args["channel"] = uid res, err := c.microsubPostRequest("channels", args) if err != nil { return microsub.Channel{}, err } defer res.Body.Close() var channel microsub.Channel dec := json.NewDecoder(res.Body) err = dec.Decode(&channel) if err != nil { return microsub.Channel{}, err } return channel, nil } // ChannelsDelete deletes a channel. func (c *Client) ChannelsDelete(uid string) error { args := make(map[string]string) args["channel"] = uid args["method"] = "delete" res, err := c.microsubPostRequest("channels", args) if err != nil { return err } res.Body.Close() return nil } // FollowURL follows a url. func (c *Client) FollowURL(channel, url string) (microsub.Feed, error) { args := make(map[string]string) args["channel"] = channel args["url"] = url res, err := c.microsubPostRequest("follow", args) if err != nil { return microsub.Feed{}, err } defer res.Body.Close() var feed microsub.Feed dec := json.NewDecoder(res.Body) err = dec.Decode(&feed) if err != nil { return microsub.Feed{}, err } return feed, nil } // UnfollowURL unfollows a url in a channel. func (c *Client) UnfollowURL(channel, url string) error { args := make(map[string]string) args["channel"] = channel args["url"] = url res, err := c.microsubPostRequest("unfollow", args) if err != nil { return err } res.Body.Close() return nil } // Search asks the server to search for the query. func (c *Client) Search(query string) ([]microsub.Feed, error) { args := make(map[string]string) args["query"] = query res, err := c.microsubPostRequest("search", args) if err != nil { return []microsub.Feed{}, err } type searchResponse struct { Results []microsub.Feed `json:"results"` } defer res.Body.Close() var response searchResponse dec := json.NewDecoder(res.Body) err = dec.Decode(&response) if err != nil { return []microsub.Feed{}, err } return response.Results, nil } // ItemSearch send a search request to the server func (c *Client) ItemSearch(channel, query string) ([]microsub.Item, error) { args := make(map[string]string) args["query"] = query args["channel"] = channel res, err := c.microsubPostRequest("search", args) if err != nil { return []microsub.Item{}, err } type searchResponse struct { Items []microsub.Item `json:"items"` } defer res.Body.Close() var response searchResponse dec := json.NewDecoder(res.Body) err = dec.Decode(&response) if err != nil { return []microsub.Item{}, err } return response.Items, nil } // MarkRead marks an item read on the server. func (c *Client) MarkRead(channel string, uids []string) error { args := make(map[string]string) args["channel"] = channel args["method"] = "mark_read" data := url.Values{} for _, uid := range uids { data.Add("entry[]", uid) } res, err := c.microsubPostFormRequest("timeline", args, data) if err != nil { return err } res.Body.Close() return nil } // Events open an event channel to the server. func (c *Client) Events() (chan sse.Message, error) { ch := make(chan sse.Message) errorCounter := 0 go func() { for { res, err := c.microsubGetRequest("events", nil) if err != nil { log.Printf("could not request events: %+v", err) errorCounter++ if errorCounter > 5 { break } continue } err = sse.Reader(res.Body, ch) if err != nil { log.Printf("could not create reader: %+v", err) break } } close(ch) }() return ch, nil }