Add Microsub CLI client
This commit is contained in:
parent
798c5c39d6
commit
c84e139903
69
cmd/client/main.go
Normal file
69
cmd/client/main.go
Normal 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
231
pkg/client/requests.go
Normal 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
147
pkg/indieauth/auth.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user