diff --git a/activitypub.go b/activitypub.go index bbcd3bb..0308b6c 100644 --- a/activitypub.go +++ b/activitypub.go @@ -530,10 +530,14 @@ func deleteFederatedPost(app *App, p *PublicPost, collID int64) error { inboxes := map[string][]string{} for _, f := range *followers { - if _, ok := inboxes[f.SharedInbox]; ok { - inboxes[f.SharedInbox] = append(inboxes[f.SharedInbox], f.ActorID) + inbox := f.SharedInbox + if inbox == "" { + inbox = f.Inbox + } + if _, ok := inboxes[inbox]; ok { + inboxes[inbox] = append(inboxes[inbox], f.ActorID) } else { - inboxes[f.SharedInbox] = []string{f.ActorID} + inboxes[inbox] = []string{f.ActorID} } } @@ -573,10 +577,14 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error { inboxes := map[string][]string{} for _, f := range *followers { - if _, ok := inboxes[f.SharedInbox]; ok { - inboxes[f.SharedInbox] = append(inboxes[f.SharedInbox], f.ActorID) + inbox := f.SharedInbox + if inbox == "" { + inbox = f.Inbox + } + if _, ok := inboxes[inbox]; ok { + inboxes[inbox] = append(inboxes[inbox], f.ActorID) } else { - inboxes[f.SharedInbox] = []string{f.ActorID} + inboxes[inbox] = []string{f.ActorID} } } @@ -629,8 +637,7 @@ func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser, 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 + if err := unmarshalActor(actorResp, actor); err != nil { log.Error("Unable to unmarshal actor! %v", err) return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't parse actor."} } @@ -645,3 +652,48 @@ func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser, } return actor, remoteUser, nil } + +// unmarshal actor normalizes the actor response to conform to +// the type Person from github.com/writeas/web-core/activitysteams +// +// some implementations return different context field types +// this converts any non-slice contexts into a slice +func unmarshalActor(actorResp []byte, actor *activitystreams.Person) error { + // FIXME: Hubzilla has an object for the Actor's url: cannot unmarshal object into Go struct field Person.url of type string + + // flexActor overrides the Context field to allow + // all valid representations during unmarshal + flexActor := struct { + activitystreams.Person + Context json.RawMessage `json:"@context,omitempty"` + }{} + if err := json.Unmarshal(actorResp, &flexActor); err != nil { + return err + } + + actor.Endpoints = flexActor.Endpoints + actor.Followers = flexActor.Followers + actor.Following = flexActor.Following + actor.ID = flexActor.ID + actor.Icon = flexActor.Icon + actor.Inbox = flexActor.Inbox + actor.Name = flexActor.Name + actor.Outbox = flexActor.Outbox + actor.PreferredUsername = flexActor.PreferredUsername + actor.PublicKey = flexActor.PublicKey + actor.Summary = flexActor.Summary + actor.Type = flexActor.Type + actor.URL = flexActor.URL + + func(val interface{}) { + switch val.(type) { + case []interface{}: + // already a slice, do nothing + actor.Context = val.([]interface{}) + default: + actor.Context = []interface{}{val} + } + }(flexActor.Context) + + return nil +} diff --git a/activitypub_test.go b/activitypub_test.go new file mode 100644 index 0000000..7a1a89a --- /dev/null +++ b/activitypub_test.go @@ -0,0 +1,31 @@ +package writefreely + +import ( + "testing" + + "github.com/writeas/web-core/activitystreams" +) + +var actorTestTable = []struct { + Name string + Resp []byte +}{ + { + "Context as a string", + []byte(`{"@context":"https://www.w3.org/ns/activitystreams"}`), + }, + { + "Context as a list", + []byte(`{"@context":["one string", "two strings"]}`), + }, +} + +func TestUnmarshalActor(t *testing.T) { + for _, tc := range actorTestTable { + actor := activitystreams.Person{} + err := unmarshalActor(tc.Resp, &actor) + if err != nil { + t.Errorf("%s failed with error %s", tc.Name, err) + } + } +} diff --git a/export.go b/export.go index 77b295f..47a2603 100644 --- a/export.go +++ b/export.go @@ -14,9 +14,10 @@ import ( "archive/zip" "bytes" "encoding/csv" - "github.com/writeas/web-core/log" "strings" "time" + + "github.com/writeas/web-core/log" ) func exportPostsCSV(u *User, posts *[]PublicPost) []byte { @@ -37,7 +38,7 @@ func exportPostsCSV(u *User, posts *[]PublicPost) []byte { w := csv.NewWriter(&b) w.WriteAll(r) // calls Flush internally if err := w.Error(); err != nil { - log.Info("error writing csv:", err) + log.Info("error writing csv: %v", err) } return b.Bytes() diff --git a/less/core.less b/less/core.less index a25d867..118acd8 100644 --- a/less/core.less +++ b/less/core.less @@ -252,6 +252,8 @@ body { margin-bottom: 0.25em; &+time { display: block; + margin-top: 0.25em; + margin-bottom: 0.25em; } } time { @@ -604,6 +606,9 @@ body#collection article, body#subpage article { padding-top: 0; padding-bottom: 0; .book { + h2 { + font-size: 1.4em; + } a.hidden.action { color: #666; float: right; diff --git a/posts.go b/posts.go index 8156748..35cb6b9 100644 --- a/posts.go +++ b/posts.go @@ -14,6 +14,12 @@ import ( "database/sql" "encoding/json" "fmt" + "html/template" + "net/http" + "regexp" + "strings" + "time" + "github.com/gorilla/mux" "github.com/guregu/null" "github.com/guregu/null/zero" @@ -31,11 +37,6 @@ import ( "github.com/writeas/web-core/tags" "github.com/writeas/writefreely/page" "github.com/writeas/writefreely/parse" - "html/template" - "net/http" - "regexp" - "strings" - "time" ) const ( @@ -67,7 +68,8 @@ type ( } AuthenticatedPost struct { - ID string `json:"id" schema:"id"` + ID string `json:"id" schema:"id"` + Web bool `json:"web" schema:"web"` *SubmittedPost } @@ -623,6 +625,10 @@ func existingPost(app *App, w http.ResponseWriter, r *http.Request) error { } } + if p.Web { + p.IsRTL.Valid = true + } + if p.SubmittedPost == nil { return ErrPostNoUpdatableVals } @@ -732,7 +738,24 @@ func deletePost(app *App, w http.ResponseWriter, r *http.Request) error { var collID sql.NullInt64 var coll *Collection var pp *PublicPost - if accessToken != "" || u != nil { + if editToken != "" { + // TODO: SELECT owner_id, as well, and return appropriate error if NULL instead of running two queries + var dummy int64 + err = app.db.QueryRow("SELECT 1 FROM posts WHERE id = ?", friendlyID).Scan(&dummy) + switch { + case err == sql.ErrNoRows: + return impart.HTTPError{http.StatusNotFound, "Post not found."} + } + err = app.db.QueryRow("SELECT 1 FROM posts WHERE id = ? AND owner_id IS NULL", friendlyID).Scan(&dummy) + switch { + case err == sql.ErrNoRows: + // Post already has an owner. This could provide a bad experience + // for the user, but it's more important to ensure data isn't lost + // unexpectedly. So prevent deletion via token. + return impart.HTTPError{http.StatusConflict, "This post belongs to some user (hopefully yours). Please log in and delete it from that user's account."} + } + res, err = app.db.Exec("DELETE FROM posts WHERE id = ? AND modify_token = ? AND owner_id IS NULL", friendlyID, editToken) + } else if accessToken != "" || u != nil { // Caller provided some way to authenticate; assume caller expects the // post to be deleted based on a specific post owner, thus we should // return corresponding errors. @@ -780,26 +803,7 @@ func deletePost(app *App, w http.ResponseWriter, r *http.Request) error { res, err = t.Exec("DELETE FROM posts WHERE id = ? AND owner_id = ?", friendlyID, ownerID) } } else { - if editToken == "" { - return impart.HTTPError{http.StatusBadRequest, "No authenticated user or post token given."} - } - - // TODO: SELECT owner_id, as well, and return appropriate error if NULL instead of running two queries - var dummy int64 - err = app.db.QueryRow("SELECT 1 FROM posts WHERE id = ?", friendlyID).Scan(&dummy) - switch { - case err == sql.ErrNoRows: - return impart.HTTPError{http.StatusNotFound, "Post not found."} - } - err = app.db.QueryRow("SELECT 1 FROM posts WHERE id = ? AND owner_id IS NULL", friendlyID).Scan(&dummy) - switch { - case err == sql.ErrNoRows: - // Post already has an owner. This could provide a bad experience - // for the user, but it's more important to ensure data isn't lost - // unexpectedly. So prevent deletion via token. - return impart.HTTPError{http.StatusConflict, "This post belongs to some user (hopefully yours). Please log in and delete it from that user's account."} - } - res, err = app.db.Exec("DELETE FROM posts WHERE id = ? AND modify_token = ? AND owner_id IS NULL", friendlyID, editToken) + return impart.HTTPError{http.StatusBadRequest, "No authenticated user or post token given."} } if err != nil { return err diff --git a/templates/edit-meta.tmpl b/templates/edit-meta.tmpl index 5d2bf1a..108c552 100644 --- a/templates/edit-meta.tmpl +++ b/templates/edit-meta.tmpl @@ -263,6 +263,7 @@
 
+