Compare commits

..

No commits in common. "b3fe5c17ed6db5aba74b37eed29b1789ede7c265" and "a7f19759dfd5c90697a5000f8ae933a0ab22bd95" have entirely different histories.

14 changed files with 160 additions and 383 deletions

View File

@ -1,6 +1,11 @@
# ekster # ekster
a [Microsub](https://indieweb.org/Microsub) server a microsub server
## Warning!
Very alpha: no warranty.
## Installing and running ekster ## Installing and running ekster
@ -14,7 +19,7 @@ you need a Go environment. Use these commands to install the programs.
go get -u p83.nl/go/ekster/cmd/eksterd go get -u p83.nl/go/ekster/cmd/eksterd
go get -u p83.nl/go/ekster/cmd/ek go get -u p83.nl/go/ekster/cmd/ek
`eksterd` uses [Redis](https://redis.io/) as the database to temporarily save `eksterd` uses [Redis](https://redis.io/) as the database, to temporarily save
the items and feeds. The more permanent information is saved in `backend.json`. the items and feeds. The more permanent information is saved in `backend.json`.
#### Running eksterd #### Running eksterd
@ -157,7 +162,3 @@ Micropub client.
`ekster` will check every 10 minutes, if the token is still valid. This could `ekster` will check every 10 minutes, if the token is still valid. This could
be retrieved automatically, but this doesn't happen at the moment. be retrieved automatically, but this doesn't happen at the moment.
## Other Microsub projects
* <https://indieweb.org/Microsub>
* Aperture: [code](https://github.com/aaronparecki/Aperture), [hosted](https://aperture.p3k.io)

View File

@ -26,12 +26,20 @@ import (
"time" "time"
"github.com/gomodule/redigo/redis" "github.com/gomodule/redigo/redis"
"p83.nl/go/ekster/pkg/auth"
) )
// TokenResponse is the information that we get back from the token endpoint of the user...
type TokenResponse struct {
Me string `json:"me"`
ClientID string `json:"client_id"`
Scope string `json:"scope"`
IssuedAt int64 `json:"issued_at"`
Nonce int64 `json:"nonce"`
}
var authHeaderRegex = regexp.MustCompile("^Bearer (.+)$") var authHeaderRegex = regexp.MustCompile("^Bearer (.+)$")
func (b *memoryBackend) cachedCheckAuthToken(conn redis.Conn, header string, r *auth.TokenResponse) bool { func (h *microsubHandler) cachedCheckAuthToken(conn redis.Conn, header string, r *TokenResponse) bool {
log.Println("Cached checking Auth Token") log.Println("Cached checking Auth Token")
tokens := authHeaderRegex.FindStringSubmatch(header) tokens := authHeaderRegex.FindStringSubmatch(header)
@ -52,7 +60,7 @@ func (b *memoryBackend) cachedCheckAuthToken(conn redis.Conn, header string, r *
log.Printf("Error while HGETALL %v\n", err) log.Printf("Error while HGETALL %v\n", err)
} }
authorized := b.checkAuthToken(header, r) authorized := h.checkAuthToken(header, r)
authorized = true authorized = true
if authorized { if authorized {
@ -77,10 +85,10 @@ func (b *memoryBackend) cachedCheckAuthToken(conn redis.Conn, header string, r *
return authorized return authorized
} }
func (b *memoryBackend) checkAuthToken(header string, token *auth.TokenResponse) bool { func (h *microsubHandler) checkAuthToken(header string, token *TokenResponse) bool {
log.Println("Checking auth token") log.Println("Checking auth token")
tokenEndpoint := b.TokenEndpoint tokenEndpoint := h.Backend.(*memoryBackend).TokenEndpoint
req, err := http.NewRequest("GET", tokenEndpoint, nil) req, err := http.NewRequest("GET", tokenEndpoint, nil)
if err != nil { if err != nil {

View File

@ -1,4 +1,4 @@
package server package main
import ( import (
"encoding/json" "encoding/json"

View File

@ -211,7 +211,7 @@ func isLoggedIn(backend *memoryBackend, sess *session) bool {
return false return false
} }
if !authEnabled { if !auth {
return true return true
} }
@ -448,7 +448,7 @@ func (h *mainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
scope = "create" scope = "create"
} }
authReq := authRequest{ auth := authRequest{
Me: me, Me: me,
ClientID: clientID, ClientID: clientID,
RedirectURI: redirectURI, RedirectURI: redirectURI,
@ -456,7 +456,7 @@ func (h *mainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
State: state, State: state,
} }
_, err = conn.Do("HMSET", redis.Args{}.Add("state:"+state).AddFlat(&authReq)...) _, err = conn.Do("HMSET", redis.Args{}.Add("state:"+state).AddFlat(&auth)...)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
fmt.Fprintf(w, "ERROR: %q\n", err) fmt.Fprintf(w, "ERROR: %q\n", err)

View File

@ -23,13 +23,12 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"regexp"
"time" "time"
"github.com/gomodule/redigo/redis" "github.com/gomodule/redigo/redis"
"p83.nl/go/ekster/pkg/auth"
"p83.nl/go/ekster/pkg/microsub" "p83.nl/go/ekster/pkg/microsub"
"p83.nl/go/ekster/pkg/server"
) )
const ( const (
@ -39,15 +38,16 @@ const (
var ( var (
pool *redis.Pool pool *redis.Pool
port int port int
authEnabled bool auth bool
redisServer = flag.String("redis", "redis:6379", "") redisServer = flag.String("redis", "redis:6379", "")
entryRegex = regexp.MustCompile("^entry\\[\\d+\\]$")
) )
func init() { func init() {
log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime) log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime)
flag.IntVar(&port, "port", 80, "port for serving api") flag.IntVar(&port, "port", 80, "port for serving api")
flag.BoolVar(&authEnabled, "auth", true, "use auth") flag.BoolVar(&auth, "auth", true, "use auth")
} }
func newPool(addr string) *redis.Pool { func newPool(addr string) *redis.Pool {
@ -58,33 +58,11 @@ func newPool(addr string) *redis.Pool {
} }
} }
func WithAuth(handler http.Handler, b *memoryBackend) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authorization := r.Header.Get("Authorization")
var token auth.TokenResponse
if !b.AuthTokenAccepted(authorization, &token) {
log.Printf("Token could not be validated")
http.Error(w, "Can't validate token", 403)
return
}
if token.Me != b.Me {
log.Printf("Missing \"me\" in token response: %#v\n", token)
http.Error(w, "Wrong me", 403)
return
}
handler.ServeHTTP(w, r)
})
}
func main() { func main() {
log.Println("eksterd - microsub server") log.Println("eksterd - microsub server")
flag.Parse() flag.Parse()
if authEnabled { if auth {
log.Println("Using auth") log.Println("Using auth")
} else { } else {
log.Println("Authentication disabled") log.Println("Authentication disabled")
@ -120,13 +98,10 @@ func main() {
Backend: backend.(*memoryBackend), Backend: backend.(*memoryBackend),
}) })
handler := server.NewMicrosubHandler(backend) http.Handle("/microsub", &microsubHandler{
if authEnabled { Backend: backend,
handler = WithAuth(handler, backend.(*memoryBackend)) HubIncomingBackend: &hubBackend,
} })
http.Handle("/microsub", handler)
http.Handle("/incoming/", &incomingHandler{ http.Handle("/incoming/", &incomingHandler{
Backend: &hubBackend, Backend: &hubBackend,
}) })

View File

@ -32,7 +32,6 @@ import (
"sync" "sync"
"time" "time"
"p83.nl/go/ekster/pkg/auth"
"p83.nl/go/ekster/pkg/fetch" "p83.nl/go/ekster/pkg/fetch"
"p83.nl/go/ekster/pkg/microsub" "p83.nl/go/ekster/pkg/microsub"
"p83.nl/go/ekster/pkg/util" "p83.nl/go/ekster/pkg/util"
@ -42,8 +41,6 @@ import (
) )
type memoryBackend struct { type memoryBackend struct {
hubIncomingBackend
lock sync.RWMutex lock sync.RWMutex
Channels map[string]microsub.Channel Channels map[string]microsub.Channel
Feeds map[string][]microsub.Feed Feeds map[string][]microsub.Feed
@ -81,12 +78,6 @@ func (f *fetch2) Fetch(url string) (*http.Response, error) {
return Fetch2(url) return Fetch2(url)
} }
func (b *memoryBackend) AuthTokenAccepted(header string, r *auth.TokenResponse) bool {
conn := pool.Get()
defer conn.Close()
return b.cachedCheckAuthToken(conn, header, r)
}
func (b *memoryBackend) Debug() { func (b *memoryBackend) Debug() {
b.lock.RLock() b.lock.RLock()
defer b.lock.RUnlock() defer b.lock.RUnlock()
@ -421,8 +412,6 @@ func (b *memoryBackend) FollowURL(uid string, url string) (microsub.Feed, error)
b.ProcessContent(uid, feed.URL, resp.Header.Get("Content-Type"), resp.Body) b.ProcessContent(uid, feed.URL, resp.Header.Get("Content-Type"), resp.Body)
b.CreateFeed(url, uid)
return feed, nil return feed, nil
} }
@ -509,14 +498,12 @@ func (b *memoryBackend) Search(query string) ([]microsub.Feed, error) {
} }
defer feedResp.Body.Close() defer feedResp.Body.Close()
// TODO: Combine FeedHeader and FeedItems so we can use it here
parsedFeed, err := fetch.FeedHeader(&fetch2{}, fetchUrl.String(), feedResp.Header.Get("Content-Type"), feedResp.Body) parsedFeed, err := fetch.FeedHeader(&fetch2{}, fetchUrl.String(), feedResp.Header.Get("Content-Type"), feedResp.Body)
if err != nil { if err != nil {
log.Printf("Error in parse of %s - %v\n", fetchUrl, err) log.Printf("Error in parse of %s - %v\n", fetchUrl, err)
continue continue
} }
// TODO: Only include the feed if it contains some items
feeds = append(feeds, parsedFeed) feeds = append(feeds, parsedFeed)
if alts, e := md.Rels["alternate"]; e { if alts, e := md.Rels["alternate"]; e {

View File

@ -15,35 +15,30 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package server package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"regexp" "os"
"p83.nl/go/ekster/pkg/microsub" "p83.nl/go/ekster/pkg/microsub"
)
var ( "github.com/gomodule/redigo/redis"
entryRegex = regexp.MustCompile("^entry\\[\\d+\\]$")
)
const (
OutputContentType = "application/json; charset=utf-8"
) )
type microsubHandler struct { type microsubHandler struct {
backend microsub.Microsub Backend microsub.Microsub
} HubIncomingBackend HubBackend
func NewMicrosubHandler(backend microsub.Microsub) http.Handler {
return &microsubHandler{backend}
} }
func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var logger = log.New(os.Stdout, "logger: ", log.Lshortfile)
conn := redis.NewLoggingConn(pool.Get(), logger, "microsub")
defer conn.Close()
r.ParseForm() r.ParseForm()
log.Printf("%s %s\n", r.Method, r.URL) log.Printf("%s %s\n", r.Method, r.URL)
log.Println(r.URL.Query()) log.Println(r.URL.Query())
@ -56,12 +51,31 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
if auth {
authorization := r.Header.Get("Authorization")
var token TokenResponse
if !h.cachedCheckAuthToken(conn, authorization, &token) {
log.Printf("Token could not be validated")
http.Error(w, "Can't validate token", 403)
return
}
if token.Me != h.Backend.(*memoryBackend).Me {
log.Printf("Missing \"me\" in token response: %#v\n", token)
http.Error(w, "Wrong me", 403)
return
}
}
const OutputContentType = "application/json; charset=utf-8"
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
w.Header().Add("Access-Control-Allow-Origin", "*") w.Header().Add("Access-Control-Allow-Origin", "*")
values := r.URL.Query() values := r.URL.Query()
action := values.Get("action") action := values.Get("action")
if action == "channels" { if action == "channels" {
channels, err := h.backend.ChannelsGetList() channels, err := h.Backend.ChannelsGetList()
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
@ -76,7 +90,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
} else if action == "timeline" { } else if action == "timeline" {
timeline, err := h.backend.TimelineGet(values.Get("before"), values.Get("after"), values.Get("channel")) timeline, err := h.Backend.TimelineGet(values.Get("before"), values.Get("after"), values.Get("channel"))
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
@ -91,7 +105,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
} else if action == "preview" { } else if action == "preview" {
timeline, err := h.backend.PreviewURL(values.Get("url")) timeline, err := h.Backend.PreviewURL(values.Get("url"))
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
@ -106,7 +120,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} else if action == "follow" { } else if action == "follow" {
channel := values.Get("channel") channel := values.Get("channel")
following, err := h.backend.FollowGetList(channel) following, err := h.Backend.FollowGetList(channel)
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
@ -123,7 +137,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} else if action == "events" { } else if action == "events" {
conn, _, _ := w.(http.Hijacker).Hijack() conn, _, _ := w.(http.Hijacker).Hijack()
cons := newConsumer(conn) cons := newConsumer(conn)
h.backend.AddEventListener(cons) h.Backend.AddEventListener(cons)
} else { } else {
http.Error(w, fmt.Sprintf("unknown action %s\n", action), 500) http.Error(w, fmt.Sprintf("unknown action %s\n", action), 500)
return return
@ -139,19 +153,20 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
method := values.Get("method") method := values.Get("method")
uid := values.Get("channel") uid := values.Get("channel")
if method == "delete" { if method == "delete" {
err := h.backend.ChannelsDelete(uid) err := h.Backend.ChannelsDelete(uid)
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
} }
w.Header().Add("Content-Type", OutputContentType) w.Header().Add("Content-Type", OutputContentType)
fmt.Fprintln(w, "[]") fmt.Fprintln(w, "[]")
h.Backend.(Debug).Debug()
return return
} }
jw := json.NewEncoder(w) jw := json.NewEncoder(w)
if uid == "" { if uid == "" {
channel, err := h.backend.ChannelsCreate(name) channel, err := h.Backend.ChannelsCreate(name)
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
@ -163,7 +178,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
} else { } else {
channel, err := h.backend.ChannelsUpdate(uid, name) channel, err := h.Backend.ChannelsUpdate(uid, name)
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
@ -175,11 +190,12 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
h.Backend.(Debug).Debug()
} else if action == "follow" { } else if action == "follow" {
uid := values.Get("channel") uid := values.Get("channel")
url := values.Get("url") url := values.Get("url")
// h.HubIncomingBackend.CreateFeed(url, uid) h.HubIncomingBackend.CreateFeed(url, uid)
feed, err := h.backend.FollowURL(uid, url) feed, err := h.Backend.FollowURL(uid, url)
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
@ -194,7 +210,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} else if action == "unfollow" { } else if action == "unfollow" {
uid := values.Get("channel") uid := values.Get("channel")
url := values.Get("url") url := values.Get("url")
err := h.backend.UnfollowURL(uid, url) err := h.Backend.UnfollowURL(uid, url)
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
@ -203,7 +219,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "[]") fmt.Fprintln(w, "[]")
} else if action == "search" { } else if action == "search" {
query := values.Get("query") query := values.Get("query")
feeds, err := h.backend.Search(query) feeds, err := h.Backend.Search(query)
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
@ -239,7 +255,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
if len(markAsRead) > 0 { if len(markAsRead) > 0 {
err := h.backend.MarkRead(channel, markAsRead) err := h.Backend.MarkRead(channel, markAsRead)
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return

View File

@ -1,14 +0,0 @@
package auth
type Auther interface {
AuthTokenAccepted(header string, r *TokenResponse) bool
}
// TokenResponse is the information that we get back from the token endpoint of the user...
type TokenResponse struct {
Me string `json:"me"`
ClientID string `json:"client_id"`
Scope string `json:"scope"`
IssuedAt int64 `json:"issued_at"`
Nonce int64 `json:"nonce"`
}

View File

@ -20,7 +20,6 @@ package jf2
import ( import (
"fmt" "fmt"
"log" "log"
"reflect"
"strings" "strings"
"time" "time"
@ -29,54 +28,6 @@ import (
"willnorris.com/go/microformats" "willnorris.com/go/microformats"
) )
func ConvertItemProps(item interface{}, props map[string][]interface{}) {
sv := reflect.ValueOf(item).Elem()
st := reflect.TypeOf(item).Elem()
for i := 0; i < st.NumField(); i++ {
ft := st.Field(i)
fv := sv.Field(i)
if value, ok := ft.Tag.Lookup("mf2"); ok {
if value == "" {
continue
}
if s, e := props[value]; e {
if len(s) > 0 {
if str, ok := s[0].(string); ft.Type.Kind() == reflect.String && ok {
fv.SetString(str)
} else if ft.Type.Kind() == reflect.Slice {
for _, v := range s {
fv.Set(reflect.Append(fv, reflect.ValueOf(v)))
}
} else if card, ok := s[0].(map[string]interface{}); ok {
var hcard microsub.Card
if t, ok := card["type"].([]interface{}); ok {
hcard.Type = t[0].(string)[2:]
}
if properties, ok := card["properties"].(map[string]interface{}); ok {
ps := make(map[string][]interface{})
for k, v := range properties {
ps[k] = v.([]interface{})
}
ConvertItemProps(&hcard, ps)
}
fv.Set(reflect.ValueOf(&hcard))
}
}
}
}
}
}
func ConvertItem(item interface{}, md *microformats.Microformat) {
sv := reflect.ValueOf(item).Elem()
sv.FieldByName("Type").SetString(md.Type[0][2:])
ConvertItemProps(item, md.Properties)
}
func simplify(itemType string, item map[string][]interface{}, author map[string]string) map[string]interface{} { func simplify(itemType string, item map[string][]interface{}, author map[string]string) map[string]interface{} {
feedItem := make(map[string]interface{}) feedItem := make(map[string]interface{})
@ -249,7 +200,6 @@ func fetchValue(key string, values map[string]string) string {
} }
return "" return ""
} }
func MapToAuthor(result map[string]string) *microsub.Card { func MapToAuthor(result map[string]string) *microsub.Card {
item := &microsub.Card{} item := &microsub.Card{}
item.Type = "card" item.Type = "card"

View File

@ -18,197 +18,78 @@
package jf2 package jf2
import ( import (
"encoding/json" "log"
"net/url"
"os" "os"
"testing" "testing"
"p83.nl/go/ekster/pkg/microsub"
"willnorris.com/go/microformats" "willnorris.com/go/microformats"
) )
// func TestInReplyTo(t *testing.T) { func TestInReplyTo(t *testing.T) {
//
// f, err := os.Open("./tests/tantek-in-reply-to.html")
// if err != nil {
// log.Fatal(err)
// }
// defer f.Close()
//
// u, err := url.Parse("http://tantek.com/2018/115/t1/")
// if err != nil {
// log.Fatal(err)
// }
//
// data := microformats.Parse(f, u)
// results := SimplifyMicroformatData(data)
//
// if results[0]["type"] != "entry" {
// t.Fatalf("not an h-entry, but %s", results[0]["type"])
// }
// if results[0]["in-reply-to"] != "https://github.com/w3c/csswg-drafts/issues/2589" {
// t.Fatalf("not in-reply-to, but %s", results[0]["in-reply-to"])
// }
// if results[0]["syndication"] != "https://github.com/w3c/csswg-drafts/issues/2589#thumbs_up-by-tantek" {
// t.Fatalf("not in-reply-to, but %s", results[0]["syndication"])
// }
// if results[0]["published"] != "2018-04-25 11:14-0700" {
// t.Fatalf("not published, but %s", results[0]["published"])
// }
// if results[0]["updated"] != "2018-04-25 11:14-0700" {
// t.Fatalf("not updated, but %s", results[0]["updated"])
// }
// if results[0]["url"] != "http://tantek.com/2018/115/t1/" {
// t.Fatalf("not url, but %s", results[0]["url"])
// }
// if results[0]["uid"] != "http://tantek.com/2018/115/t1/" {
// t.Fatalf("not uid, but %s", results[0]["url"])
// }
//
// if authorValue, e := results[0]["author"]; e {
// if author, ok := authorValue.(map[string]string); ok {
// if author["name"] != "Tantek Çelik" {
// t.Fatalf("name is not expected name, but %q", author["name"])
// }
// if author["photo"] != "http://tantek.com/logo.jpg" {
// t.Fatalf("photo is not expected photo, but %q", author["photo"])
// }
// if author["url"] != "http://tantek.com/" {
// t.Fatalf("url is not expected url, but %q", author["url"])
// }
// } else {
// t.Fatal("author not a map")
// }
// } else {
// t.Fatal("author missing")
// }
//
// if contentValue, e := results[0]["content"]; e {
// if content, ok := contentValue.(map[string]string); ok {
// if content["text"] != "👍" {
// t.Fatal("text content missing")
// }
// if content["html"] != "👍" {
// t.Fatal("html content missing")
// }
// }
// }
// }
func TestMapToAuthor(t *testing.T) { f, err := os.Open("./tests/tantek-in-reply-to.html")
cardmap := make(map[string]string)
cardmap["name"] = "Peter"
cardmap["url"] = "https://p83.nl/"
cardmap["photo"] = "https://peterstuifzand.nl/img/profile.jpg"
card := MapToAuthor(cardmap)
if card.Type != "card" {
t.Error("mapped author type is not card")
}
if card.Name != cardmap["name"] {
t.Errorf("%q is not equal to %q", card.Name, "Peter")
}
if card.URL != cardmap["url"] {
t.Errorf("%q is not equal to %q", card.URL, cardmap["url"])
}
if card.Photo != cardmap["photo"] {
t.Errorf("%q is not equal to %q", card.Photo, cardmap["photo"])
}
}
func TestMapToItem(t *testing.T) {
itemmap := make(map[string]interface{})
itemmap["type"] = "entry"
itemmap["name"] = "Title"
c := make(map[string]interface{})
c["text"] = "Simple content"
c["html"] = "<p>Simple content</p>"
itemmap["content"] = c
itemmap["like-of"] = []string{
"https://p83.nl/",
"https://p83.nl/test.html",
}
item := MapToItem(itemmap)
if item.Type != "entry" {
t.Errorf("Expected Type entry, was %q", item.Type)
}
if item.Name != "Title" {
t.Errorf("Expected Name == %q, was actually %q", "Title", item.Name)
}
if item.Content.Text != "Simple content" {
t.Errorf("Expected Content.Text == %q, was actually %q", "Simple content", item.Content.Text)
}
if item.Content.HTML != "<p>Simple content</p>" {
t.Errorf("Expected Content.HTML == %q, was actually %q", "<p>Simple content</p>", item.Content.HTML)
}
// if val := item.LikeOf[0]; val != "https://p83.nl/" {
// t.Errorf("Expected LikeOf[0] == %q, was actually %q", "https://p83.nl/", val)
// }
// if val := item.LikeOf[1]; val != "https://p83.nl/test.html" {
// t.Errorf("Expected LikeOf[1] == %q, was actually %q", "https://p83.nl/test.html", val)
// }
}
func TestConvertItem0(t *testing.T) {
var item microsub.Item
var mdItem microformats.Microformat
f, err := os.Open("tests/test0.json")
if err != nil { if err != nil {
t.Fatalf("error while opening test0.json: %s", err) log.Fatal(err)
} }
json.NewDecoder(f).Decode(&mdItem) defer f.Close()
ConvertItem(&item, &mdItem)
if item.Type != "entry" { u, err := url.Parse("http://tantek.com/2018/115/t1/")
t.Errorf("Expected Type entry, was %q", item.Type)
}
if item.Name != "name test" {
t.Errorf("Expected Name == %q, was %q", "name test", item.Name)
}
}
func TestConvertItem1(t *testing.T) {
var item microsub.Item
var mdItem microformats.Microformat
f, err := os.Open("tests/test1.json")
if err != nil { if err != nil {
t.Fatalf("error while opening test1.json: %s", err) log.Fatal(err)
} }
json.NewDecoder(f).Decode(&mdItem)
ConvertItem(&item, &mdItem)
if item.Type != "entry" { data := microformats.Parse(f, u)
t.Errorf("Expected Type entry, was %q", item.Type) results := SimplifyMicroformatData(data)
if results[0]["type"] != "entry" {
t.Fatalf("not an h-entry, but %s", results[0]["type"])
} }
if item.Author.Type != "card" { if results[0]["in-reply-to"] != "https://github.com/w3c/csswg-drafts/issues/2589" {
t.Errorf("Expected Author.Type card, was %q", item.Author.Type) t.Fatalf("not in-reply-to, but %s", results[0]["in-reply-to"])
} }
if item.Author.Name != "Peter" { if results[0]["syndication"] != "https://github.com/w3c/csswg-drafts/issues/2589#thumbs_up-by-tantek" {
t.Errorf("Expected Author.Name == %q, was %q", "Peter", item.Author.Name) t.Fatalf("not in-reply-to, but %s", results[0]["syndication"])
} }
} if results[0]["published"] != "2018-04-25 11:14-0700" {
t.Fatalf("not published, but %s", results[0]["published"])
func TestConvertItem2(t *testing.T) { }
var item microsub.Item if results[0]["updated"] != "2018-04-25 11:14-0700" {
var mdItem microformats.Microformat t.Fatalf("not updated, but %s", results[0]["updated"])
f, err := os.Open("tests/test2.json") }
if err != nil { if results[0]["url"] != "http://tantek.com/2018/115/t1/" {
t.Fatalf("error while opening test2.json: %s", err) t.Fatalf("not url, but %s", results[0]["url"])
} }
json.NewDecoder(f).Decode(&mdItem) if results[0]["uid"] != "http://tantek.com/2018/115/t1/" {
ConvertItem(&item, &mdItem) t.Fatalf("not uid, but %s", results[0]["url"])
}
if item.Type != "entry" {
t.Errorf("Expected Type entry, was %q", item.Type) if authorValue, e := results[0]["author"]; e {
} if author, ok := authorValue.(map[string]string); ok {
if item.Photo[0] != "https://peterstuifzand.nl/img/profile.jpg" { if author["name"] != "Tantek Çelik" {
t.Errorf("Expected Photo[0], was %q", item.Type) t.Fatalf("name is not expected name, but %q", author["name"])
} }
if item.Author.Type != "card" { if author["photo"] != "http://tantek.com/logo.jpg" {
t.Errorf("Expected Author.Type card, was %q", item.Author.Type) t.Fatalf("photo is not expected photo, but %q", author["photo"])
} }
if item.Author.Name != "Peter" { if author["url"] != "http://tantek.com/" {
t.Errorf("Expected Author.Name == %q, was %q", "Peter", item.Author.Name) t.Fatalf("url is not expected url, but %q", author["url"])
}
} else {
t.Fatal("author not a map")
}
} else {
t.Fatal("author missing")
}
if contentValue, e := results[0]["content"]; e {
if content, ok := contentValue.(map[string]string); ok {
if content["text"] != "👍" {
t.Fatal("text content missing")
}
if content["html"] != "👍" {
t.Fatal("html content missing")
}
}
} }
} }

View File

@ -1 +0,0 @@
{"type":["h-entry"],"properties":{"name":["name test"]}}

View File

@ -1 +0,0 @@
{"type":["h-entry"],"properties":{"name":["name test"],"author":[{"type":["h-card"],"properties":{"name":["Peter"]}}]}}

View File

@ -1,25 +0,0 @@
{
"type": [
"h-entry"
],
"properties": {
"name": [
"name test"
],
"photo": [
"https://peterstuifzand.nl/img/profile.jpg"
],
"author": [
{
"type": [
"h-card"
],
"properties": {
"name": [
"Peter"
]
}
}
]
}
}

View File

@ -40,40 +40,40 @@ type Channel struct {
type Card struct { type Card struct {
Filled bool `json:"-,omitempty"` Filled bool `json:"-,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Name string `json:"name,omitempty" mf2:"name"` Name string `json:"name,omitempty"`
URL string `json:"url,omitempty" mf2:"url"` URL string `json:"url,omitempty"`
Photo string `json:"photo,omitempty" mf2:"photo"` Photo string `json:"photo,omitempty"`
Locality string `json:"locality,omitempty" mf2:"locality"` Locality string `json:"locality,omitempty"`
Region string `json:"region,omitempty" mf2:"region"` Region string `json:"region,omitempty"`
CountryName string `json:"country-name,omitempty" mf2:"country-name"` CountryName string `json:"country-name,omitempty"`
Longitude string `json:"longitude,omitempty" mf2:"longitude"` Longitude string `json:"longitude,omitempty"`
Latitude string `json:"latitude,omitempty" mf2:"latitude"` Latitude string `json:"latitude,omitempty"`
} }
type Content struct { type Content struct {
Text string `json:"text,omitempty" mf2:"value"` Text string `json:"text,omitempty"`
HTML string `json:"html,omitempty" mf2:"html"` HTML string `json:"html,omitempty"`
} }
// Item is a post object // Item is a post object
type Item struct { type Item struct {
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name,omitempty" mf2:"name"` Name string `json:"name,omitempty"`
Published string `json:"published,omitempty" mf2:"published"` Published string `json:"published,omitempty"`
Updated string `json:"updated,omitempty" mf2:"updated"` Updated string `json:"updated,omitempty"`
URL string `json:"url,omitempty" mf2:"url"` URL string `json:"url,omitempty"`
UID string `json:"uid,omitempty" mf2:"uid"` UID string `json:"uid,omitempty"`
Author *Card `json:"author,omitempty" mf2:"author"` Author *Card `json:"author,omitempty"`
Category []string `json:"category,omitempty" mf2:"category"` Category []string `json:"category,omitempty"`
Photo []string `json:"photo,omitempty" mf2:"photo"` Photo []string `json:"photo,omitempty"`
LikeOf []string `json:"like-of,omitempty" mf2:"like-of"` LikeOf []string `json:"like-of,omitempty"`
BookmarkOf []string `json:"bookmark-of,omitempty" mf2:"bookmark-of"` BookmarkOf []string `json:"bookmark-of,omitempty"`
RepostOf []string `json:"repost-of,omitempty" mf2:"repost-of"` RepostOf []string `json:"repost-of,omitempty"`
InReplyTo []string `json:"in-reply-to,omitempty" mf2:"in-reply-to"` InReplyTo []string `json:"in-reply-to,omitempty"`
Content *Content `json:"content,omitempty" mf2:"content"` Content *Content `json:"content,omitempty"`
Latitude string `json:"latitude,omitempty" mf2:"latitude"` Latitude string `json:"latitude,omitempty"`
Longitude string `json:"longitude,omitempty" mf2:"longitude"` Longitude string `json:"longitude,omitempty"`
Checkin *Card `json:"checkin,omitempty" mf2:"checkin"` Checkin *Card `json:"checkin,omitempty"`
Refs map[string]Item `json:"refs,omitempty"` Refs map[string]Item `json:"refs,omitempty"`
ID string `json:"_id,omitempty"` ID string `json:"_id,omitempty"`
Read bool `json:"_is_read"` Read bool `json:"_is_read"`