Page MenuHomeMusing Studio

No OneTemporary

diff --git a/activitypub.go b/activitypub.go
new file mode 100644
index 0000000..38cfe67
--- /dev/null
+++ b/activitypub.go
@@ -0,0 +1,635 @@
+package writefreely
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "database/sql"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "github.com/go-sql-driver/mysql"
+ "github.com/gorilla/mux"
+ "github.com/writeas/activity/streams"
+ "github.com/writeas/httpsig"
+ "github.com/writeas/impart"
+ "github.com/writeas/web-core/activitypub"
+ "github.com/writeas/web-core/activitystreams"
+ "github.com/writeas/web-core/log"
+ "io/ioutil"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "strconv"
+ "time"
+)
+
+const (
+ // TODO: delete. don't use this!
+ apCustomHandleDefault = "blog"
+)
+
+type RemoteUser struct {
+ ID int64
+ ActorID string
+ Inbox string
+ SharedInbox string
+}
+
+func (ru *RemoteUser) AsPerson() *activitystreams.Person {
+ return &activitystreams.Person{
+ BaseObject: activitystreams.BaseObject{
+ Type: "Person",
+ Context: []interface{}{
+ activitystreams.Namespace,
+ },
+ ID: ru.ActorID,
+ },
+ Inbox: ru.Inbox,
+ Endpoints: activitystreams.Endpoints{
+ SharedInbox: ru.SharedInbox,
+ },
+ }
+}
+
+func handleFetchCollectionActivities(app *app, w http.ResponseWriter, r *http.Request) error {
+ w.Header().Set("Server", serverSoftware)
+
+ vars := mux.Vars(r)
+ alias := vars["alias"]
+
+ // TODO: enforce visibility
+ // Get base Collection data
+ var c *Collection
+ var err error
+ if app.cfg.App.SingleUser {
+ c, err = app.db.GetCollectionByID(1)
+ } else {
+ c, err = app.db.GetCollection(alias)
+ }
+ if err != nil {
+ return err
+ }
+
+ p := c.PersonObject()
+
+ return impart.RenderActivityJSON(w, p, http.StatusOK)
+}
+
+func handleFetchCollectionOutbox(app *app, w http.ResponseWriter, r *http.Request) error {
+ w.Header().Set("Server", serverSoftware)
+
+ vars := mux.Vars(r)
+ alias := vars["alias"]
+
+ // TODO: enforce visibility
+ // Get base Collection data
+ var c *Collection
+ var err error
+ if app.cfg.App.SingleUser {
+ c, err = app.db.GetCollectionByID(1)
+ } else {
+ c, err = app.db.GetCollection(alias)
+ }
+ if err != nil {
+ return err
+ }
+
+ if app.cfg.App.SingleUser {
+ if alias != c.Alias {
+ return ErrCollectionNotFound
+ }
+ }
+
+ res := &CollectionObj{Collection: *c}
+ app.db.GetPostsCount(res, false)
+ accountRoot := c.FederatedAccount()
+
+ page := r.FormValue("page")
+ p, err := strconv.Atoi(page)
+ if err != nil || p < 1 {
+ // Return outbox
+ oc := activitystreams.NewOrderedCollection(accountRoot, "outbox", res.TotalPosts)
+ return impart.RenderActivityJSON(w, oc, http.StatusOK)
+ }
+
+ // Return outbox page
+ ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "outbox", res.TotalPosts, p)
+ ocp.OrderedItems = []interface{}{}
+
+ posts, err := app.db.GetPosts(c, p, false)
+ for _, pp := range *posts {
+ pp.Collection = res
+ o := pp.ActivityObject()
+ a := activitystreams.NewCreateActivity(o)
+ ocp.OrderedItems = append(ocp.OrderedItems, *a)
+ }
+
+ return impart.RenderActivityJSON(w, ocp, http.StatusOK)
+}
+
+func handleFetchCollectionFollowers(app *app, w http.ResponseWriter, r *http.Request) error {
+ w.Header().Set("Server", serverSoftware)
+
+ vars := mux.Vars(r)
+ alias := vars["alias"]
+
+ // TODO: enforce visibility
+ // Get base Collection data
+ var c *Collection
+ var err error
+ if app.cfg.App.SingleUser {
+ c, err = app.db.GetCollectionByID(1)
+ } else {
+ c, err = app.db.GetCollection(alias)
+ }
+ if err != nil {
+ return err
+ }
+
+ accountRoot := c.FederatedAccount()
+
+ folls, err := app.db.GetAPFollowers(c)
+ if err != nil {
+ return err
+ }
+
+ page := r.FormValue("page")
+ p, err := strconv.Atoi(page)
+ if err != nil || p < 1 {
+ // Return outbox
+ oc := activitystreams.NewOrderedCollection(accountRoot, "followers", len(*folls))
+ return impart.RenderActivityJSON(w, oc, http.StatusOK)
+ }
+
+ // Return outbox page
+ ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "followers", len(*folls), p)
+ ocp.OrderedItems = []interface{}{}
+ /*
+ for _, f := range *folls {
+ ocp.OrderedItems = append(ocp.OrderedItems, f.ActorID)
+ }
+ */
+ return impart.RenderActivityJSON(w, ocp, http.StatusOK)
+}
+
+func handleFetchCollectionFollowing(app *app, w http.ResponseWriter, r *http.Request) error {
+ w.Header().Set("Server", serverSoftware)
+
+ vars := mux.Vars(r)
+ alias := vars["alias"]
+
+ // TODO: enforce visibility
+ // Get base Collection data
+ var c *Collection
+ var err error
+ if app.cfg.App.SingleUser {
+ c, err = app.db.GetCollectionByID(1)
+ } else {
+ c, err = app.db.GetCollection(alias)
+ }
+ if err != nil {
+ return err
+ }
+
+ accountRoot := c.FederatedAccount()
+
+ page := r.FormValue("page")
+ p, err := strconv.Atoi(page)
+ if err != nil || p < 1 {
+ // Return outbox
+ oc := activitystreams.NewOrderedCollection(accountRoot, "following", 0)
+ return impart.RenderActivityJSON(w, oc, http.StatusOK)
+ }
+
+ // Return outbox page
+ ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "following", 0, p)
+ ocp.OrderedItems = []interface{}{}
+ return impart.RenderActivityJSON(w, ocp, http.StatusOK)
+}
+
+func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request) error {
+ w.Header().Set("Server", serverSoftware)
+
+ vars := mux.Vars(r)
+ alias := vars["alias"]
+ var c *Collection
+ var err error
+ if app.cfg.App.SingleUser {
+ c, err = app.db.GetCollectionByID(1)
+ } else {
+ c, err = app.db.GetCollection(alias)
+ }
+ if err != nil {
+ // TODO: return Reject?
+ return err
+ }
+
+ if debugging {
+ dump, err := httputil.DumpRequest(r, true)
+ if err != nil {
+ log.Error("Can't dump: %v", err)
+ } else {
+ log.Info("Rec'd! %q", dump)
+ }
+ }
+
+ var m map[string]interface{}
+ if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
+ return err
+ }
+
+ a := streams.NewAccept()
+ p := c.PersonObject()
+ var to *url.URL
+ var isFollow, isUnfollow bool
+ fullActor := &activitystreams.Person{}
+ var remoteUser *RemoteUser
+
+ res := &streams.Resolver{
+ FollowCallback: func(f *streams.Follow) error {
+ isFollow = true
+
+ // 1) Use the Follow concrete type here
+ // 2) Errors are propagated to res.Deserialize call below
+ m["@context"] = []string{activitystreams.Namespace}
+ b, _ := json.Marshal(m)
+ log.Info("Follow: %s", b)
+
+ a.AppendObject(f.Raw())
+ _, to = f.GetActor(0)
+ obj := f.Raw().GetObjectIRI(0)
+ a.AppendActor(obj)
+
+ // First get actor information
+ if to == nil {
+ return fmt.Errorf("No valid `to` string")
+ }
+ fullActor, remoteUser, err = getActor(app, to.String())
+ if err != nil {
+ return err
+ }
+ return impart.RenderActivityJSON(w, m, http.StatusOK)
+ },
+ UndoCallback: func(u *streams.Undo) error {
+ isUnfollow = true
+
+ m["@context"] = []string{activitystreams.Namespace}
+ b, _ := json.Marshal(m)
+ log.Info("Undo: %s", b)
+
+ a.AppendObject(u.Raw())
+ _, to = u.GetActor(0)
+ // TODO: get actor from object.object, not object
+ obj := u.Raw().GetObjectIRI(0)
+ a.AppendActor(obj)
+ if to != nil {
+ // Populate fullActor from DB?
+ remoteUser, err = getRemoteUser(app, to.String())
+ if err != nil {
+ if iErr, ok := err.(*impart.HTTPError); ok {
+ if iErr.Status == http.StatusNotFound {
+ log.Error("No remoteuser info for Undo event!")
+ }
+ }
+ return err
+ } else {
+ fullActor = remoteUser.AsPerson()
+ }
+ } else {
+ log.Error("No to on Undo!")
+ }
+ return impart.RenderActivityJSON(w, m, http.StatusOK)
+ },
+ }
+ if err := res.Deserialize(m); err != nil {
+ // 3) Any errors from #2 can be handled, or the payload is an unknown type.
+ log.Error("Unable to resolve Follow: %v", err)
+ if debugging {
+ log.Error("Map: %s", m)
+ }
+ return err
+ }
+
+ go func() {
+ time.Sleep(2 * time.Second)
+ am, err := a.Serialize()
+ if err != nil {
+ log.Error("Unable to serialize Accept: %v", err)
+ return
+ }
+ am["@context"] = []string{activitystreams.Namespace}
+
+ if to == nil {
+ log.Error("No to! %v", err)
+ return
+ }
+ err = makeActivityPost(p, fullActor.Inbox, am)
+ if err != nil {
+ log.Error("Unable to make activity POST: %v", err)
+ return
+ }
+
+ if isFollow {
+ t, err := app.db.Begin()
+ if err != nil {
+ log.Error("Unable to start transaction: %v", err)
+ return
+ }
+
+ var followerID int64
+
+ if remoteUser != nil {
+ followerID = remoteUser.ID
+ } else {
+ // Add follower locally, since it wasn't found before
+ res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox)
+ if err != nil {
+ if mysqlErr, ok := err.(*mysql.MySQLError); ok {
+ if mysqlErr.Number != mySQLErrDuplicateKey {
+ t.Rollback()
+ log.Error("Couldn't add new remoteuser in DB: %v\n", err)
+ return
+ }
+ } else {
+ t.Rollback()
+ log.Error("Couldn't add new remoteuser in DB: %v\n", err)
+ return
+ }
+ }
+
+ followerID, err = res.LastInsertId()
+ if err != nil {
+ t.Rollback()
+ log.Error("no lastinsertid for followers, rolling back: %v", err)
+ return
+ }
+
+ // Add in key
+ _, err = t.Exec("INSERT INTO remoteuserkeys (id, remote_user_id, public_key) VALUES (?, ?, ?)", fullActor.PublicKey.ID, followerID, fullActor.PublicKey.PublicKeyPEM)
+ if err != nil {
+ if mysqlErr, ok := err.(*mysql.MySQLError); ok {
+ if mysqlErr.Number != mySQLErrDuplicateKey {
+ t.Rollback()
+ log.Error("Couldn't add follower keys in DB: %v\n", err)
+ return
+ }
+ } else {
+ t.Rollback()
+ log.Error("Couldn't add follower keys in DB: %v\n", err)
+ return
+ }
+ }
+ }
+
+ // Add follow
+ _, err = t.Exec("INSERT INTO remotefollows (collection_id, remote_user_id, created) VALUES (?, ?, NOW())", c.ID, followerID)
+ if err != nil {
+ if mysqlErr, ok := err.(*mysql.MySQLError); ok {
+ if mysqlErr.Number != mySQLErrDuplicateKey {
+ t.Rollback()
+ log.Error("Couldn't add follower in DB: %v\n", err)
+ return
+ }
+ } else {
+ t.Rollback()
+ log.Error("Couldn't add follower in DB: %v\n", err)
+ return
+ }
+ }
+
+ err = t.Commit()
+ if err != nil {
+ t.Rollback()
+ log.Error("Rolling back after Commit(): %v\n", err)
+ return
+ }
+ } else if isUnfollow {
+ // Remove follower locally
+ _, err = app.db.Exec("DELETE FROM remotefollows WHERE collection_id = ? AND remote_user_id = (SELECT id FROM remoteusers WHERE actor_id = ?)", c.ID, to.String())
+ if err != nil {
+ log.Error("Couldn't remove follower from DB: %v\n", err)
+ }
+ }
+ }()
+
+ return nil
+}
+
+func makeActivityPost(p *activitystreams.Person, url string, m interface{}) error {
+ log.Info("POST %s", url)
+ b, err := json.Marshal(m)
+ if err != nil {
+ return err
+ }
+
+ r, _ := http.NewRequest("POST", url, bytes.NewBuffer(b))
+ r.Header.Add("Content-Type", "application/activity+json")
+ r.Header.Set("User-Agent", "Go ("+serverSoftware+"/"+softwareVer+"; +"+softwareURL+")")
+ h := sha256.New()
+ h.Write(b)
+ r.Header.Add("Digest", "SHA-256="+base64.StdEncoding.EncodeToString(h.Sum(nil)))
+
+ // Sign using the 'Signature' header
+ privKey, err := activitypub.DecodePrivateKey(p.GetPrivKey())
+ if err != nil {
+ return err
+ }
+ signer := httpsig.NewSigner(p.PublicKey.ID, privKey, httpsig.RSASHA256, []string{"(request-target)", "date", "host", "digest"})
+ err = signer.SignSigHeader(r)
+ if err != nil {
+ log.Error("Can't sign: %v", err)
+ }
+
+ if debugging {
+ dump, err := httputil.DumpRequestOut(r, true)
+ if err != nil {
+ log.Error("Can't dump: %v", err)
+ } else {
+ log.Info("%s", dump)
+ }
+ }
+
+ resp, err := http.DefaultClient.Do(r)
+ if resp != nil && resp.Body != nil {
+ defer resp.Body.Close()
+ }
+
+ if resp == nil {
+ log.Error("No response.")
+ return fmt.Errorf("No resonse.")
+ }
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+ if debugging {
+ log.Info("Status : %s", resp.Status)
+ log.Info("Response: %s", body)
+ }
+
+ return nil
+}
+
+func resolveIRI(url string) ([]byte, error) {
+ log.Info("GET %s", url)
+
+ r, _ := http.NewRequest("GET", url, nil)
+ r.Header.Add("Accept", "application/activity+json")
+ r.Header.Set("User-Agent", "Go ("+serverSoftware+"/"+softwareVer+"; +"+softwareURL+")")
+
+ if debugging {
+ dump, err := httputil.DumpRequestOut(r, true)
+ if err != nil {
+ log.Error("Can't dump: %v", err)
+ } else {
+ log.Info("%s", dump)
+ }
+ }
+
+ resp, err := http.DefaultClient.Do(r)
+ if resp != nil && resp.Body != nil {
+ defer resp.Body.Close()
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+ if debugging {
+ log.Info("Status : %s", resp.Status)
+ log.Info("Response: %s", body)
+ }
+
+ return body, nil
+}
+
+func deleteFederatedPost(app *app, p *PublicPost, collID int64) error {
+ if debugging {
+ log.Info("Deleting federated post!")
+ }
+ actor := p.Collection.PersonObject(collID)
+ na := p.ActivityObject()
+
+ // Add followers
+ p.Collection.ID = collID
+ followers, err := app.db.GetAPFollowers(&p.Collection.Collection)
+ if err != nil {
+ log.Error("Couldn't delete post (get followers)! %v", err)
+ return err
+ }
+
+ inboxes := map[string][]string{}
+ for _, f := range *followers {
+ if _, ok := inboxes[f.SharedInbox]; ok {
+ inboxes[f.SharedInbox] = append(inboxes[f.SharedInbox], f.ActorID)
+ } else {
+ inboxes[f.SharedInbox] = []string{f.ActorID}
+ }
+ }
+
+ for si, instFolls := range inboxes {
+ na.CC = []string{}
+ for _, f := range instFolls {
+ na.CC = append(na.CC, f)
+ }
+
+ err = makeActivityPost(actor, si, activitystreams.NewDeleteActivity(na))
+ if err != nil {
+ log.Error("Couldn't delete post! %v", err)
+ }
+ }
+ return nil
+}
+
+func federatePost(app *app, p *PublicPost, collID int64, isUpdate bool) error {
+ if debugging {
+ if isUpdate {
+ log.Info("Federating updated post!")
+ } else {
+ log.Info("Federating new post!")
+ }
+ }
+ actor := p.Collection.PersonObject(collID)
+ na := p.ActivityObject()
+
+ // Add followers
+ p.Collection.ID = collID
+ followers, err := app.db.GetAPFollowers(&p.Collection.Collection)
+ if err != nil {
+ log.Error("Couldn't post! %v", err)
+ return err
+ }
+ log.Info("Followers for %d: %+v", collID, followers)
+
+ inboxes := map[string][]string{}
+ for _, f := range *followers {
+ if _, ok := inboxes[f.SharedInbox]; ok {
+ inboxes[f.SharedInbox] = append(inboxes[f.SharedInbox], f.ActorID)
+ } else {
+ inboxes[f.SharedInbox] = []string{f.ActorID}
+ }
+ }
+
+ for si, instFolls := range inboxes {
+ na.CC = []string{}
+ for _, f := range instFolls {
+ na.CC = append(na.CC, f)
+ }
+ var activity *activitystreams.Activity
+ if isUpdate {
+ activity = activitystreams.NewUpdateActivity(na)
+ } else {
+ activity = activitystreams.NewCreateActivity(na)
+ }
+ err = makeActivityPost(actor, si, activity)
+ if err != nil {
+ log.Error("Couldn't post! %v", err)
+ }
+ }
+ return nil
+}
+
+func getRemoteUser(app *app, actorID string) (*RemoteUser, error) {
+ u := RemoteUser{ActorID: actorID}
+ err := app.db.QueryRow("SELECT id, inbox, shared_inbox FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox)
+ switch {
+ case err == sql.ErrNoRows:
+ return nil, impart.HTTPError{http.StatusNotFound, "No remote user with that ID."}
+ case err != nil:
+ log.Error("Couldn't get remote user %s: %v", actorID, err)
+ return nil, err
+ }
+
+ return &u, nil
+}
+
+func getActor(app *app, actorIRI string) (*activitystreams.Person, *RemoteUser, error) {
+ log.Info("Fetching actor %s locally", actorIRI)
+ actor := &activitystreams.Person{}
+ remoteUser, err := getRemoteUser(app, actorIRI)
+ if err != nil {
+ if iErr, ok := err.(impart.HTTPError); ok {
+ if iErr.Status == http.StatusNotFound {
+ // Fetch remote actor
+ log.Info("Not found; fetching actor %s remotely", actorIRI)
+ actorResp, err := resolveIRI(actorIRI)
+ if err != nil {
+ log.Error("Unable to get actor! %v", err)
+ return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't fetch actor."}
+ }
+ if err := json.Unmarshal(actorResp, &actor); err != nil {
+ // FIXME: Hubzilla has an object for the Actor's url: cannot unmarshal object into Go struct field Person.url of type string
+ log.Error("Unable to unmarshal actor! %v", err)
+ return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't parse actor."}
+ }
+ } else {
+ return nil, nil, err
+ }
+ } else {
+ return nil, nil, err
+ }
+ } else {
+ actor = remoteUser.AsPerson()
+ }
+ return actor, remoteUser, nil
+}
diff --git a/hostmeta.go b/hostmeta.go
new file mode 100644
index 0000000..4b0553a
--- /dev/null
+++ b/hostmeta.go
@@ -0,0 +1,19 @@
+package writefreely
+
+import (
+ "fmt"
+ "net/http"
+)
+
+func handleViewHostMeta(app *app, w http.ResponseWriter, r *http.Request) error {
+ w.Header().Set("Server", serverSoftware)
+ w.Header().Set("Content-Type", "application/xrd+xml; charset=utf-8")
+
+ meta := `<?xml version="1.0" encoding="UTF-8"?>
+<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
+ <Link rel="lrdd" type="application/xrd+xml" template="https://` + r.Host + `/.well-known/webfinger?resource={uri}"/>
+</XRD>`
+ fmt.Fprintf(w, meta)
+
+ return nil
+}
diff --git a/nodeinfo.go b/nodeinfo.go
index 3318fb4..fc36c34 100644
--- a/nodeinfo.go
+++ b/nodeinfo.go
@@ -1,87 +1,87 @@
package writefreely
import (
"fmt"
"github.com/writeas/go-nodeinfo"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
)
type nodeInfoResolver struct {
cfg *config.Config
db *datastore
}
func nodeInfoConfig(cfg *config.Config) *nodeinfo.Config {
name := cfg.App.SiteName
return &nodeinfo.Config{
BaseURL: cfg.App.Host,
InfoURL: "/api/nodeinfo",
Metadata: nodeinfo.Metadata{
NodeName: name,
NodeDescription: "Minimal, federated blogging platform.",
Private: cfg.App.Private,
Software: nodeinfo.SoftwareMeta{
HomePage: softwareURL,
GitHub: "https://github.com/writeas/writefreely",
Follow: "https://writing.exchange/@write_as",
},
},
Protocols: []nodeinfo.NodeProtocol{
nodeinfo.ProtocolActivityPub,
},
Services: nodeinfo.Services{
Inbound: []nodeinfo.NodeService{},
Outbound: []nodeinfo.NodeService{},
},
Software: nodeinfo.SoftwareInfo{
Name: serverSoftware,
Version: softwareVer,
},
}
}
func (r nodeInfoResolver) IsOpenRegistration() (bool, error) {
- return !r.cfg.App.Private, nil
+ return r.cfg.App.OpenRegistration, nil
}
func (r nodeInfoResolver) Usage() (nodeinfo.Usage, error) {
var collCount, postCount, activeHalfYear, activeMonth int
err := r.db.QueryRow(`SELECT COUNT(*) FROM collections`).Scan(&collCount)
if err != nil {
collCount = 0
}
err = r.db.QueryRow(`SELECT COUNT(*) FROM posts`).Scan(&postCount)
if err != nil {
log.Error("Unable to fetch post counts: %v", err)
}
if r.cfg.App.PublicStats {
// Display bi-yearly / monthly stats
err = r.db.QueryRow(fmt.Sprintf(`SELECT COUNT(*) FROM (
SELECT DISTINCT collection_id
FROM posts
INNER JOIN collections c
ON collection_id = c.id
WHERE collection_id IS NOT NULL
AND updated > DATE_SUB(NOW(), INTERVAL 6 MONTH)) co`, CollPublic)).Scan(&activeHalfYear)
err = r.db.QueryRow(fmt.Sprintf(`SELECT COUNT(*) FROM (
SELECT DISTINCT collection_id
FROM posts
INNER JOIN FROM collections c
ON collection_id = c.id
WHERE collection_id IS NOT NULL
AND updated > DATE_SUB(NOW(), INTERVAL 1 MONTH)) co`, CollPublic)).Scan(&activeMonth)
}
return nodeinfo.Usage{
Users: nodeinfo.UsageUsers{
Total: collCount,
ActiveHalfYear: activeHalfYear,
ActiveMonth: activeMonth,
},
LocalPosts: postCount,
}, nil
}
diff --git a/routes.go b/routes.go
index fafc4c1..0cdbae0 100644
--- a/routes.go
+++ b/routes.go
@@ -1,123 +1,136 @@
package writefreely
import (
"github.com/gorilla/mux"
"github.com/writeas/go-nodeinfo"
+ "github.com/writeas/go-webfinger"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
"net/http"
"strings"
)
func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datastore) {
hostSubroute := cfg.App.Host[strings.Index(cfg.App.Host, "://")+3:]
if cfg.App.SingleUser {
hostSubroute = "{domain}"
} else {
if strings.HasPrefix(hostSubroute, "localhost") {
hostSubroute = "localhost"
}
}
if cfg.App.SingleUser {
log.Info("Adding %s routes (single user)...", hostSubroute)
} else {
log.Info("Adding %s routes (multi-user)...", hostSubroute)
}
// Primary app routes
write := r.Host(hostSubroute).Subrouter()
+ // Federation endpoint configurations
+ wf := webfinger.Default(wfResolver{db, cfg})
+ wf.NoTLSHandler = nil
+
// Federation endpoints
+ // host-meta
+ write.HandleFunc("/.well-known/host-meta", handler.Web(handleViewHostMeta, UserLevelOptional))
+ // webfinger
+ write.HandleFunc(webfinger.WebFingerPath, handler.LogHandlerFunc(http.HandlerFunc(wf.Webfinger)))
// nodeinfo
niCfg := nodeInfoConfig(cfg)
ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{cfg, db})
write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover)))
write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo)))
// Handle logged in user sections
me := write.PathPrefix("/me").Subrouter()
me.HandleFunc("/", handler.Redirect("/me", UserLevelUser))
me.HandleFunc("/c", handler.Redirect("/me/c/", UserLevelUser)).Methods("GET")
me.HandleFunc("/c/", handler.User(viewCollections)).Methods("GET")
me.HandleFunc("/c/{collection}", handler.User(viewEditCollection)).Methods("GET")
me.HandleFunc("/c/{collection}/stats", handler.User(viewStats)).Methods("GET")
me.HandleFunc("/posts", handler.Redirect("/me/posts/", UserLevelUser)).Methods("GET")
me.HandleFunc("/posts/", handler.User(viewArticles)).Methods("GET")
me.HandleFunc("/posts/export.csv", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET")
me.HandleFunc("/posts/export.zip", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET")
me.HandleFunc("/posts/export.json", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET")
me.HandleFunc("/export", handler.User(viewExportOptions)).Methods("GET")
me.HandleFunc("/export.json", handler.Download(viewExportFull, UserLevelUser)).Methods("GET")
me.HandleFunc("/settings", handler.User(viewSettings)).Methods("GET")
me.HandleFunc("/logout", handler.Web(viewLogout, UserLevelNone)).Methods("GET")
write.HandleFunc("/api/me", handler.All(viewMeAPI)).Methods("GET")
apiMe := write.PathPrefix("/api/me/").Subrouter()
apiMe.HandleFunc("/", handler.All(viewMeAPI)).Methods("GET")
apiMe.HandleFunc("/posts", handler.UserAPI(viewMyPostsAPI)).Methods("GET")
apiMe.HandleFunc("/collections", handler.UserAPI(viewMyCollectionsAPI)).Methods("GET")
apiMe.HandleFunc("/password", handler.All(updatePassphrase)).Methods("POST")
apiMe.HandleFunc("/self", handler.All(updateSettings)).Methods("POST")
// Sign up validation
write.HandleFunc("/api/alias", handler.All(handleUsernameCheck)).Methods("POST")
// Handle collections
write.HandleFunc("/api/collections", handler.All(newCollection)).Methods("POST")
apiColls := write.PathPrefix("/api/collections/").Subrouter()
apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.All(fetchCollection)).Methods("GET")
apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.All(existingCollection)).Methods("POST", "DELETE")
apiColls.HandleFunc("/{alias}/posts", handler.All(fetchCollectionPosts)).Methods("GET")
apiColls.HandleFunc("/{alias}/posts", handler.All(newPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/posts/{post}", handler.All(fetchPost)).Methods("GET")
apiColls.HandleFunc("/{alias}/posts/{post:[a-zA-Z0-9]{10}}", handler.All(existingPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/posts/{post}/{property}", handler.All(fetchPostProperty)).Methods("GET")
apiColls.HandleFunc("/{alias}/collect", handler.All(addPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/pin", handler.All(pinPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/unpin", handler.All(pinPost)).Methods("POST")
+ apiColls.HandleFunc("/{alias}/inbox", handler.All(handleFetchCollectionInbox)).Methods("POST")
+ apiColls.HandleFunc("/{alias}/outbox", handler.All(handleFetchCollectionOutbox)).Methods("GET")
+ apiColls.HandleFunc("/{alias}/following", handler.All(handleFetchCollectionFollowing)).Methods("GET")
+ apiColls.HandleFunc("/{alias}/followers", handler.All(handleFetchCollectionFollowers)).Methods("GET")
// Handle posts
write.HandleFunc("/api/posts", handler.All(newPost)).Methods("POST")
posts := write.PathPrefix("/api/posts/").Subrouter()
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}", handler.All(fetchPost)).Methods("GET")
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}", handler.All(existingPost)).Methods("POST", "PUT")
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}", handler.All(deletePost)).Methods("DELETE")
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}/{property}", handler.All(fetchPostProperty)).Methods("GET")
posts.HandleFunc("/claim", handler.All(addPost)).Methods("POST")
posts.HandleFunc("/disperse", handler.All(dispersePost)).Methods("POST")
if cfg.App.SingleUser {
write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET")
} else {
write.HandleFunc("/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET")
}
// All the existing stuff
write.HandleFunc("/{action}/edit", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET")
write.HandleFunc("/{action}/meta", handler.Web(handleViewMeta, UserLevelOptional)).Methods("GET")
// Collections
if cfg.App.SingleUser {
RouteCollections(handler, write.PathPrefix("/").Subrouter())
} else {
write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelOptional))
write.HandleFunc("/{collection}/", handler.Web(handleViewCollection, UserLevelOptional))
RouteCollections(handler, write.PathPrefix("/{prefix:[@~$!\\-+]?}{collection}").Subrouter())
// Posts
write.HandleFunc("/{post}", handler.Web(handleViewPost, UserLevelOptional))
}
write.HandleFunc("/", handler.Web(handleViewHome, UserLevelOptional))
}
func RouteCollections(handler *Handler, r *mux.Router) {
r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelOptional))
r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelOptional))
r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelOptional))
r.HandleFunc("/tags/{tag}", handler.Web(handleViewCollectionTag, UserLevelOptional))
r.HandleFunc("/sitemap.xml", handler.All(handleViewSitemap))
r.HandleFunc("/feed/", handler.All(ViewFeed))
r.HandleFunc("/{slug}", handler.Web(viewCollectionPost, UserLevelOptional))
r.HandleFunc("/{slug}/edit", handler.Web(handleViewPad, UserLevelUser))
r.HandleFunc("/{slug}/edit/meta", handler.Web(handleViewMeta, UserLevelUser))
r.HandleFunc("/{slug}/", handler.Web(handleCollectionPostRedirect, UserLevelOptional)).Methods("GET")
}
diff --git a/static/img/avatars/a.png b/static/img/avatars/a.png
new file mode 100644
index 0000000..e3ce790
Binary files /dev/null and b/static/img/avatars/a.png differ
diff --git a/static/img/avatars/b.png b/static/img/avatars/b.png
new file mode 100644
index 0000000..73cfd74
Binary files /dev/null and b/static/img/avatars/b.png differ
diff --git a/static/img/avatars/c.png b/static/img/avatars/c.png
new file mode 100644
index 0000000..708d369
Binary files /dev/null and b/static/img/avatars/c.png differ
diff --git a/static/img/avatars/d.png b/static/img/avatars/d.png
new file mode 100644
index 0000000..4e0ebca
Binary files /dev/null and b/static/img/avatars/d.png differ
diff --git a/static/img/avatars/e.png b/static/img/avatars/e.png
new file mode 100644
index 0000000..afb1d8b
Binary files /dev/null and b/static/img/avatars/e.png differ
diff --git a/static/img/avatars/f.png b/static/img/avatars/f.png
new file mode 100644
index 0000000..7cef28f
Binary files /dev/null and b/static/img/avatars/f.png differ
diff --git a/static/img/avatars/g.png b/static/img/avatars/g.png
new file mode 100644
index 0000000..069c7fe
Binary files /dev/null and b/static/img/avatars/g.png differ
diff --git a/static/img/avatars/h.png b/static/img/avatars/h.png
new file mode 100644
index 0000000..1d583df
Binary files /dev/null and b/static/img/avatars/h.png differ
diff --git a/static/img/avatars/i.png b/static/img/avatars/i.png
new file mode 100644
index 0000000..942cfcc
Binary files /dev/null and b/static/img/avatars/i.png differ
diff --git a/static/img/avatars/j.png b/static/img/avatars/j.png
new file mode 100644
index 0000000..8c47592
Binary files /dev/null and b/static/img/avatars/j.png differ
diff --git a/static/img/avatars/k.png b/static/img/avatars/k.png
new file mode 100644
index 0000000..7b8684d
Binary files /dev/null and b/static/img/avatars/k.png differ
diff --git a/static/img/avatars/l.png b/static/img/avatars/l.png
new file mode 100644
index 0000000..bdd746c
Binary files /dev/null and b/static/img/avatars/l.png differ
diff --git a/static/img/avatars/m.png b/static/img/avatars/m.png
new file mode 100644
index 0000000..e2a4ad4
Binary files /dev/null and b/static/img/avatars/m.png differ
diff --git a/static/img/avatars/n.png b/static/img/avatars/n.png
new file mode 100644
index 0000000..8bc7813
Binary files /dev/null and b/static/img/avatars/n.png differ
diff --git a/static/img/avatars/o.png b/static/img/avatars/o.png
new file mode 100644
index 0000000..ada8351
Binary files /dev/null and b/static/img/avatars/o.png differ
diff --git a/static/img/avatars/p.png b/static/img/avatars/p.png
new file mode 100644
index 0000000..b0e7999
Binary files /dev/null and b/static/img/avatars/p.png differ
diff --git a/static/img/avatars/q.png b/static/img/avatars/q.png
new file mode 100644
index 0000000..6888f1b
Binary files /dev/null and b/static/img/avatars/q.png differ
diff --git a/static/img/avatars/r.png b/static/img/avatars/r.png
new file mode 100644
index 0000000..8e80584
Binary files /dev/null and b/static/img/avatars/r.png differ
diff --git a/static/img/avatars/s.png b/static/img/avatars/s.png
new file mode 100644
index 0000000..025a770
Binary files /dev/null and b/static/img/avatars/s.png differ
diff --git a/static/img/avatars/t.png b/static/img/avatars/t.png
new file mode 100644
index 0000000..a1a14ff
Binary files /dev/null and b/static/img/avatars/t.png differ
diff --git a/static/img/avatars/u.png b/static/img/avatars/u.png
new file mode 100644
index 0000000..3235390
Binary files /dev/null and b/static/img/avatars/u.png differ
diff --git a/static/img/avatars/v.png b/static/img/avatars/v.png
new file mode 100644
index 0000000..61809c1
Binary files /dev/null and b/static/img/avatars/v.png differ
diff --git a/static/img/avatars/w.png b/static/img/avatars/w.png
new file mode 100644
index 0000000..9ed04ed
Binary files /dev/null and b/static/img/avatars/w.png differ
diff --git a/static/img/avatars/x.png b/static/img/avatars/x.png
new file mode 100644
index 0000000..c6efce6
Binary files /dev/null and b/static/img/avatars/x.png differ
diff --git a/static/img/avatars/y.png b/static/img/avatars/y.png
new file mode 100644
index 0000000..2f0a568
Binary files /dev/null and b/static/img/avatars/y.png differ
diff --git a/static/img/avatars/z.png b/static/img/avatars/z.png
new file mode 100644
index 0000000..f904e15
Binary files /dev/null and b/static/img/avatars/z.png differ
diff --git a/webfinger.go b/webfinger.go
new file mode 100644
index 0000000..c1aa4f6
--- /dev/null
+++ b/webfinger.go
@@ -0,0 +1,71 @@
+package writefreely
+
+import (
+ "github.com/writeas/go-webfinger"
+ "github.com/writeas/impart"
+ "github.com/writeas/web-core/log"
+ "github.com/writeas/writefreely/config"
+ "net/http"
+)
+
+type wfResolver struct {
+ db *datastore
+ cfg *config.Config
+}
+
+var wfUserNotFoundErr = impart.HTTPError{http.StatusNotFound, "User not found."}
+
+func (wfr wfResolver) FindUser(username string, host, requestHost string, r []webfinger.Rel) (*webfinger.Resource, error) {
+ var c *Collection
+ var err error
+ if wfr.cfg.App.SingleUser {
+ c, err = wfr.db.GetCollectionByID(1)
+ } else {
+ c, err = wfr.db.GetCollection(username)
+ }
+ if err != nil {
+ log.Error("Unable to get blog: %v", err)
+ return nil, err
+ }
+ if wfr.cfg.App.SingleUser {
+ // Ensure handle matches user-chosen one on single-user blogs
+ if username != c.Alias {
+ log.Info("Username '%s' is not handle '%s'", username, c.Alias)
+ return nil, wfUserNotFoundErr
+ }
+ }
+ // Only return information if site has federation enabled.
+ // TODO: enable two levels of federation? Unlisted or Public on timelines?
+ if !wfr.cfg.App.Federation {
+ return nil, wfUserNotFoundErr
+ }
+
+ res := webfinger.Resource{
+ Subject: "acct:" + username + "@" + host,
+ Aliases: []string{
+ c.CanonicalURL(),
+ c.FederatedAccount(),
+ },
+ Links: []webfinger.Link{
+ {
+ HRef: c.CanonicalURL(),
+ Type: "text/html",
+ Rel: "https://webfinger.net/rel/profile-page",
+ },
+ {
+ HRef: c.FederatedAccount(),
+ Type: "application/activity+json",
+ Rel: "self",
+ },
+ },
+ }
+ return &res, nil
+}
+
+func (wfr wfResolver) DummyUser(username string, hostname string, r []webfinger.Rel) (*webfinger.Resource, error) {
+ return nil, wfUserNotFoundErr
+}
+
+func (wfr wfResolver) IsNotFoundError(err error) bool {
+ return err == wfUserNotFoundErr
+}

File Metadata

Mime Type
text/x-diff
Expires
Sun, May 17, 1:54 AM (14 h, 31 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3732189

Event Timeline