ekster/pkg/server/microsub.go

290 lines
7.9 KiB
Go
Raw Normal View History

/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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"
"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"
"p83.nl/go/ekster/pkg/sse"
2018-05-22 18:29:07 +00:00
)
2018-09-12 20:35:49 +00:00
var (
entryRegex = regexp.MustCompile(`^entry\[\d+\]$`)
2018-09-12 20:35:49 +00:00
)
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 *sse.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(), http.StatusInternalServerError)
}
}
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, *sse.Broker) {
broker := sse.NewBroker()
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")
w.Header().Add("Access-Control-Allow-Headers", "Authorization, Cache-Control, Last-Event-ID")
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 {
2021-10-31 20:53:36 +00:00
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
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 {
2021-10-31 20:53:36 +00:00
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
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 {
2021-10-31 20:53:36 +00:00
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
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 {
2021-10-31 20:53:36 +00:00
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
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" {
events, err := h.backend.Events()
if err != nil {
2021-10-31 20:53:36 +00:00
log.Println(err)
http.Error(w, "could not start sse connection", http.StatusInternalServerError)
}
// Remove this client from the map of connected clients
// when this handler exits.
defer func() {
h.Broker.CloseClient(events)
}()
// Listen to connection close and un-register messageChan
notify := r.Context().Done()
go func() {
<-notify
h.Broker.CloseClient(events)
}()
err = sse.WriteMessages(w, events)
if err != nil {
log.Println(err)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
2018-05-22 18:29:07 +00:00
} else {
http.Error(w, fmt.Sprintf("unknown action %s", action), http.StatusBadRequest)
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 {
2021-10-31 20:53:36 +00:00
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
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 {
2021-10-31 20:53:36 +00:00
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
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 {
2021-10-31 20:53:36 +00:00
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
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(), http.StatusInternalServerError)
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(), http.StatusInternalServerError)
return
}
respondJSON(w, []string{})
} else if action == "preview" {
timeline, err := h.backend.PreviewURL(values.Get("url"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
respondJSON(w, timeline)
2018-05-22 18:29:07 +00:00
} else if action == "search" {
query := values.Get("query")
channel := values.Get("channel")
if channel == "" {
feeds, err := h.backend.Search(query)
if err != nil {
2021-06-05 18:02:46 +00:00
respondJSON(w, map[string]interface{}{
"query": query,
"error": err.Error(),
})
return
}
respondJSON(w, map[string][]microsub.Feed{
"results": feeds,
})
} else {
items, err := h.backend.ItemSearch(channel, query)
if err != nil {
2021-06-05 18:02:46 +00:00
respondJSON(w, map[string]interface{}{
"query": query,
"error": err.Error(),
})
return
}
log.Printf("Searching for %s in %s (%d results)", query, channel, len(items))
respondJSON(w, map[string]interface{}{
"query": query,
"items": items,
})
}
2018-05-22 18:29:07 +00:00
} 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(), http.StatusInternalServerError)
return
}
2021-05-13 09:21:31 +00:00
} else {
log.Println("No uids specified for mark read")
2018-05-22 18:29:07 +00:00
}
} else {
http.Error(w, fmt.Sprintf("unknown method in timeline %s\n", method), http.StatusInternalServerError)
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), http.StatusBadRequest)
2018-05-22 18:29:07 +00:00
}
return
}
}