Add Microsub CLI client

This commit is contained in:
Peter Stuifzand 2018-05-12 13:08:36 +02:00
parent 798c5c39d6
commit c84e139903
3 changed files with 447 additions and 0 deletions

69
cmd/client/main.go Normal file
View File

@ -0,0 +1,69 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/url"
"os"
"github.com/pstuifzand/microsub-server/pkg/client"
"github.com/pstuifzand/microsub-server/pkg/indieauth"
)
func main() {
if len(os.Args) == 3 && os.Args[1] == "connect" {
err := os.MkdirAll("/home/peter/.config/microsub/", os.FileMode(0770))
if err != nil {
log.Fatal(err)
}
f, err := os.Create("/home/peter/.config/microsub/client.json")
if err != nil {
log.Fatal(err)
}
defer f.Close()
me := os.Args[2]
endpoints, err := indieauth.GetEndpoints(me)
token, err := indieauth.Authorize(me, endpoints)
if err != nil {
log.Fatal(err)
}
enc := json.NewEncoder(f)
enc.Encode(token)
log.Println("Authorization successful")
return
} else if len(os.Args) == 3 && os.Args[1] == "channels" {
me := os.Args[2]
endpoints, err := indieauth.GetEndpoints(me)
f, err := os.Open("/home/peter/.config/microsub/client.json")
if err != nil {
log.Fatal(err)
}
defer f.Close()
var token indieauth.TokenResponse
dec := json.NewDecoder(f)
err = dec.Decode(&token)
if err != nil {
log.Fatal(err)
}
var c client.Client
u, _ := url.Parse(endpoints.MicrosubEndpoint)
c.MicrosubEndpoint = u
c.Token = token.AccessToken
channels := c.ChannelsGetList()
for _, ch := range channels {
fmt.Println(ch.UID, " ", ch.Name)
}
}
}

231
pkg/client/requests.go Normal file
View File

@ -0,0 +1,231 @@
package client
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"github.com/pstuifzand/microsub-server/microsub"
)
type Client struct {
MicrosubEndpoint *url.URL
Token string
}
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))
return client.Do(req)
}
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))
return client.Do(req)
}
func (c *Client) ChannelsGetList() []microsub.Channel {
args := make(map[string]string)
res, err := c.microsubGetRequest("channels", args)
if err != nil {
return []microsub.Channel{}
}
defer res.Body.Close()
type channelsResponse struct {
Channels []microsub.Channel `json:"channels"`
}
dec := json.NewDecoder(res.Body)
var channels channelsResponse
dec.Decode(&channels)
return channels.Channels
}
func (c *Client) TimelineGet(after, before, channel string) microsub.Timeline {
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{}
}
defer res.Body.Close()
dec := json.NewDecoder(res.Body)
var timeline microsub.Timeline
err = dec.Decode(&timeline)
if err != nil {
log.Fatal(err)
}
return timeline
}
func (c *Client) PreviewURL(url string) []microsub.Timeline {
args := make(map[string]string)
args["url"] = url
res, err := c.microsubGetRequest("preview", args)
if err != nil {
return []microsub.Timeline{}
}
defer res.Body.Close()
dec := json.NewDecoder(res.Body)
var timeline []microsub.Timeline
dec.Decode(&timeline)
return timeline
}
func (c *Client) FollowGetList(channel string) []microsub.Feed {
args := make(map[string]string)
args["channel"] = channel
res, err := c.microsubGetRequest("follow", args)
if err != nil {
return []microsub.Feed{}
}
defer res.Body.Close()
dec := json.NewDecoder(res.Body)
type followResponse struct {
Items []microsub.Feed `json:"items"`
}
var response followResponse
dec.Decode(&response)
return response.Items
}
func (c *Client) ChannelsCreate(name string) microsub.Channel {
args := make(map[string]string)
args["name"] = name
res, err := c.microsubPostRequest("channels", args)
if err != nil {
return microsub.Channel{}
}
defer res.Body.Close()
var channel microsub.Channel
dec := json.NewDecoder(res.Body)
dec.Decode(&channel)
return channel
}
func (c *Client) ChannelsUpdate(uid, name string) microsub.Channel {
args := make(map[string]string)
args["name"] = name
args["uid"] = uid
res, err := c.microsubPostRequest("channels", args)
if err != nil {
return microsub.Channel{}
}
defer res.Body.Close()
var channel microsub.Channel
dec := json.NewDecoder(res.Body)
dec.Decode(&channel)
return channel
}
func (c *Client) ChannelsDelete(uid string) {
args := make(map[string]string)
args["uid"] = uid
args["method"] = "delete"
res, err := c.microsubPostRequest("channels", args)
if err != nil {
return
}
res.Body.Close()
}
func (c *Client) FollowURL(channel, url string) microsub.Feed {
args := make(map[string]string)
args["channel"] = channel
args["url"] = url
res, err := c.microsubPostRequest("follow", args)
if err != nil {
return microsub.Feed{}
}
defer res.Body.Close()
var feed microsub.Feed
dec := json.NewDecoder(res.Body)
dec.Decode(&feed)
return feed
}
func (c *Client) UnfollowURL(channel, url string) {
args := make(map[string]string)
args["channel"] = channel
args["url"] = url
res, err := c.microsubPostRequest("unfollow", args)
if err != nil {
return
}
res.Body.Close()
}
func (c *Client) Search(query string) []microsub.Feed {
args := make(map[string]string)
args["query"] = query
res, err := c.microsubPostRequest("search", args)
if err != nil {
return []microsub.Feed{}
}
type searchResponse struct {
Results []microsub.Feed `json:"results"`
}
defer res.Body.Close()
var response searchResponse
dec := json.NewDecoder(res.Body)
dec.Decode(&response)
return response.Results
}
func (c *Client) MarkRead(channel string, uids []string) {
// TODO(peter): Add Authorization header
client := http.Client{}
u := *c.MicrosubEndpoint
q := u.Query()
q.Add("action", "mark_read")
q.Add("channel", channel)
data := url.Values{}
for _, uid := range uids {
data.Add("entry[]", uid)
}
u.RawQuery = q.Encode()
res, err := client.PostForm(u.String(), data)
if err == nil {
defer res.Body.Close()
}
}

