ekster/pkg/server/microsub.go

266 lines
6.8 KiB
Go
Raw Normal View History

2018-07-28 16:03:21 +00:00
/*
2019-03-07 19:55:25 +00:00
Package server contains the microsub server itself. It implements http.Handler.
It follows the spec at https://indieweb.org/Microsub-spec.
2018-07-28 16:03:21 +00:00
*/
2018-09-12 20:35:49 +00:00
package server
2018-05-22 18:29:07 +00:00
import (
"encoding/json"
"fmt"
2019-02-16 06:20:15 +00:00
"log"
2018-05-22 18:29:07 +00:00
"net/http"
2018-09-12 20:35:49 +00:00
"regexp"
2018-05-22 18:29:07 +00:00
"p83.nl/go/ekster/pkg/microsub"
2018-05-22 18:29:07 +00:00
)
2018-09-12 20:35:49 +00:00
var (
entryRegex = regexp.MustCompile("^entry\\[\\d+\\]$")
)
2019-03-07 19:55:25 +00:00
// Constants used for the responses
const (
OutputContentType = "application/json; charset=utf-8"
)
2018-05-22 18:29:07 +00:00
type microsubHandler struct {
2018-09-12 20:35:49 +00:00
backend microsub.Microsub
Broker *Broker
2018-09-12 20:35:49 +00:00
}
func respondJSON(w http.ResponseWriter, value interface{}) {
jw := json.NewEncoder(w)
jw.SetIndent("", " ")
jw.SetEscapeHTML(false)
w.Header().Add("Content-Type", OutputContentType)
err := jw.Encode(value)
if err != nil {
http.Error(w, err.Error(), 500)
}
}
2019-03-07 19:55:25 +00:00
// NewMicrosubHandler is the main entry point for the microsub server
// It returns a handler for HTTP and a broker that will send events.
func NewMicrosubHandler(backend microsub.Microsub) (http.Handler, *Broker) {
broker := NewServer()
return &microsubHandler{backend, broker}, broker
2018-05-22 18:29:07 +00:00
}
2019-03-07 19:55:25 +00:00
// Methods required by http.Handler
2018-05-22 18:29:07 +00:00
func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
2019-02-16 06:20:15 +00:00
// log.Printf("Incoming request: %s %s\n", r.Method, r.URL)
// log.Println(r.URL.Query())
// log.Println(r.PostForm)
2018-05-22 18:29:07 +00:00
2018-08-26 16:54:45 +00:00
if r.Method == http.MethodOptions {
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Methods", "GET, POST")
2018-09-11 18:53:48 +00:00
w.Header().Add("Access-Control-Allow-Headers", "Authorization, Cache-Control")
2018-08-26 16:54:45 +00:00
return
}
2018-05-22 18:29:07 +00:00
if r.Method == http.MethodGet {
w.Header().Add("Access-Control-Allow-Origin", "*")
2018-05-22 18:29:07 +00:00
values := r.URL.Query()
action := values.Get("action")
if action == "channels" {
2018-09-12 20:35:49 +00:00
channels, err := h.backend.ChannelsGetList()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
respondJSON(w, map[string][]microsub.Channel{
2018-05-22 18:29:07 +00:00
"channels": channels,
})
} else if action == "timeline" {
2018-09-12 20:35:49 +00:00
timeline, err := h.backend.TimelineGet(values.Get("before"), values.Get("after"), values.Get("channel"))
if err != nil {
http.Error(w, err.Error(), 500)
return
}
respondJSON(w, timeline)
2018-05-22 18:29:07 +00:00
} else if action == "preview" {
2018-09-12 20:35:49 +00:00
timeline, err := h.backend.PreviewURL(values.Get("url"))
if err != nil {
http.Error(w, err.Error(), 500)
return
}
respondJSON(w, timeline)
2018-05-22 18:29:07 +00:00
} else if action == "follow" {
channel := values.Get("channel")
2018-09-12 20:35:49 +00:00
following, err := h.backend.FollowGetList(channel)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
respondJSON(w, map[string][]microsub.Feed{
2018-05-22 18:29:07 +00:00
"items": following,
})
2018-09-08 15:49:20 +00:00
} else if action == "events" {
// Make sure that the writer supports flushing.
//
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
return
}
// Set the headers related to event streaming.
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
2019-02-20 09:50:43 +00:00
fmt.Fprintf(w, "event: welcome\r\n")
fmt.Fprintf(w, "data: {\"key\":\"hello world\"}\r\n\r\n")
flusher.Flush()
// Each connection registers its own message channel with the Broker's connections registry
messageChan := make(MessageChan)
// Signal the broker that we have a new connection
h.Broker.newClients <- messageChan
// Remove this client from the map of connected clients
// when this handler exits.
defer func() {
h.Broker.closingClients <- messageChan
}()
// Listen to connection close and un-register messageChan
notify := w.(http.CloseNotifier).CloseNotify()
go func() {
<-notify
h.Broker.closingClients <- messageChan
}()
// block waiting or messages broadcast on this connection's messageChan
for {
// Write to the ResponseWriter, Server Sent Events compatible
message := <-messageChan
output, err := json.Marshal(message.Object)
if err != nil {
log.Println(err)
continue
}
fmt.Fprintf(w, "event: %s\n", message.Event)
fmt.Fprintf(w, "data: %s\n\n", output)
// Flush the data immediately instead of buffering it for later.
flusher.Flush()
}
2018-05-22 18:29:07 +00:00
} else {
http.Error(w, fmt.Sprintf("unknown action %s\n", action), 400)
return
2018-05-22 18:29:07 +00:00
}
return
} else if r.Method == http.MethodPost {
w.Header().Add("Access-Control-Allow-Origin", "*")
2019-02-16 06:50:19 +00:00
values := r.Form
2018-05-22 18:29:07 +00:00
action := values.Get("action")
if action == "channels" {
name := values.Get("name")
method := values.Get("method")
uid := values.Get("channel")
if method == "delete" {
2018-09-12 20:35:49 +00:00
err := h.backend.ChannelsDelete(uid)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
respondJSON(w, []string{})
2018-05-22 18:29:07 +00:00
return
}
if uid == "" {
2018-09-12 20:35:49 +00:00
channel, err := h.backend.ChannelsCreate(name)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
respondJSON(w, channel)
} else if name != "" {
2018-09-12 20:35:49 +00:00
channel, err := h.backend.ChannelsUpdate(uid, name)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
respondJSON(w, channel)
2018-05-22 18:29:07 +00:00
}
} else if action == "follow" {
uid := values.Get("channel")
url := values.Get("url")
2018-09-12 20:35:49 +00:00
// h.HubIncomingBackend.CreateFeed(url, uid)
feed, err := h.backend.FollowURL(uid, url)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
respondJSON(w, feed)
2018-05-22 18:29:07 +00:00
} else if action == "unfollow" {
uid := values.Get("channel")
url := values.Get("url")
2018-09-12 20:35:49 +00:00
err := h.backend.UnfollowURL(uid, url)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
respondJSON(w, []string{})
2018-05-22 18:29:07 +00:00
} else if action == "search" {
query := values.Get("query")
2018-09-12 20:35:49 +00:00
feeds, err := h.backend.Search(query)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
respondJSON(w, map[string][]microsub.Feed{
2018-05-22 18:29:07 +00:00
"results": feeds,
})
} else if action == "timeline" || r.PostForm.Get("action") == "timeline" {
method := values.Get("method")
if method == "mark_read" || r.PostForm.Get("method") == "mark_read" {
values = r.Form
channel := values.Get("channel")
2018-08-15 17:04:44 +00:00
var markAsRead []string
2018-05-22 18:29:07 +00:00
if uids, e := values["entry"]; e {
2018-08-15 17:04:44 +00:00
markAsRead = uids
2018-05-22 18:29:07 +00:00
} else if uids, e := values["entry[]"]; e {
2018-08-15 17:04:44 +00:00
markAsRead = uids
2018-05-22 18:29:07 +00:00
} else {
uids := []string{}
for k, v := range values {
if entryRegex.MatchString(k) {
uids = append(uids, v...)
}
}
2018-08-15 17:04:44 +00:00
markAsRead = uids
}
if len(markAsRead) > 0 {
2018-09-12 20:35:49 +00:00
err := h.backend.MarkRead(channel, markAsRead)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
2018-05-22 18:29:07 +00:00
}
} else {
http.Error(w, fmt.Sprintf("unknown method in timeline %s\n", method), 500)
return
2018-05-22 18:29:07 +00:00
}
respondJSON(w, []string{})
2018-05-22 18:29:07 +00:00
} else {
http.Error(w, fmt.Sprintf("unknown action %s\n", action), 400)
2018-05-22 18:29:07 +00:00
}
return
}
return
}