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