147
pkg/indieauth/auth.go Normal file
View File

@ -0,0 +1,147 @@
package indieauth
import (
"context"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"net/url"
"willnorris.com/go/microformats"
)
type Endpoints struct {
AuthorizationEndpoint string
TokenEndpoint string
MicropubEndpoint string
MicrosubEndpoint string
}
type TokenResponse struct {
Me string `json:"me"`
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
}
func GetEndpoints(me string) (Endpoints, error) {
var endpoints Endpoints
baseURL, err := url.Parse(me)
if err != nil {
return endpoints, err
}
res, err := http.Get(me)
if err != nil {
return endpoints, err
}
defer res.Body.Close()
data := microformats.Parse(res.Body, baseURL)
if auth, e := data.Rels["authorization_endpoint"]; e {
endpoints.AuthorizationEndpoint = auth[0]
}
if token, e := data.Rels["token_endpoint"]; e {
endpoints.TokenEndpoint = token[0]
}
if micropub, e := data.Rels["micropub"]; e {
endpoints.MicropubEndpoint = micropub[0]
}
if microsub, e := data.Rels["microsub"]; e {
endpoints.MicrosubEndpoint = microsub[0]
}
return endpoints, nil
}
func Authorize(me string, endpoints Endpoints) (TokenResponse, error) {
var tokenResponse TokenResponse
authURL, err := url.Parse(endpoints.AuthorizationEndpoint)
if err != nil {
return tokenResponse, err
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return tokenResponse, err
}
clientID := "https://p83.nl/microsub-client"
local := ln.Addr().String()
redirectURI := fmt.Sprintf("http://%s/", local)
state := "12345344"
q := authURL.Query()
q.Add("response_type", "code")
q.Add("me", me)
q.Add("client_id", clientID)
q.Add("redirect_uri", redirectURI)
q.Add("state", state)
q.Add("scope", "read follow mute block channels")
authURL.RawQuery = q.Encode()
log.Printf("Browse to %s\n", authURL.String())
shutdown := make(chan struct{}, 1)
code := ""
handler := func(w http.ResponseWriter, r *http.Request) {
code = r.URL.Query().Get("code")
responseState := r.URL.Query().Get("state")
if state != responseState {
log.Println("Wrong state response")
}
close(shutdown)
}
var srv http.Server
srv.Handler = http.HandlerFunc(handler)
idleConnsClosed := make(chan struct{})
go func() {
<-shutdown
// We received an interrupt signal, shut down.
if err := srv.Shutdown(context.Background()); err != nil {
// Error from closing listeners, or context timeout:
log.Printf("HTTP server Shutdown: %v", err)
}
close(idleConnsClosed)
}()
if err := srv.Serve(ln); err != http.ErrServerClosed {
// Error starting or closing listener:
log.Printf("HTTP server ListenAndServe: %v", err)
}
<-idleConnsClosed
reqValues := url.Values{}
reqValues.Add("grant_type", "authorization_code")
reqValues.Add("code", code)
reqValues.Add("redirect_uri", redirectURI)
reqValues.Add("client_id", clientID)
reqValues.Add("me", me)
res, err := http.PostForm(endpoints.TokenEndpoint, reqValues)
if err != nil {
return tokenResponse, err
}
defer res.Body.Close()
dec := json.NewDecoder(res.Body)
err = dec.Decode(&tokenResponse)
if err != nil {
return tokenResponse, err
}
return tokenResponse, nil
}