Merge pull request #195 from writeas/activitypub-mentions
Send out ActivityPub mentions Closes T627
This commit is contained in:
commit
9be05ef32e
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||||
*
|
*
|
||||||
* This file is part of WriteFreely.
|
* This file is part of WriteFreely.
|
||||||
*
|
*
|
||||||
|
@ -46,6 +46,7 @@ type RemoteUser struct {
|
||||||
ActorID string
|
ActorID string
|
||||||
Inbox string
|
Inbox string
|
||||||
SharedInbox string
|
SharedInbox string
|
||||||
|
Handle string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ru *RemoteUser) AsPerson() *activitystreams.Person {
|
func (ru *RemoteUser) AsPerson() *activitystreams.Person {
|
||||||
|
@ -151,7 +152,7 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques
|
||||||
posts, err := app.db.GetPosts(app.cfg, c, p, false, true, false)
|
posts, err := app.db.GetPosts(app.cfg, c, p, false, true, false)
|
||||||
for _, pp := range *posts {
|
for _, pp := range *posts {
|
||||||
pp.Collection = res
|
pp.Collection = res
|
||||||
o := pp.ActivityObject(app.cfg)
|
o := pp.ActivityObject(app)
|
||||||
a := activitystreams.NewCreateActivity(o)
|
a := activitystreams.NewCreateActivity(o)
|
||||||
ocp.OrderedItems = append(ocp.OrderedItems, *a)
|
ocp.OrderedItems = append(ocp.OrderedItems, *a)
|
||||||
}
|
}
|
||||||
|
@ -570,7 +571,7 @@ func deleteFederatedPost(app *App, p *PublicPost, collID int64) error {
|
||||||
}
|
}
|
||||||
p.Collection.hostName = app.cfg.App.Host
|
p.Collection.hostName = app.cfg.App.Host
|
||||||
actor := p.Collection.PersonObject(collID)
|
actor := p.Collection.PersonObject(collID)
|
||||||
na := p.ActivityObject(app.cfg)
|
na := p.ActivityObject(app)
|
||||||
|
|
||||||
// Add followers
|
// Add followers
|
||||||
p.Collection.ID = collID
|
p.Collection.ID = collID
|
||||||
|
@ -616,7 +617,7 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
actor := p.Collection.PersonObject(collID)
|
actor := p.Collection.PersonObject(collID)
|
||||||
na := p.ActivityObject(app.cfg)
|
na := p.ActivityObject(app)
|
||||||
|
|
||||||
// Add followers
|
// Add followers
|
||||||
p.Collection.ID = collID
|
p.Collection.ID = collID
|
||||||
|
@ -634,18 +635,25 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
|
||||||
inbox = f.Inbox
|
inbox = f.Inbox
|
||||||
}
|
}
|
||||||
if _, ok := inboxes[inbox]; ok {
|
if _, ok := inboxes[inbox]; ok {
|
||||||
|
// check if we're already sending to this shared inbox
|
||||||
inboxes[inbox] = append(inboxes[inbox], f.ActorID)
|
inboxes[inbox] = append(inboxes[inbox], f.ActorID)
|
||||||
} else {
|
} else {
|
||||||
|
// add the new shared inbox to the list
|
||||||
inboxes[inbox] = []string{f.ActorID}
|
inboxes[inbox] = []string{f.ActorID}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var activity *activitystreams.Activity
|
||||||
|
// for each one of the shared inboxes
|
||||||
for si, instFolls := range inboxes {
|
for si, instFolls := range inboxes {
|
||||||
|
// add all followers from that instance
|
||||||
|
// to the CC field
|
||||||
na.CC = []string{}
|
na.CC = []string{}
|
||||||
for _, f := range instFolls {
|
for _, f := range instFolls {
|
||||||
na.CC = append(na.CC, f)
|
na.CC = append(na.CC, f)
|
||||||
}
|
}
|
||||||
var activity *activitystreams.Activity
|
// create a new "Create" activity
|
||||||
|
// with our article as object
|
||||||
if isUpdate {
|
if isUpdate {
|
||||||
activity = activitystreams.NewUpdateActivity(na)
|
activity = activitystreams.NewUpdateActivity(na)
|
||||||
} else {
|
} else {
|
||||||
|
@ -653,17 +661,42 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
|
||||||
activity.To = na.To
|
activity.To = na.To
|
||||||
activity.CC = na.CC
|
activity.CC = na.CC
|
||||||
}
|
}
|
||||||
|
// and post it to that sharedInbox
|
||||||
err = makeActivityPost(app.cfg.App.Host, actor, si, activity)
|
err = makeActivityPost(app.cfg.App.Host, actor, si, activity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Couldn't post! %v", err)
|
log.Error("Couldn't post! %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// re-create the object so that the CC list gets reset and has
|
||||||
|
// the mentioned users. This might seem wasteful but the code is
|
||||||
|
// cleaner than adding the mentioned users to CC here instead of
|
||||||
|
// in p.ActivityObject()
|
||||||
|
na = p.ActivityObject(app)
|
||||||
|
for _, tag := range na.Tag {
|
||||||
|
if tag.Type == "Mention" {
|
||||||
|
activity = activitystreams.NewCreateActivity(na)
|
||||||
|
activity.To = na.To
|
||||||
|
activity.CC = na.CC
|
||||||
|
// This here might be redundant in some cases as we might have already
|
||||||
|
// sent this to the sharedInbox of this instance above, but we need too
|
||||||
|
// much logic to catch this at the expense of the odd extra request.
|
||||||
|
// I don't believe we'd ever have too many mentions in a single post that this
|
||||||
|
// could become a burden.
|
||||||
|
remoteUser, err := getRemoteUser(app, tag.HRef)
|
||||||
|
err = makeActivityPost(app.cfg.App.Host, actor, remoteUser.Inbox, activity)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Couldn't post! %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
|
func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
|
||||||
u := RemoteUser{ActorID: actorID}
|
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)
|
err := app.db.QueryRow("SELECT id, inbox, shared_inbox, handle FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox, &u.Handle)
|
||||||
switch {
|
switch {
|
||||||
case err == sql.ErrNoRows:
|
case err == sql.ErrNoRows:
|
||||||
return nil, impart.HTTPError{http.StatusNotFound, "No remote user with that ID."}
|
return nil, impart.HTTPError{http.StatusNotFound, "No remote user with that ID."}
|
||||||
|
@ -675,6 +708,21 @@ func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
|
||||||
return &u, nil
|
return &u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getRemoteUserFromHandle retrieves the profile page of a remote user
|
||||||
|
// from the @user@server.tld handle
|
||||||
|
func getRemoteUserFromHandle(app *App, handle string) (*RemoteUser, error) {
|
||||||
|
u := RemoteUser{Handle: handle}
|
||||||
|
err := app.db.QueryRow("SELECT id, actor_id, inbox, shared_inbox FROM remoteusers WHERE handle = ?", handle).Scan(&u.ID, &u.ActorID, &u.Inbox, &u.SharedInbox)
|
||||||
|
switch {
|
||||||
|
case err == sql.ErrNoRows:
|
||||||
|
return nil, ErrRemoteUserNotFound
|
||||||
|
case err != nil:
|
||||||
|
log.Error("Couldn't get remote user %s: %v", handle, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser, error) {
|
func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser, error) {
|
||||||
log.Info("Fetching actor %s locally", actorIRI)
|
log.Info("Fetching actor %s locally", actorIRI)
|
||||||
actor := &activitystreams.Person{}
|
actor := &activitystreams.Person{}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2018 A Bunch Tell LLC.
|
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||||
*
|
*
|
||||||
* This file is part of WriteFreely.
|
* This file is part of WriteFreely.
|
||||||
*
|
*
|
||||||
|
@ -857,6 +857,19 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleViewMention(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
handle := vars["handle"]
|
||||||
|
|
||||||
|
remoteUser, err := app.db.GetProfilePageFromHandle(app, handle)
|
||||||
|
if err != nil || remoteUser == "" {
|
||||||
|
log.Error("Couldn't find user %s: %v", handle, err)
|
||||||
|
return ErrRemoteUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return impart.HTTPError{Status: http.StatusFound, Message: remoteUser}
|
||||||
|
}
|
||||||
|
|
||||||
func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) error {
|
func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
tag := vars["tag"]
|
tag := vars["tag"]
|
||||||
|
|
38
database.go
38
database.go
|
@ -22,6 +22,7 @@ import (
|
||||||
"github.com/guregu/null"
|
"github.com/guregu/null"
|
||||||
"github.com/guregu/null/zero"
|
"github.com/guregu/null/zero"
|
||||||
uuid "github.com/nu7hatch/gouuid"
|
uuid "github.com/nu7hatch/gouuid"
|
||||||
|
"github.com/writeas/activityserve"
|
||||||
"github.com/writeas/impart"
|
"github.com/writeas/impart"
|
||||||
"github.com/writeas/nerds/store"
|
"github.com/writeas/nerds/store"
|
||||||
"github.com/writeas/web-core/activitypub"
|
"github.com/writeas/web-core/activitypub"
|
||||||
|
@ -2555,3 +2556,40 @@ func handleFailedPostInsert(err error) error {
|
||||||
log.Error("Couldn't insert into posts: %v", err)
|
log.Error("Couldn't insert into posts: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *datastore) GetProfilePageFromHandle(app *App, handle string) (string, error) {
|
||||||
|
actorIRI := ""
|
||||||
|
remoteUser, err := getRemoteUserFromHandle(app, handle)
|
||||||
|
if err != nil {
|
||||||
|
// can't find using handle in the table but the table may already have this user without
|
||||||
|
// handle from a previous version
|
||||||
|
// TODO: Make this determination. We should know whether a user exists without a handle, or doesn't exist at all
|
||||||
|
actorIRI = RemoteLookup(handle)
|
||||||
|
_, errRemoteUser := getRemoteUser(app, actorIRI)
|
||||||
|
// if it exists then we need to update the handle
|
||||||
|
if errRemoteUser == nil {
|
||||||
|
_, err := app.db.Exec("UPDATE remoteusers SET handle = ? WHERE actor_id = ?", handle, actorIRI)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Can't update handle (" + handle + ") in database for user " + actorIRI)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// this probably means we don't have the user in the table so let's try to insert it
|
||||||
|
// here we need to ask the server for the inboxes
|
||||||
|
remoteActor, err := activityserve.NewRemoteActor(actorIRI)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Couldn't fetch remote actor", err)
|
||||||
|
}
|
||||||
|
if debugging {
|
||||||
|
log.Info("%s %s %s %s", actorIRI, remoteActor.GetInbox(), remoteActor.GetSharedInbox(), handle)
|
||||||
|
}
|
||||||
|
_, err = app.db.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox, handle) VALUES(?, ?, ?, ?)", actorIRI, remoteActor.GetInbox(), remoteActor.GetSharedInbox(), handle)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Can't insert remote user in database", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
actorIRI = remoteUser.ActorID
|
||||||
|
}
|
||||||
|
return actorIRI, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2018 A Bunch Tell LLC.
|
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||||
*
|
*
|
||||||
* This file is part of WriteFreely.
|
* This file is part of WriteFreely.
|
||||||
*
|
*
|
||||||
|
@ -45,8 +45,9 @@ var (
|
||||||
ErrPostUnpublished = impart.HTTPError{Status: http.StatusGone, Message: "Post unpublished by author."}
|
ErrPostUnpublished = impart.HTTPError{Status: http.StatusGone, Message: "Post unpublished by author."}
|
||||||
ErrPostFetchError = impart.HTTPError{Status: http.StatusInternalServerError, Message: "We encountered an error getting the post. The humans have been alerted."}
|
ErrPostFetchError = impart.HTTPError{Status: http.StatusInternalServerError, Message: "We encountered an error getting the post. The humans have been alerted."}
|
||||||
|
|
||||||
ErrUserNotFound = impart.HTTPError{http.StatusNotFound, "User doesn't exist."}
|
ErrUserNotFound = impart.HTTPError{http.StatusNotFound, "User doesn't exist."}
|
||||||
ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."}
|
ErrRemoteUserNotFound = impart.HTTPError{http.StatusNotFound, "Remote user not found."}
|
||||||
|
ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."}
|
||||||
|
|
||||||
ErrUserSuspended = impart.HTTPError{http.StatusForbidden, "Account is silenced."}
|
ErrUserSuspended = impart.HTTPError{http.StatusForbidden, "Account is silenced."}
|
||||||
)
|
)
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -6,11 +6,14 @@ require (
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
|
||||||
github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49 // indirect
|
github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49 // indirect
|
||||||
github.com/clbanning/mxj v1.8.4 // indirect
|
github.com/clbanning/mxj v1.8.4 // indirect
|
||||||
|
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/fatih/color v1.7.0
|
github.com/fatih/color v1.7.0
|
||||||
|
github.com/go-fed/httpsig v0.1.1-0.20190924171022-f4c36041199d // indirect
|
||||||
github.com/go-sql-driver/mysql v1.4.1
|
github.com/go-sql-driver/mysql v1.4.1
|
||||||
github.com/go-test/deep v1.0.1 // indirect
|
github.com/go-test/deep v1.0.1 // indirect
|
||||||
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect
|
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect
|
||||||
|
github.com/gologme/log v0.0.0-20181207131047-4e5d8ccb38e8 // indirect
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
||||||
github.com/gorilla/feeds v1.1.0
|
github.com/gorilla/feeds v1.1.0
|
||||||
github.com/gorilla/mux v1.7.0
|
github.com/gorilla/mux v1.7.0
|
||||||
|
@ -36,6 +39,7 @@ require (
|
||||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
|
||||||
github.com/stretchr/testify v1.3.0
|
github.com/stretchr/testify v1.3.0
|
||||||
github.com/writeas/activity v0.1.2
|
github.com/writeas/activity v0.1.2
|
||||||
|
github.com/writeas/activityserve v0.0.0-20191115095800-dd6d19cc8b89
|
||||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible
|
github.com/writeas/go-strip-markdown v2.0.1+incompatible
|
||||||
github.com/writeas/go-webfinger v0.0.0-20190106002315-85cf805c86d2
|
github.com/writeas/go-webfinger v0.0.0-20190106002315-85cf805c86d2
|
||||||
github.com/writeas/httpsig v1.0.0
|
github.com/writeas/httpsig v1.0.0
|
||||||
|
|
13
go.sum
13
go.sum
|
@ -25,13 +25,18 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU=
|
||||||
|
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
|
github.com/go-fed/httpsig v0.1.0 h1:6F2OxRVnNTN4OPN+Mc2jxs2WEay9/qiHT/jphlvAwIY=
|
||||||
github.com/go-fed/httpsig v0.1.0/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
github.com/go-fed/httpsig v0.1.0/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||||
|
github.com/go-fed/httpsig v0.1.1-0.20190924171022-f4c36041199d h1:+uoOvOnNDgsYbWtAij4xP6Rgir3eJGjocFPxBJETU/U=
|
||||||
|
github.com/go-fed/httpsig v0.1.1-0.20190924171022-f4c36041199d/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
|
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
|
||||||
|
@ -40,6 +45,8 @@ github.com/golang/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:tluoj9z5200j
|
||||||
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 h1:6DVPu65tee05kY0/rciBQ47ue+AnuY8KTayV6VHikIo=
|
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 h1:6DVPu65tee05kY0/rciBQ47ue+AnuY8KTayV6VHikIo=
|
||||||
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/gologme/log v0.0.0-20181207131047-4e5d8ccb38e8 h1:WD8iJ37bRNwvETMfVTusVSAi0WdXTpfNVGY2aHycNKY=
|
||||||
|
github.com/gologme/log v0.0.0-20181207131047-4e5d8ccb38e8/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U=
|
||||||
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg=
|
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg=
|
||||||
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
|
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||||
|
@ -119,6 +126,12 @@ github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 h1:vY5WqiEon0ZSTG
|
||||||
github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk=
|
github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk=
|
||||||
github.com/writeas/activity v0.1.2 h1:Y12B5lIrabfqKE7e7HFCWiXrlfXljr9tlkFm2mp7DgY=
|
github.com/writeas/activity v0.1.2 h1:Y12B5lIrabfqKE7e7HFCWiXrlfXljr9tlkFm2mp7DgY=
|
||||||
github.com/writeas/activity v0.1.2/go.mod h1:mYYgiewmEM+8tlifirK/vl6tmB2EbjYaxwb+ndUw5T0=
|
github.com/writeas/activity v0.1.2/go.mod h1:mYYgiewmEM+8tlifirK/vl6tmB2EbjYaxwb+ndUw5T0=
|
||||||
|
github.com/writeas/activityserve v0.0.0-20191008122325-5fc3b48e70c5 h1:nG84xWpxBM8YU/FJchezJqg7yZH8ImSRow6NoYtbSII=
|
||||||
|
github.com/writeas/activityserve v0.0.0-20191008122325-5fc3b48e70c5/go.mod h1:Kz62mzYsCnrFTSTSFLXFj3fGYBQOntmBWTDDq57b46A=
|
||||||
|
github.com/writeas/activityserve v0.0.0-20191011072627-3a81f7784d5b h1:rd2wX/bTqD55hxtBjAhwLcUgaQE36c70KX3NzpDAwVI=
|
||||||
|
github.com/writeas/activityserve v0.0.0-20191011072627-3a81f7784d5b/go.mod h1:Kz62mzYsCnrFTSTSFLXFj3fGYBQOntmBWTDDq57b46A=
|
||||||
|
github.com/writeas/activityserve v0.0.0-20191115095800-dd6d19cc8b89 h1:NJhzq9aTccL3SSSZMrcnYhkD6sObdY9otNZ1X6/ZKNE=
|
||||||
|
github.com/writeas/activityserve v0.0.0-20191115095800-dd6d19cc8b89/go.mod h1:Kz62mzYsCnrFTSTSFLXFj3fGYBQOntmBWTDDq57b46A=
|
||||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw=
|
github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw=
|
||||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE=
|
github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE=
|
||||||
github.com/writeas/go-webfinger v0.0.0-20190106002315-85cf805c86d2 h1:DUsp4OhdfI+e6iUqcPQlwx8QYXuUDsToTz/x82D3Zuo=
|
github.com/writeas/go-webfinger v0.0.0-20190106002315-85cf805c86d2 h1:DUsp4OhdfI+e6iUqcPQlwx8QYXuUDsToTz/x82D3Zuo=
|
||||||
|
|
|
@ -56,11 +56,12 @@ func (m *migration) Migrate(db *datastore) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var migrations = []Migration{
|
var migrations = []Migration{
|
||||||
New("support user invites", supportUserInvites), // -> V1 (v0.8.0)
|
New("support user invites", supportUserInvites), // -> V1 (v0.8.0)
|
||||||
New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0)
|
New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0)
|
||||||
New("support users suspension", supportUserStatus), // V2 -> V3 (v0.11.0)
|
New("support users suspension", supportUserStatus), // V2 -> V3 (v0.11.0)
|
||||||
New("support oauth", oauth), // V3 -> V4
|
New("support oauth", oauth), // V3 -> V4
|
||||||
New("support slack oauth", oauthSlack), // V4 -> v5
|
New("support slack oauth", oauthSlack), // V4 -> v5
|
||||||
|
New("support ActivityPub mentions", supportActivityPubMentions), // V5 -> V6 (v0.12.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentVer returns the current migration version the application is on
|
// CurrentVer returns the current migration version the application is on
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2019 A Bunch Tell LLC.
|
||||||
|
*
|
||||||
|
* This file is part of WriteFreely.
|
||||||
|
*
|
||||||
|
* WriteFreely is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License, included
|
||||||
|
* in the LICENSE file in this source code package.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
func supportActivityPubMentions(db *datastore) error {
|
||||||
|
t, err := db.Begin()
|
||||||
|
|
||||||
|
_, err = t.Exec(`ALTER TABLE remoteusers ADD COLUMN handle ` + db.typeVarChar(255) + ` DEFAULT '' NOT NULL`)
|
||||||
|
if err != nil {
|
||||||
|
t.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = t.Commit()
|
||||||
|
if err != nil {
|
||||||
|
t.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ var (
|
||||||
titleElementReg = regexp.MustCompile("</?h[1-6]>")
|
titleElementReg = regexp.MustCompile("</?h[1-6]>")
|
||||||
hashtagReg = regexp.MustCompile(`{{\[\[\|\|([^|]+)\|\|\]\]}}`)
|
hashtagReg = regexp.MustCompile(`{{\[\[\|\|([^|]+)\|\|\]\]}}`)
|
||||||
markeddownReg = regexp.MustCompile("<p>(.+)</p>")
|
markeddownReg = regexp.MustCompile("<p>(.+)</p>")
|
||||||
|
mentionReg = regexp.MustCompile(`@([A-Za-z0-9._%+-]+)(@[A-Za-z0-9.-]+\.[A-Za-z]+)\b`)
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool) {
|
func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool) {
|
||||||
|
@ -86,6 +87,8 @@ func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string, cfg *c
|
||||||
tagPrefix = "/read/t/"
|
tagPrefix = "/read/t/"
|
||||||
}
|
}
|
||||||
md = []byte(hashtagReg.ReplaceAll(md, []byte("<a href=\""+tagPrefix+"$1\" class=\"hashtag\"><span>#</span><span class=\"p-category\">$1</span></a>")))
|
md = []byte(hashtagReg.ReplaceAll(md, []byte("<a href=\""+tagPrefix+"$1\" class=\"hashtag\"><span>#</span><span class=\"p-category\">$1</span></a>")))
|
||||||
|
handlePrefix := cfg.App.Host + "/@/"
|
||||||
|
md = []byte(mentionReg.ReplaceAll(md, []byte("<a href=\""+handlePrefix+"$1$2\" class=\"u-url mention\">@<span>$1$2</span></a>")))
|
||||||
}
|
}
|
||||||
// Strip out bad HTML
|
// Strip out bad HTML
|
||||||
policy := getSanitizationPolicy()
|
policy := getSanitizationPolicy()
|
||||||
|
|
31
posts.go
31
posts.go
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||||
*
|
*
|
||||||
* This file is part of WriteFreely.
|
* This file is part of WriteFreely.
|
||||||
*
|
*
|
||||||
|
@ -35,7 +35,6 @@ import (
|
||||||
"github.com/writeas/web-core/i18n"
|
"github.com/writeas/web-core/i18n"
|
||||||
"github.com/writeas/web-core/log"
|
"github.com/writeas/web-core/log"
|
||||||
"github.com/writeas/web-core/tags"
|
"github.com/writeas/web-core/tags"
|
||||||
"github.com/writeas/writefreely/config"
|
|
||||||
"github.com/writeas/writefreely/page"
|
"github.com/writeas/writefreely/page"
|
||||||
"github.com/writeas/writefreely/parse"
|
"github.com/writeas/writefreely/parse"
|
||||||
)
|
)
|
||||||
|
@ -1091,7 +1090,7 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
p.Collection = &CollectionObj{Collection: *coll}
|
p.Collection = &CollectionObj{Collection: *coll}
|
||||||
po := p.ActivityObject(app.cfg)
|
po := p.ActivityObject(app)
|
||||||
po.Context = []interface{}{activitystreams.Namespace}
|
po.Context = []interface{}{activitystreams.Namespace}
|
||||||
setCacheControl(w, apCacheTime)
|
setCacheControl(w, apCacheTime)
|
||||||
return impart.RenderActivityJSON(w, po, http.StatusOK)
|
return impart.RenderActivityJSON(w, po, http.StatusOK)
|
||||||
|
@ -1127,7 +1126,8 @@ func (p *PublicPost) CanonicalURL(hostName string) string {
|
||||||
return p.Collection.CanonicalURL() + p.Slug.String
|
return p.Collection.CanonicalURL() + p.Slug.String
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PublicPost) ActivityObject(cfg *config.Config) *activitystreams.Object {
|
func (p *PublicPost) ActivityObject(app *App) *activitystreams.Object {
|
||||||
|
cfg := app.cfg
|
||||||
o := activitystreams.NewArticleObject()
|
o := activitystreams.NewArticleObject()
|
||||||
o.ID = p.Collection.FederatedAPIBase() + "api/posts/" + p.ID
|
o.ID = p.Collection.FederatedAPIBase() + "api/posts/" + p.ID
|
||||||
o.Published = p.Created
|
o.Published = p.Created
|
||||||
|
@ -1167,6 +1167,27 @@ func (p *PublicPost) ActivityObject(cfg *config.Config) *activitystreams.Object
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Find mentioned users
|
||||||
|
mentionedUsers := make(map[string]string)
|
||||||
|
|
||||||
|
stripper := bluemonday.StrictPolicy()
|
||||||
|
content := stripper.Sanitize(p.Content)
|
||||||
|
mentionRegex := regexp.MustCompile(`@[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]+\b`)
|
||||||
|
mentions := mentionRegex.FindAllString(content, -1)
|
||||||
|
|
||||||
|
for _, handle := range mentions {
|
||||||
|
actorIRI, err := app.db.GetProfilePageFromHandle(app, handle)
|
||||||
|
if err != nil {
|
||||||
|
log.Info("Can't find this user either in the database nor in the remote instance")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mentionedUsers[handle] = actorIRI
|
||||||
|
}
|
||||||
|
|
||||||
|
for handle, iri := range mentionedUsers {
|
||||||
|
o.CC = append(o.CC, iri)
|
||||||
|
o.Tag = append(o.Tag, activitystreams.Tag{Type: "Mention", HRef: iri, Name: handle})
|
||||||
|
}
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1433,7 +1454,7 @@ Are you sure it was ever here?`,
|
||||||
return ErrCollectionPageNotFound
|
return ErrCollectionPageNotFound
|
||||||
}
|
}
|
||||||
p.extractData()
|
p.extractData()
|
||||||
ap := p.ActivityObject(app.cfg)
|
ap := p.ActivityObject(app)
|
||||||
ap.Context = []interface{}{activitystreams.Namespace}
|
ap.Context = []interface{}{activitystreams.Namespace}
|
||||||
setCacheControl(w, apCacheTime)
|
setCacheControl(w, apCacheTime)
|
||||||
return impart.RenderActivityJSON(w, ap, http.StatusOK)
|
return impart.RenderActivityJSON(w, ap, http.StatusOK)
|
||||||
|
|
|
@ -70,6 +70,9 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
||||||
write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover)))
|
write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover)))
|
||||||
write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo)))
|
write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo)))
|
||||||
|
|
||||||
|
// handle mentions
|
||||||
|
write.HandleFunc("/@/{handle}", handler.Web(handleViewMention, UserLevelReader))
|
||||||
|
|
||||||
configureSlackOauth(handler, write, apper.App())
|
configureSlackOauth(handler, write, apper.App())
|
||||||
configureWriteAsOauth(handler, write, apper.App())
|
configureWriteAsOauth(handler, write, apper.App())
|
||||||
|
|
||||||
|
|
51
webfinger.go
51
webfinger.go
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2018 A Bunch Tell LLC.
|
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||||
*
|
*
|
||||||
* This file is part of WriteFreely.
|
* This file is part of WriteFreely.
|
||||||
*
|
*
|
||||||
|
@ -11,7 +11,10 @@
|
||||||
package writefreely
|
package writefreely
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/writeas/go-webfinger"
|
"github.com/writeas/go-webfinger"
|
||||||
"github.com/writeas/impart"
|
"github.com/writeas/impart"
|
||||||
|
@ -89,3 +92,49 @@ func (wfr wfResolver) DummyUser(username string, hostname string, r []webfinger.
|
||||||
func (wfr wfResolver) IsNotFoundError(err error) bool {
|
func (wfr wfResolver) IsNotFoundError(err error) bool {
|
||||||
return err == wfUserNotFoundErr
|
return err == wfUserNotFoundErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoteLookup looks up a user by handle at a remote server
|
||||||
|
// and returns the actor URL
|
||||||
|
func RemoteLookup(handle string) string {
|
||||||
|
handle = strings.TrimLeft(handle, "@")
|
||||||
|
// let's take the server part of the handle
|
||||||
|
parts := strings.Split(handle, "@")
|
||||||
|
resp, err := http.Get("https://" + parts[1] + "/.well-known/webfinger?resource=acct:" + handle)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error performing webfinger request", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error reading webfinger response", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var result webfinger.Resource
|
||||||
|
err = json.Unmarshal(body, &result)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unsupported webfinger response received: %v", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var href string
|
||||||
|
// iterate over webfinger links and find the one with
|
||||||
|
// a self "rel"
|
||||||
|
for _, link := range result.Links {
|
||||||
|
if link.Rel == "self" {
|
||||||
|
href = link.HRef
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we didn't find it with the above then
|
||||||
|
// try using aliases
|
||||||
|
if href == "" {
|
||||||
|
// take the last alias because mastodon has the
|
||||||
|
// https://instance.tld/@user first which
|
||||||
|
// doesn't work as an href
|
||||||
|
href = result.Aliases[len(result.Aliases)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return href
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue