mirror of https://github.com/jlelse/GoBlog
Rework activitypub using library and support replies using the comment system
This commit is contained in:
parent
e5d0d136c0
commit
e005cda096
243
activityPub.go
243
activityPub.go
|
@ -18,10 +18,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
ap "github.com/go-ap/activitypub"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-fed/httpsig"
|
"github.com/go-fed/httpsig"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/spf13/cast"
|
|
||||||
"go.goblog.app/app/pkgs/bufferpool"
|
"go.goblog.app/app/pkgs/bufferpool"
|
||||||
"go.goblog.app/app/pkgs/contenttype"
|
"go.goblog.app/app/pkgs/contenttype"
|
||||||
)
|
)
|
||||||
|
@ -168,73 +168,86 @@ func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Parse activity
|
// Parse activity
|
||||||
activity := map[string]any{}
|
limit := int64(10 * 1000 * 1000) // 10 MB
|
||||||
err = json.NewDecoder(r.Body).Decode(&activity)
|
body, err := io.ReadAll(io.LimitReader(r.Body, limit))
|
||||||
_ = r.Body.Close()
|
if err != nil {
|
||||||
|
a.serveError(w, r, "Failed to read body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
apItem, err := ap.UnmarshalJSON(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.serveError(w, r, "Failed to decode body", http.StatusBadRequest)
|
a.serveError(w, r, "Failed to decode body", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Get and check activity actor
|
// Check if it's an activity
|
||||||
activityActor, ok := activity["actor"].(string)
|
activity, err := ap.ToActivity(apItem)
|
||||||
if !ok {
|
if err != nil {
|
||||||
a.serveError(w, r, "actor in activity is no string", http.StatusBadRequest)
|
a.serveError(w, r, "No activity", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if activityActor != requestActor.ID {
|
// Check actor
|
||||||
|
activityActor := activity.Actor.GetID()
|
||||||
|
if activity.Actor == nil || (!activity.Actor.IsLink() && !activity.Actor.IsObject()) {
|
||||||
|
a.serveError(w, r, "Activity has no actor", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if activityActor != requestActor.GetID() {
|
||||||
a.serveError(w, r, "Request actor isn't activity actor", http.StatusForbidden)
|
a.serveError(w, r, "Request actor isn't activity actor", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Do
|
// Handle activity
|
||||||
switch activity["type"] {
|
switch activity.GetType() {
|
||||||
case "Follow":
|
case ap.FollowType:
|
||||||
a.apAccept(blogName, blogIri, blog, activity)
|
a.apAccept(blogName, blogIri, blog, activity)
|
||||||
case "Undo":
|
case ap.UndoType:
|
||||||
if object, ok := activity["object"].(map[string]any); ok {
|
if activity.Object.IsObject() {
|
||||||
ot := cast.ToString(object["type"])
|
objectActivity, err := ap.ToActivity(activity.Object)
|
||||||
actor := cast.ToString(object["actor"])
|
if err == nil && objectActivity.GetType() == ap.FollowType && objectActivity.Actor.GetID() == activityActor {
|
||||||
if ot == "Follow" && actor == activityActor {
|
_ = a.db.apRemoveFollower(blogName, activityActor.String())
|
||||||
_ = a.db.apRemoveFollower(blogName, activityActor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "Create":
|
case ap.CreateType, ap.UpdateType:
|
||||||
if object, ok := activity["object"].(map[string]any); ok {
|
if activity.Object.IsObject() {
|
||||||
baseUrl := cast.ToString(object["id"])
|
object, err := ap.ToObject(activity.Object)
|
||||||
if ou := cast.ToString(object["url"]); ou != "" {
|
if err == nil && (object.GetType() == ap.NoteType || object.GetType() == ap.ArticleType) {
|
||||||
baseUrl = ou
|
objectLink := object.GetID()
|
||||||
|
if replyTo := object.InReplyTo.GetID(); objectLink != "" && replyTo != "" && strings.HasPrefix(replyTo.String(), blogIri) {
|
||||||
|
target := replyTo.String()
|
||||||
|
original := objectLink.String()
|
||||||
|
name := requestActor.Name.First().Value.String()
|
||||||
|
if username := requestActor.PreferredUsername.First().String(); name == "" && username != "" {
|
||||||
|
name = username
|
||||||
}
|
}
|
||||||
if r := cast.ToString(object["inReplyTo"]); r != "" && baseUrl != "" && strings.HasPrefix(r, blogIri) {
|
website := requestActor.GetLink().String()
|
||||||
// It's an ActivityPub reply; save reply as webmention
|
if actorUrl := requestActor.URL.GetLink(); actorUrl != "" {
|
||||||
_ = a.createWebmention(baseUrl, r)
|
website = actorUrl.String()
|
||||||
} else if content := cast.ToString(object["content"]); content != "" && baseUrl != "" {
|
}
|
||||||
// May be a mention; find links to blog and save them as webmentions
|
content := object.Content.First().Value.String()
|
||||||
if links, err := allLinksFromHTMLString(content, baseUrl); err == nil {
|
a.createComment(blog, target, content, name, website, original)
|
||||||
for _, link := range links {
|
|
||||||
if strings.HasPrefix(link, a.cfg.Server.PublicAddress) {
|
|
||||||
_ = a.createWebmention(baseUrl, link)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case ap.DeleteType, ap.BlockType:
|
||||||
|
if activity.Object.GetID() == activityActor {
|
||||||
|
_ = a.db.apRemoveFollower(blogName, activityActor.String())
|
||||||
|
} else {
|
||||||
|
// Check if comment exists
|
||||||
|
exists, commentId, err := a.db.commentIdByOriginal(activity.Object.GetID().String())
|
||||||
|
if err == nil && exists {
|
||||||
|
_ = a.db.deleteComment(commentId)
|
||||||
|
_ = a.db.deleteWebmentionUUrl(activity.Object.GetID().String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "Delete", "Block":
|
case ap.AnnounceType:
|
||||||
if o := cast.ToString(activity["object"]); o == activityActor {
|
a.sendNotification(fmt.Sprintf("%s announced %s", activityActor, activity.Object.GetID()))
|
||||||
_ = a.db.apRemoveFollower(blogName, activityActor)
|
case ap.LikeType:
|
||||||
}
|
a.sendNotification(fmt.Sprintf("%s liked %s", activityActor, activity.Object.GetID()))
|
||||||
case "Like":
|
|
||||||
if o := cast.ToString(activity["object"]); o != "" && strings.HasPrefix(o, blogIri) {
|
|
||||||
a.sendNotification(fmt.Sprintf("%s liked %s", activityActor, o))
|
|
||||||
}
|
|
||||||
case "Announce":
|
|
||||||
if o := cast.ToString(activity["object"]); o != "" && strings.HasPrefix(o, blogIri) {
|
|
||||||
a.sendNotification(fmt.Sprintf("%s announced %s", activityActor, o))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Return 200
|
// Return 200
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) apVerifySignature(r *http.Request, blogIri string) (*asPerson, string, int, error) {
|
func (a *goBlog) apVerifySignature(r *http.Request, blogIri string) (*ap.Actor, string, int, error) {
|
||||||
verifier, err := httpsig.NewVerifier(r)
|
verifier, err := httpsig.NewVerifier(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Error with signature header etc.
|
// Error with signature header etc.
|
||||||
|
@ -246,7 +259,7 @@ func (a *goBlog) apVerifySignature(r *http.Request, blogIri string) (*asPerson,
|
||||||
// Actor not found or something else bad
|
// Actor not found or something else bad
|
||||||
return nil, keyID, statusCode, err
|
return nil, keyID, statusCode, err
|
||||||
}
|
}
|
||||||
if actor.PublicKey == nil || actor.PublicKey.PublicKeyPem == "" {
|
if actor.PublicKey.PublicKeyPem == "" {
|
||||||
return nil, keyID, 0, errors.New("actor has no public key")
|
return nil, keyID, 0, errors.New("actor has no public key")
|
||||||
}
|
}
|
||||||
block, _ := pem.Decode([]byte(actor.PublicKey.PublicKeyPem))
|
block, _ := pem.Decode([]byte(actor.PublicKey.PublicKeyPem))
|
||||||
|
@ -267,7 +280,7 @@ func handleWellKnownHostMeta(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = io.WriteString(w, `<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>`)
|
_, _ = io.WriteString(w, `<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>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) apGetRemoteActor(iri, ownBlogIri string) (*asPerson, int, error) {
|
func (a *goBlog) apGetRemoteActor(iri, ownBlogIri string) (*ap.Actor, int, error) {
|
||||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, iri, strings.NewReader(""))
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, iri, strings.NewReader(""))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
|
@ -292,8 +305,17 @@ func (a *goBlog) apGetRemoteActor(iri, ownBlogIri string) (*asPerson, int, error
|
||||||
if !apRequestIsSuccess(resp.StatusCode) {
|
if !apRequestIsSuccess(resp.StatusCode) {
|
||||||
return nil, resp.StatusCode, nil
|
return nil, resp.StatusCode, nil
|
||||||
}
|
}
|
||||||
actor := &asPerson{}
|
// Parse response
|
||||||
err = json.NewDecoder(resp.Body).Decode(actor)
|
limit := int64(10 * 1000 * 1000) // 10 MB
|
||||||
|
body, err := io.ReadAll(io.LimitReader(resp.Body, limit))
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
apObject, err := ap.UnmarshalJSON(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
actor, err := ap.ToActor(apObject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
@ -352,35 +374,28 @@ func (db *database) apRemoveInbox(inbox string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) apPost(p *post) {
|
func (a *goBlog) apPost(p *post) {
|
||||||
n := a.toASNote(p)
|
blogConfig := a.cfg.Blogs[p.Blog]
|
||||||
a.apSendToAllFollowers(p.Blog, map[string]any{
|
note := a.toAPNote(p)
|
||||||
"@context": []string{asContext},
|
create := ap.CreateNew(a.activityPubId(p), note)
|
||||||
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
create.Actor = a.apAPIri(blogConfig)
|
||||||
"id": a.activityPubId(p),
|
create.Published = time.Now()
|
||||||
"published": n.Published,
|
a.apSendToAllFollowers(p.Blog, create)
|
||||||
"type": "Create",
|
|
||||||
"object": n,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) apUpdate(p *post) {
|
func (a *goBlog) apUpdate(p *post) {
|
||||||
a.apSendToAllFollowers(p.Blog, map[string]any{
|
blogConfig := a.cfg.Blogs[p.Blog]
|
||||||
"@context": []string{asContext},
|
note := a.toAPNote(p)
|
||||||
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
update := ap.UpdateNew(a.activityPubId(p), note)
|
||||||
"id": a.activityPubId(p),
|
update.Actor = a.apAPIri(blogConfig)
|
||||||
"published": time.Now().Format("2006-01-02T15:04:05-07:00"),
|
update.Published = time.Now()
|
||||||
"type": "Update",
|
a.apSendToAllFollowers(p.Blog, update)
|
||||||
"object": a.toASNote(p),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) apDelete(p *post) {
|
func (a *goBlog) apDelete(p *post) {
|
||||||
a.apSendToAllFollowers(p.Blog, map[string]any{
|
blogConfig := a.cfg.Blogs[p.Blog]
|
||||||
"@context": []string{asContext},
|
delete := ap.DeleteNew(a.apNewID(blogConfig), a.activityPubId(p))
|
||||||
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
delete.Actor = a.apAPIri(blogConfig)
|
||||||
"type": "Delete",
|
a.apSendToAllFollowers(p.Blog, delete)
|
||||||
"object": a.activityPubId(p),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) apUndelete(p *post) {
|
func (a *goBlog) apUndelete(p *post) {
|
||||||
|
@ -397,55 +412,45 @@ func (a *goBlog) apUndelete(p *post) {
|
||||||
a.apPost(p)
|
a.apPost(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) apAccept(blogName, blogIri string, blog *configBlog, follow map[string]any) {
|
func (a *goBlog) apAccept(blogName, blogIri string, blog *configBlog, follow *ap.Activity) {
|
||||||
// it's a follow, write it down
|
newFollower := follow.Actor.GetID()
|
||||||
newFollower := follow["actor"].(string)
|
log.Println("New follow request:", newFollower.String())
|
||||||
log.Println("New follow request:", newFollower)
|
// Get remote actor
|
||||||
// check we aren't following ourselves
|
follower, status, err := a.apGetRemoteActor(newFollower.String(), blogIri)
|
||||||
if newFollower == follow["object"] {
|
|
||||||
// actor and object are equal
|
|
||||||
return
|
|
||||||
}
|
|
||||||
follower, status, err := a.apGetRemoteActor(newFollower, blogIri)
|
|
||||||
if err != nil || status != 0 {
|
if err != nil || status != 0 {
|
||||||
// Couldn't retrieve remote actor info
|
// Couldn't retrieve remote actor info
|
||||||
log.Println("Failed to retrieve remote actor info:", newFollower)
|
log.Println("Failed to retrieve remote actor info:", newFollower)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Add or update follower
|
// Add or update follower
|
||||||
inbox := follower.Inbox
|
inbox := follower.Inbox.GetID()
|
||||||
if endpoints := follower.Endpoints; endpoints != nil && endpoints.SharedInbox != "" {
|
if endpoints := follower.Endpoints; endpoints != nil && endpoints.SharedInbox != nil && endpoints.SharedInbox.GetID() != "" {
|
||||||
inbox = endpoints.SharedInbox
|
inbox = endpoints.SharedInbox.GetID()
|
||||||
}
|
}
|
||||||
if err = a.db.apAddFollower(blogName, follower.ID, inbox); err != nil {
|
if inbox == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = a.db.apAddFollower(blogName, follower.GetID().String(), inbox.String()); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Send accept response to the new follower
|
// Send accept response to the new follower
|
||||||
accept := map[string]any{
|
accept := ap.AcceptNew(a.apNewID(blog), follow)
|
||||||
"@context": []string{asContext},
|
accept.To = append(accept.To, newFollower)
|
||||||
"type": "Accept",
|
accept.Actor = a.apAPIri(blog)
|
||||||
"to": follow["actor"],
|
_ = a.apQueueSendSigned(a.apIri(blog), inbox.String(), accept)
|
||||||
"actor": a.apIri(blog),
|
|
||||||
"object": follow,
|
|
||||||
}
|
|
||||||
_, accept["id"] = a.apNewID(blog)
|
|
||||||
_ = a.apQueueSendSigned(a.apIri(blog), inbox, accept)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) apSendProfileUpdates() {
|
func (a *goBlog) apSendProfileUpdates() {
|
||||||
for blog, config := range a.cfg.Blogs {
|
for blog, config := range a.cfg.Blogs {
|
||||||
person := a.toAsPerson(blog)
|
person := a.toApPerson(blog)
|
||||||
a.apSendToAllFollowers(blog, map[string]any{
|
update := ap.UpdateNew(a.apNewID(config), person)
|
||||||
"@context": []string{asContext},
|
update.Actor = a.apAPIri(config)
|
||||||
"actor": a.apIri(config),
|
update.Published = time.Now()
|
||||||
"published": time.Now().Format("2006-01-02T15:04:05-07:00"),
|
a.apSendToAllFollowers(blog, update)
|
||||||
"type": "Update",
|
|
||||||
"object": person,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) apSendToAllFollowers(blog string, activity any) {
|
func (a *goBlog) apSendToAllFollowers(blog string, activity *ap.Activity) {
|
||||||
inboxes, err := a.db.apGetAllInboxes(blog)
|
inboxes, err := a.db.apGetAllInboxes(blog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to retrieve inboxes:", err.Error())
|
log.Println("Failed to retrieve inboxes:", err.Error())
|
||||||
|
@ -454,7 +459,7 @@ func (a *goBlog) apSendToAllFollowers(blog string, activity any) {
|
||||||
a.apSendTo(a.apIri(a.cfg.Blogs[blog]), activity, inboxes)
|
a.apSendTo(a.apIri(a.cfg.Blogs[blog]), activity, inboxes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) apSendTo(blogIri string, activity any, inboxes []string) {
|
func (a *goBlog) apSendTo(blogIri string, activity *ap.Activity, inboxes []string) {
|
||||||
for _, i := range inboxes {
|
for _, i := range inboxes {
|
||||||
go func(inbox string) {
|
go func(inbox string) {
|
||||||
_ = a.apQueueSendSigned(blogIri, inbox, activity)
|
_ = a.apQueueSendSigned(blogIri, inbox, activity)
|
||||||
|
@ -462,14 +467,18 @@ func (a *goBlog) apSendTo(blogIri string, activity any, inboxes []string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) apNewID(blog *configBlog) (hash string, url string) {
|
func (a *goBlog) apNewID(blog *configBlog) ap.ID {
|
||||||
return hash, a.apIri(blog) + "#" + uuid.NewString()
|
return ap.ID(a.apIri(blog) + "#" + uuid.NewString())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) apIri(b *configBlog) string {
|
func (a *goBlog) apIri(b *configBlog) string {
|
||||||
return a.getFullAddress(b.getRelativePath(""))
|
return a.getFullAddress(b.getRelativePath(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *goBlog) apAPIri(b *configBlog) ap.IRI {
|
||||||
|
return ap.IRI(a.apIri(b))
|
||||||
|
}
|
||||||
|
|
||||||
func apRequestIsSuccess(code int) bool {
|
func apRequestIsSuccess(code int) bool {
|
||||||
return code == http.StatusOK || code == http.StatusCreated || code == http.StatusAccepted || code == http.StatusNoContent
|
return code == http.StatusOK || code == http.StatusCreated || code == http.StatusAccepted || code == http.StatusNoContent
|
||||||
}
|
}
|
||||||
|
@ -519,23 +528,3 @@ func (a *goBlog) loadActivityPubPrivateKey() error {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) apShowFollowers(w http.ResponseWriter, r *http.Request) {
|
|
||||||
blogName := chi.URLParam(r, "blog")
|
|
||||||
blog, ok := a.cfg.Blogs[blogName]
|
|
||||||
if !ok || blog == nil {
|
|
||||||
a.serveError(w, r, "Blog not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
followers, err := a.db.apGetAllFollowers(blogName)
|
|
||||||
if err != nil {
|
|
||||||
a.serveError(w, r, "Failed to get followers", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
a.render(w, r, a.renderActivityPubFollowers, &renderData{
|
|
||||||
BlogString: blogName,
|
|
||||||
Data: &activityPubFollowersRenderData{
|
|
||||||
followers: followers,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,13 +4,14 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
ap "github.com/go-ap/activitypub"
|
||||||
|
"github.com/go-ap/jsonld"
|
||||||
"go.goblog.app/app/pkgs/bufferpool"
|
"go.goblog.app/app/pkgs/bufferpool"
|
||||||
"go.goblog.app/app/pkgs/contenttype"
|
"go.goblog.app/app/pkgs/contenttype"
|
||||||
)
|
)
|
||||||
|
@ -47,7 +48,7 @@ func (a *goBlog) initAPSendQueue() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) apQueueSendSigned(blogIri, to string, activity any) error {
|
func (a *goBlog) apQueueSendSigned(blogIri, to string, activity any) error {
|
||||||
body, err := json.Marshal(activity)
|
body, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(activity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *goBlog) apShowFollowers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
blogName := chi.URLParam(r, "blog")
|
||||||
|
blog, ok := a.cfg.Blogs[blogName]
|
||||||
|
if !ok || blog == nil {
|
||||||
|
a.serveError(w, r, "Blog not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
followers, err := a.db.apGetAllFollowers(blogName)
|
||||||
|
if err != nil {
|
||||||
|
a.serveError(w, r, "Failed to get followers", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.render(w, r, a.renderActivityPubFollowers, &renderData{
|
||||||
|
BlogString: blogName,
|
||||||
|
Data: &activityPubFollowersRenderData{
|
||||||
|
followers: followers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,20 +1,19 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/araddon/dateparse"
|
"github.com/araddon/dateparse"
|
||||||
ct "github.com/elnormous/contenttype"
|
ct "github.com/elnormous/contenttype"
|
||||||
"go.goblog.app/app/pkgs/bufferpool"
|
ap "github.com/go-ap/activitypub"
|
||||||
|
"github.com/go-ap/jsonld"
|
||||||
"go.goblog.app/app/pkgs/contenttype"
|
"go.goblog.app/app/pkgs/contenttype"
|
||||||
)
|
)
|
||||||
|
|
||||||
const asContext = "https://www.w3.org/ns/activitystreams"
|
|
||||||
|
|
||||||
const asRequestKey contextKey = "asRequest"
|
const asRequestKey contextKey = "asRequest"
|
||||||
|
|
||||||
func (a *goBlog) checkActivityStreamsRequest(next http.Handler) http.Handler {
|
func (a *goBlog) checkActivityStreamsRequest(next http.Handler) http.Handler {
|
||||||
|
@ -37,177 +36,122 @@ func (a *goBlog) checkActivityStreamsRequest(next http.Handler) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type asNote struct {
|
func (a *goBlog) serveActivityStreamsPost(p *post, w http.ResponseWriter, r *http.Request) {
|
||||||
Context any `json:"@context,omitempty"`
|
note := a.toAPNote(p)
|
||||||
To []string `json:"to,omitempty"`
|
// Encode
|
||||||
InReplyTo string `json:"inReplyTo,omitempty"`
|
binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(note)
|
||||||
Name string `json:"name,omitempty"`
|
if err != nil {
|
||||||
Type string `json:"type,omitempty"`
|
a.serveError(w, r, "Encoding failed", http.StatusInternalServerError)
|
||||||
Content string `json:"content,omitempty"`
|
|
||||||
MediaType string `json:"mediaType,omitempty"`
|
|
||||||
Attachment []*asAttachment `json:"attachment,omitempty"`
|
|
||||||
Published string `json:"published,omitempty"`
|
|
||||||
Updated string `json:"updated,omitempty"`
|
|
||||||
ID string `json:"id,omitempty"`
|
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
AttributedTo string `json:"attributedTo,omitempty"`
|
|
||||||
Tag []*asTag `json:"tag,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type asPerson struct {
|
|
||||||
Context any `json:"@context,omitempty"`
|
|
||||||
ID string `json:"id,omitempty"`
|
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
Summary string `json:"summary,omitempty"`
|
|
||||||
PreferredUsername string `json:"preferredUsername,omitempty"`
|
|
||||||
Icon *asAttachment `json:"icon,omitempty"`
|
|
||||||
Inbox string `json:"inbox,omitempty"`
|
|
||||||
PublicKey *asPublicKey `json:"publicKey,omitempty"`
|
|
||||||
Endpoints *asEndpoints `json:"endpoints,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type asAttachment struct {
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
MediaType string `json:"mediaType,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type asTag struct {
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
Href string `json:"href,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type asPublicKey struct {
|
|
||||||
ID string `json:"id,omitempty"`
|
|
||||||
Owner string `json:"owner,omitempty"`
|
|
||||||
PublicKeyPem string `json:"publicKeyPem,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type asEndpoints struct {
|
|
||||||
SharedInbox string `json:"sharedInbox,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *goBlog) serveActivityStreamsPost(p *post, w http.ResponseWriter) {
|
|
||||||
buf := bufferpool.Get()
|
|
||||||
defer bufferpool.Put(buf)
|
|
||||||
if err := json.NewEncoder(buf).Encode(a.toASNote(p)); err != nil {
|
|
||||||
http.Error(w, "Encoding failed", http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Send response
|
||||||
w.Header().Set(contentType, contenttype.ASUTF8)
|
w.Header().Set(contentType, contenttype.ASUTF8)
|
||||||
_ = a.min.Get().Minify(contenttype.AS, w, buf)
|
_ = a.min.Get().Minify(contenttype.AS, w, bytes.NewReader(binary))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) toASNote(p *post) *asNote {
|
func (a *goBlog) toAPNote(p *post) *ap.Note {
|
||||||
// Create a Note object
|
// Create a Note object
|
||||||
as := &asNote{
|
note := ap.ObjectNew(ap.NoteType)
|
||||||
Context: []string{asContext},
|
note.To.Append(ap.PublicNS)
|
||||||
To: []string{"https://www.w3.org/ns/activitystreams#Public"},
|
note.MediaType = ap.MimeType(contenttype.HTML)
|
||||||
MediaType: contenttype.HTML,
|
note.ID = a.activityPubId(p)
|
||||||
ID: a.activityPubId(p),
|
note.URL = ap.IRI(a.fullPostURL(p))
|
||||||
URL: a.fullPostURL(p),
|
note.AttributedTo = a.apAPIri(a.cfg.Blogs[p.Blog])
|
||||||
AttributedTo: a.apIri(a.cfg.Blogs[p.Blog]),
|
|
||||||
}
|
|
||||||
// Name and Type
|
// Name and Type
|
||||||
if title := p.RenderedTitle; title != "" {
|
if title := p.RenderedTitle; title != "" {
|
||||||
as.Name = title
|
note.Type = ap.ArticleType
|
||||||
as.Type = "Article"
|
note.Name.Add(ap.DefaultLangRef(title))
|
||||||
} else {
|
|
||||||
as.Type = "Note"
|
|
||||||
}
|
}
|
||||||
// Content
|
// Content
|
||||||
as.Content = a.postHtml(p, true)
|
note.Content.Add(ap.DefaultLangRef(a.postHtml(p, true)))
|
||||||
// Attachments
|
// Attachments
|
||||||
if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 {
|
if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 {
|
||||||
|
var attachments ap.ItemCollection
|
||||||
for _, image := range images {
|
for _, image := range images {
|
||||||
as.Attachment = append(as.Attachment, &asAttachment{
|
apImage := ap.ObjectNew(ap.ImageType)
|
||||||
Type: "Image",
|
apImage.URL = ap.IRI(image)
|
||||||
URL: image,
|
attachments.Append(apImage)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
note.Attachment = attachments
|
||||||
}
|
}
|
||||||
// Tags
|
// Tags
|
||||||
for _, tagTax := range a.cfg.ActivityPub.TagsTaxonomies {
|
for _, tagTax := range a.cfg.ActivityPub.TagsTaxonomies {
|
||||||
for _, tag := range p.Parameters[tagTax] {
|
for _, tag := range p.Parameters[tagTax] {
|
||||||
as.Tag = append(as.Tag, &asTag{
|
apTag := &ap.Object{Type: "Hashtag"}
|
||||||
Type: "Hashtag",
|
apTag.Name.Add(ap.DefaultLangRef(tag))
|
||||||
Name: tag,
|
apTag.URL = ap.IRI(a.getFullAddress(a.getRelativePath(p.Blog, fmt.Sprintf("/%s/%s", tagTax, urlize(tag)))))
|
||||||
Href: a.getFullAddress(a.getRelativePath(p.Blog, fmt.Sprintf("/%s/%s", tagTax, urlize(tag)))),
|
note.Tag.Append(apTag)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Dates
|
// Dates
|
||||||
dateFormat := "2006-01-02T15:04:05-07:00"
|
|
||||||
if p.Published != "" {
|
if p.Published != "" {
|
||||||
if t, err := dateparse.ParseLocal(p.Published); err == nil {
|
if t, err := dateparse.ParseLocal(p.Published); err == nil {
|
||||||
as.Published = t.Format(dateFormat)
|
note.Published = t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if p.Updated != "" {
|
if p.Updated != "" {
|
||||||
if t, err := dateparse.ParseLocal(p.Updated); err == nil {
|
if t, err := dateparse.ParseLocal(p.Updated); err == nil {
|
||||||
as.Updated = t.Format(dateFormat)
|
note.Published = t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Reply
|
// Reply
|
||||||
if replyLink := p.firstParameter(a.cfg.Micropub.ReplyParam); replyLink != "" {
|
if replyLink := p.firstParameter(a.cfg.Micropub.ReplyParam); replyLink != "" {
|
||||||
as.InReplyTo = replyLink
|
note.InReplyTo = ap.IRI(replyLink)
|
||||||
}
|
}
|
||||||
return as
|
return note
|
||||||
}
|
}
|
||||||
|
|
||||||
const activityPubVersionParam = "activitypubversion"
|
const activityPubVersionParam = "activitypubversion"
|
||||||
|
|
||||||
func (a *goBlog) activityPubId(p *post) string {
|
func (a *goBlog) activityPubId(p *post) ap.IRI {
|
||||||
fu := a.fullPostURL(p)
|
fu := a.fullPostURL(p)
|
||||||
if version := p.firstParameter(activityPubVersionParam); version != "" {
|
if version := p.firstParameter(activityPubVersionParam); version != "" {
|
||||||
return fu + "?activitypubversion=" + version
|
return ap.IRI(fu + "?activitypubversion=" + version)
|
||||||
}
|
}
|
||||||
return fu
|
return ap.IRI(fu)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) toAsPerson(blog string) *asPerson {
|
func (a *goBlog) toApPerson(blog string) *ap.Person {
|
||||||
b := a.cfg.Blogs[blog]
|
b := a.cfg.Blogs[blog]
|
||||||
asBlog := &asPerson{
|
|
||||||
Context: []string{asContext},
|
apIri := a.apAPIri(b)
|
||||||
Type: "Person",
|
|
||||||
ID: a.apIri(b),
|
apBlog := ap.PersonNew(apIri)
|
||||||
URL: a.apIri(b),
|
apBlog.URL = apIri
|
||||||
Name: a.renderMdTitle(b.Title),
|
|
||||||
Summary: b.Description,
|
apBlog.Name.Set(ap.DefaultLang, ap.Content(a.renderMdTitle(b.Title)))
|
||||||
PreferredUsername: blog,
|
apBlog.Summary.Set(ap.DefaultLang, ap.Content(b.Description))
|
||||||
Inbox: a.getFullAddress("/activitypub/inbox/" + blog),
|
apBlog.PreferredUsername.Set(ap.DefaultLang, ap.Content(blog))
|
||||||
PublicKey: &asPublicKey{
|
|
||||||
Owner: a.apIri(b),
|
apBlog.Inbox = ap.IRI(a.getFullAddress("/activitypub/inbox/" + blog))
|
||||||
ID: a.apIri(b) + "#main-key",
|
apBlog.PublicKey.Owner = apIri
|
||||||
PublicKeyPem: string(pem.EncodeToMemory(&pem.Block{
|
apBlog.PublicKey.ID = ap.IRI(a.apIri(b) + "#main-key")
|
||||||
|
apBlog.PublicKey.PublicKeyPem = string(pem.EncodeToMemory(&pem.Block{
|
||||||
Type: "PUBLIC KEY",
|
Type: "PUBLIC KEY",
|
||||||
Headers: nil,
|
Headers: nil,
|
||||||
Bytes: a.apPubKeyBytes,
|
Bytes: a.apPubKeyBytes,
|
||||||
})),
|
}))
|
||||||
},
|
|
||||||
}
|
|
||||||
if pic := a.cfg.User.Picture; pic != "" {
|
if pic := a.cfg.User.Picture; pic != "" {
|
||||||
asBlog.Icon = &asAttachment{
|
icon := &ap.Image{}
|
||||||
Type: "Image",
|
icon.Type = ap.ImageType
|
||||||
URL: pic,
|
icon.MediaType = ap.MimeType(mimeTypeFromUrl(pic))
|
||||||
MediaType: mimeTypeFromUrl(pic),
|
icon.URL = ap.IRI(pic)
|
||||||
|
apBlog.Icon = icon
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return asBlog
|
return apBlog
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *http.Request) {
|
||||||
person := a.toAsPerson(blog)
|
person := a.toApPerson(blog)
|
||||||
// Encode
|
// Encode
|
||||||
buf := bufferpool.Get()
|
binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(person)
|
||||||
defer bufferpool.Put(buf)
|
if err != nil {
|
||||||
if err := json.NewEncoder(buf).Encode(person); err != nil {
|
|
||||||
a.serveError(w, r, "Encoding failed", http.StatusInternalServerError)
|
a.serveError(w, r, "Encoding failed", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Send response
|
||||||
w.Header().Set(contentType, contenttype.ASUTF8)
|
w.Header().Set(contentType, contenttype.ASUTF8)
|
||||||
_ = a.min.Get().Minify(contenttype.AS, w, buf)
|
_ = a.min.Get().Minify(contenttype.AS, w, bytes.NewReader(binary))
|
||||||
}
|
}
|
||||||
|
|
122
comments.go
122
comments.go
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
@ -20,6 +21,7 @@ type comment struct {
|
||||||
Name string
|
Name string
|
||||||
Website string
|
Website string
|
||||||
Comment string
|
Comment string
|
||||||
|
Original string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) serveComment(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveComment(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -28,13 +30,13 @@ func (a *goBlog) serveComment(w http.ResponseWriter, r *http.Request) {
|
||||||
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
row, err := a.db.QueryRow("select id, target, name, website, comment from comments where id = @id", sql.Named("id", id))
|
row, err := a.db.QueryRow("select id, target, name, website, comment, original from comments where id = @id", sql.Named("id", id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
comment := &comment{}
|
comment := &comment{}
|
||||||
if err = row.Scan(&comment.ID, &comment.Target, &comment.Name, &comment.Website, &comment.Comment); err == sql.ErrNoRows {
|
if err = row.Scan(&comment.ID, &comment.Target, &comment.Name, &comment.Website, &comment.Comment, &comment.Original); err == sql.ErrNoRows {
|
||||||
a.serve404(w, r)
|
a.serve404(w, r)
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -42,60 +44,102 @@ func (a *goBlog) serveComment(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, bc := a.getBlog(r)
|
_, bc := a.getBlog(r)
|
||||||
|
canonical := a.getFullAddress(bc.getRelativePath(path.Join(commentPath, strconv.Itoa(id))))
|
||||||
|
if comment.Original != "" {
|
||||||
|
canonical = comment.Original
|
||||||
|
}
|
||||||
a.render(w, r, a.renderComment, &renderData{
|
a.render(w, r, a.renderComment, &renderData{
|
||||||
Canonical: a.getFullAddress(bc.getRelativePath(path.Join(commentPath, strconv.Itoa(id)))),
|
Canonical: canonical,
|
||||||
Data: comment,
|
Data: comment,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) createComment(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) createCommentFromRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
// Check target
|
target := r.FormValue("target")
|
||||||
target := a.checkCommentTarget(w, r)
|
comment := r.FormValue("comment")
|
||||||
if target == "" {
|
name := r.FormValue("name")
|
||||||
|
website := r.FormValue("website")
|
||||||
|
_, bc := a.getBlog(r)
|
||||||
|
// Create comment
|
||||||
|
result, errStatus, err := a.createComment(bc, target, comment, name, website, "")
|
||||||
|
if err != nil {
|
||||||
|
a.serveError(w, r, err.Error(), errStatus)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Redirect to comment
|
||||||
|
http.Redirect(w, r, result, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *goBlog) createComment(bc *configBlog, target, comment, name, website, original string) (string, int, error) {
|
||||||
|
updateId := -1
|
||||||
|
// Check target
|
||||||
|
target, status, err := a.checkCommentTarget(target)
|
||||||
|
if err != nil {
|
||||||
|
return "", status, err
|
||||||
|
}
|
||||||
// Check and clean comment
|
// Check and clean comment
|
||||||
comment := cleanHTMLText(r.FormValue("comment"))
|
comment = cleanHTMLText(comment)
|
||||||
if comment == "" {
|
if comment == "" {
|
||||||
a.serveError(w, r, "Comment is empty", http.StatusBadRequest)
|
return "", http.StatusBadRequest, errors.New("comment is empty")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
name := defaultIfEmpty(cleanHTMLText(r.FormValue("name")), "Anonymous")
|
name = defaultIfEmpty(cleanHTMLText(name), "Anonymous")
|
||||||
website := cleanHTMLText(r.FormValue("website"))
|
website = cleanHTMLText(website)
|
||||||
// Insert
|
original = cleanHTMLText(original)
|
||||||
result, err := a.db.Exec("insert into comments (target, comment, name, website) values (@target, @comment, @name, @website)", sql.Named("target", target), sql.Named("comment", comment), sql.Named("name", name), sql.Named("website", website))
|
if original != "" {
|
||||||
|
// Check if comment already exists
|
||||||
|
exists, id, err := a.db.commentIdByOriginal(original)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
return "", http.StatusInternalServerError, errors.New("failed to check the database")
|
||||||
return
|
}
|
||||||
|
if exists {
|
||||||
|
updateId = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Insert
|
||||||
|
if updateId == -1 {
|
||||||
|
result, err := a.db.Exec(
|
||||||
|
"insert into comments (target, comment, name, website, original) values (@target, @comment, @name, @website, @original)",
|
||||||
|
sql.Named("target", target), sql.Named("comment", comment), sql.Named("name", name), sql.Named("website", website), sql.Named("original", original),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", http.StatusInternalServerError, errors.New("failed to save comment to database")
|
||||||
}
|
}
|
||||||
if commentID, err := result.LastInsertId(); err != nil {
|
if commentID, err := result.LastInsertId(); err != nil {
|
||||||
// Serve error
|
return "", http.StatusInternalServerError, errors.New("failed to save comment to database")
|
||||||
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
|
||||||
} else {
|
} else {
|
||||||
_, bc := a.getBlog(r)
|
|
||||||
commentAddress := bc.getRelativePath(path.Join(commentPath, strconv.Itoa(int(commentID))))
|
commentAddress := bc.getRelativePath(path.Join(commentPath, strconv.Itoa(int(commentID))))
|
||||||
// Send webmention
|
// Send webmention
|
||||||
_ = a.createWebmention(a.getFullAddress(commentAddress), a.getFullAddress(target))
|
_ = a.createWebmention(a.getFullAddress(commentAddress), a.getFullAddress(target))
|
||||||
// Redirect to comment
|
// Return comment path
|
||||||
http.Redirect(w, r, commentAddress, http.StatusFound)
|
return commentAddress, 0, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err := a.db.Exec(
|
||||||
|
"update comments set target = @target, comment = @comment, name = @name, website = @website where id = @id",
|
||||||
|
sql.Named("target", target), sql.Named("comment", comment), sql.Named("name", name), sql.Named("website", website), sql.Named("id", updateId),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", http.StatusInternalServerError, errors.New("failed to update comment in database")
|
||||||
|
}
|
||||||
|
commentAddress := bc.getRelativePath(path.Join(commentPath, strconv.Itoa(updateId)))
|
||||||
|
// Send webmention
|
||||||
|
_ = a.createWebmention(a.getFullAddress(commentAddress), a.getFullAddress(target))
|
||||||
|
// Return comment path
|
||||||
|
return commentAddress, 0, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) checkCommentTarget(w http.ResponseWriter, r *http.Request) string {
|
func (a *goBlog) checkCommentTarget(target string) (string, int, error) {
|
||||||
target := r.FormValue("target")
|
|
||||||
if target == "" {
|
if target == "" {
|
||||||
a.serveError(w, r, "No target specified", http.StatusBadRequest)
|
return "", http.StatusBadRequest, errors.New("no target specified")
|
||||||
return ""
|
|
||||||
} else if !strings.HasPrefix(target, a.cfg.Server.PublicAddress) {
|
} else if !strings.HasPrefix(target, a.cfg.Server.PublicAddress) {
|
||||||
a.serveError(w, r, "Bad target", http.StatusBadRequest)
|
return "", http.StatusBadRequest, errors.New("bad target")
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
targetURL, err := url.Parse(target)
|
targetURL, err := url.Parse(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
return "", http.StatusBadRequest, errors.New("failed to parse URL")
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
return targetURL.Path
|
return targetURL.Path, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type commentsRequestConfig struct {
|
type commentsRequestConfig struct {
|
||||||
|
@ -105,7 +149,7 @@ type commentsRequestConfig struct {
|
||||||
func buildCommentsQuery(config *commentsRequestConfig) (query string, args []any) {
|
func buildCommentsQuery(config *commentsRequestConfig) (query string, args []any) {
|
||||||
queryBuilder := bufferpool.Get()
|
queryBuilder := bufferpool.Get()
|
||||||
defer bufferpool.Put(queryBuilder)
|
defer bufferpool.Put(queryBuilder)
|
||||||
queryBuilder.WriteString("select id, target, name, website, comment from comments order by id desc")
|
queryBuilder.WriteString("select id, target, name, website, comment, original from comments order by id desc")
|
||||||
if config.limit != 0 || config.offset != 0 {
|
if config.limit != 0 || config.offset != 0 {
|
||||||
queryBuilder.WriteString(" limit @limit offset @offset")
|
queryBuilder.WriteString(" limit @limit offset @offset")
|
||||||
args = append(args, sql.Named("limit", config.limit), sql.Named("offset", config.offset))
|
args = append(args, sql.Named("limit", config.limit), sql.Named("offset", config.offset))
|
||||||
|
@ -122,7 +166,7 @@ func (db *database) getComments(config *commentsRequestConfig) ([]*comment, erro
|
||||||
}
|
}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
c := &comment{}
|
c := &comment{}
|
||||||
err = rows.Scan(&c.ID, &c.Target, &c.Name, &c.Website, &c.Comment)
|
err = rows.Scan(&c.ID, &c.Target, &c.Name, &c.Website, &c.Comment, &c.Original)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -147,6 +191,20 @@ func (db *database) deleteComment(id int) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *database) commentIdByOriginal(original string) (bool, int, error) {
|
||||||
|
var id int
|
||||||
|
row, err := db.QueryRow("select id from comments where original = @original", sql.Named("original", original))
|
||||||
|
if err != nil {
|
||||||
|
return false, 0, err
|
||||||
|
}
|
||||||
|
if err := row.Scan(&id); err != nil && errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return false, 0, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return false, 0, err
|
||||||
|
}
|
||||||
|
return true, id, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *goBlog) commentsEnabledForBlog(blog *configBlog) bool {
|
func (a *goBlog) commentsEnabledForBlog(blog *configBlog) bool {
|
||||||
return blog.Comments != nil && blog.Comments.Enabled
|
return blog.Comments != nil && blog.Comments.Enabled
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ func Test_comments(t *testing.T) {
|
||||||
req.Header.Add(contentType, contenttype.WWWForm)
|
req.Header.Add(contentType, contenttype.WWWForm)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
app.createComment(rec, req.WithContext(context.WithValue(req.Context(), blogKey, "en")))
|
app.createCommentFromRequest(rec, req.WithContext(context.WithValue(req.Context(), blogKey, "en")))
|
||||||
|
|
||||||
res := rec.Result()
|
res := rec.Result()
|
||||||
assert.Equal(t, http.StatusFound, res.StatusCode)
|
assert.Equal(t, http.StatusFound, res.StatusCode)
|
||||||
|
@ -117,7 +117,7 @@ func Test_comments(t *testing.T) {
|
||||||
req.Header.Add(contentType, contenttype.WWWForm)
|
req.Header.Add(contentType, contenttype.WWWForm)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
app.createComment(rec, req.WithContext(context.WithValue(req.Context(), blogKey, "en")))
|
app.createCommentFromRequest(rec, req.WithContext(context.WithValue(req.Context(), blogKey, "en")))
|
||||||
|
|
||||||
res := rec.Result()
|
res := rec.Result()
|
||||||
assert.Equal(t, http.StatusFound, res.StatusCode)
|
assert.Equal(t, http.StatusFound, res.StatusCode)
|
||||||
|
@ -153,7 +153,7 @@ func Test_comments(t *testing.T) {
|
||||||
req.Header.Add(contentType, contenttype.WWWForm)
|
req.Header.Add(contentType, contenttype.WWWForm)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
app.createComment(rec, req.WithContext(context.WithValue(req.Context(), blogKey, "en")))
|
app.createCommentFromRequest(rec, req.WithContext(context.WithValue(req.Context(), blogKey, "en")))
|
||||||
|
|
||||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
alter table comments add original text not null default "";
|
7
go.mod
7
go.mod
|
@ -21,6 +21,8 @@ require (
|
||||||
github.com/elnormous/contenttype v1.0.3
|
github.com/elnormous/contenttype v1.0.3
|
||||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
||||||
github.com/emersion/go-smtp v0.15.0
|
github.com/emersion/go-smtp v0.15.0
|
||||||
|
github.com/go-ap/activitypub v0.0.0-20221119120906-cb8207231e18
|
||||||
|
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
|
||||||
github.com/go-chi/chi/v5 v5.0.7
|
github.com/go-chi/chi/v5 v5.0.7
|
||||||
github.com/go-fed/httpsig v1.1.0
|
github.com/go-fed/httpsig v1.1.0
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||||
|
@ -70,6 +72,7 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||||
|
@ -79,6 +82,7 @@ require (
|
||||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
|
github.com/go-ap/errors v0.0.0-20221115052505-8aaa26f930b4 // indirect
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/gorilla/css v1.0.0 // indirect
|
github.com/gorilla/css v1.0.0 // indirect
|
||||||
|
@ -104,9 +108,10 @@ require (
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/subosito/gotenv v1.4.1 // indirect
|
github.com/subosito/gotenv v1.4.1 // indirect
|
||||||
github.com/tdewolff/parse/v2 v2.6.4 // indirect
|
github.com/tdewolff/parse/v2 v2.6.4 // indirect
|
||||||
|
github.com/valyala/fastjson v1.6.3 // indirect
|
||||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
|
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
|
||||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
|
golang.org/x/oauth2 v0.1.0 // indirect
|
||||||
golang.org/x/sys v0.2.0 // indirect
|
golang.org/x/sys v0.2.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // indirect
|
google.golang.org/protobuf v1.28.1 // indirect
|
||||||
|
|
14
go.sum
14
go.sum
|
@ -44,6 +44,8 @@ git.jlel.se/jlelse/goldmark-mark v0.0.0-20210522162520-9788c89266a4 h1:p3c/vCY6M
|
||||||
git.jlel.se/jlelse/goldmark-mark v0.0.0-20210522162520-9788c89266a4/go.mod h1:ZFhxwbX+afhgbzh5rpkSJUp6vIduNPtIGDrsWpIcHTE=
|
git.jlel.se/jlelse/goldmark-mark v0.0.0-20210522162520-9788c89266a4/go.mod h1:ZFhxwbX+afhgbzh5rpkSJUp6vIduNPtIGDrsWpIcHTE=
|
||||||
git.jlel.se/jlelse/template-strings v0.0.0-20220211095702-c012e3b5045b h1:zrGLEeWzv7bzGRUKsS42akQpszXwEU+8nXV2Z2iDSJM=
|
git.jlel.se/jlelse/template-strings v0.0.0-20220211095702-c012e3b5045b h1:zrGLEeWzv7bzGRUKsS42akQpszXwEU+8nXV2Z2iDSJM=
|
||||||
git.jlel.se/jlelse/template-strings v0.0.0-20220211095702-c012e3b5045b/go.mod h1:UNLE8cup2GTHbsE89xezRwq3GhKspPI9NyckPbgJEmw=
|
git.jlel.se/jlelse/template-strings v0.0.0-20220211095702-c012e3b5045b/go.mod h1:UNLE8cup2GTHbsE89xezRwq3GhKspPI9NyckPbgJEmw=
|
||||||
|
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
|
||||||
|
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||||
|
@ -122,6 +124,12 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||||
|
github.com/go-ap/activitypub v0.0.0-20221119120906-cb8207231e18 h1:ViTJy/5JI0zKtpeju09rAM0HPTc0Xmnh+uYtxYZ+tfs=
|
||||||
|
github.com/go-ap/activitypub v0.0.0-20221119120906-cb8207231e18/go.mod h1:acWwxqPWwm9hmjSlva1N1na1J6eGeC9kKTS3YEBOVJI=
|
||||||
|
github.com/go-ap/errors v0.0.0-20221115052505-8aaa26f930b4 h1:oySiT87Q2cd0o5O8er2zyjiRcTQA0KuOgw1N9+RQqG0=
|
||||||
|
github.com/go-ap/errors v0.0.0-20221115052505-8aaa26f930b4/go.mod h1:SaTNjEEkp0q+w3pUS1ccyEL/lUrHteORlDq/e21mCc8=
|
||||||
|
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw=
|
||||||
|
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA=
|
||||||
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
||||||
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||||
|
@ -382,6 +390,8 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
|
||||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
|
github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc=
|
||||||
|
github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||||
github.com/vcraescu/go-paginator v1.0.1-0.20201114172518-2cfc59fe05c2 h1:l5j4nE6rosbObXB/uPmzxOQ2z5uXlFOgttlHJ2YL/0w=
|
github.com/vcraescu/go-paginator v1.0.1-0.20201114172518-2cfc59fe05c2 h1:l5j4nE6rosbObXB/uPmzxOQ2z5uXlFOgttlHJ2YL/0w=
|
||||||
github.com/vcraescu/go-paginator v1.0.1-0.20201114172518-2cfc59fe05c2/go.mod h1:NEDNuq1asYbAeX+uy6w56MDQSFmBQz9k+N9Hy6m4r2U=
|
github.com/vcraescu/go-paginator v1.0.1-0.20201114172518-2cfc59fe05c2/go.mod h1:NEDNuq1asYbAeX+uy6w56MDQSFmBQz9k+N9Hy6m4r2U=
|
||||||
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
|
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
|
||||||
|
@ -495,8 +505,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
|
||||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk=
|
golang.org/x/oauth2 v0.1.0 h1:isLCZuhj4v+tYv7eskaN4v/TM+A1begWWgyVJDdl1+Y=
|
||||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
|
|
@ -369,7 +369,7 @@ func (a *goBlog) blogCommentsRouter(conf *configBlog) func(r chi.Router) {
|
||||||
middleware.WithValue(pathKey, commentsPath),
|
middleware.WithValue(pathKey, commentsPath),
|
||||||
)
|
)
|
||||||
r.With(a.cacheMiddleware, noIndexHeader).Get("/{id:[0-9]+}", a.serveComment)
|
r.With(a.cacheMiddleware, noIndexHeader).Get("/{id:[0-9]+}", a.serveComment)
|
||||||
r.With(a.captchaMiddleware).Post("/", a.createComment)
|
r.With(a.captchaMiddleware).Post("/", a.createCommentFromRequest)
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
// Admin
|
// Admin
|
||||||
r.Use(a.authMiddleware)
|
r.Use(a.authMiddleware)
|
||||||
|
|
2
posts.go
2
posts.go
|
@ -66,7 +66,7 @@ func (a *goBlog) servePost(w http.ResponseWriter, r *http.Request) {
|
||||||
a.serveActivityStreams(p.Blog, w, r)
|
a.serveActivityStreams(p.Blog, w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.serveActivityStreamsPost(p, w)
|
a.serveActivityStreamsPost(p, w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
canonical := p.firstParameter("original")
|
canonical := p.firstParameter("original")
|
||||||
|
|
18
ui.go
18
ui.go
|
@ -358,7 +358,16 @@ func (a *goBlog) renderComment(h *htmlbuilder.HtmlBuilder, rd *renderData) {
|
||||||
hb.WriteElementOpen("p", "class", "e-content")
|
hb.WriteElementOpen("p", "class", "e-content")
|
||||||
hb.WriteUnescaped(c.Comment) // Already escaped
|
hb.WriteUnescaped(c.Comment) // Already escaped
|
||||||
hb.WriteElementClose("p")
|
hb.WriteElementClose("p")
|
||||||
|
// Original
|
||||||
|
if c.Original != "" {
|
||||||
|
hb.WriteElementOpen("p", "class", "")
|
||||||
|
hb.WriteElementOpen("a", "class", "u-url", "target", "_blank", "rel", "nofollow noopener noreferrer ugc", "href", c.Original)
|
||||||
|
hb.WriteEscaped(c.Original)
|
||||||
|
hb.WriteElementClose("a")
|
||||||
|
hb.WriteElementClose("p")
|
||||||
|
}
|
||||||
hb.WriteElementClose("main")
|
hb.WriteElementClose("main")
|
||||||
|
// Original
|
||||||
// Interactions
|
// Interactions
|
||||||
if a.commentsEnabledForBlog(rd.Blog) {
|
if a.commentsEnabledForBlog(rd.Blog) {
|
||||||
a.renderInteractions(hb, rd)
|
a.renderInteractions(hb, rd)
|
||||||
|
@ -1215,7 +1224,7 @@ func (a *goBlog) renderCommentsAdmin(hb *htmlbuilder.HtmlBuilder, rd *renderData
|
||||||
hb.WriteElementOpen("h1")
|
hb.WriteElementOpen("h1")
|
||||||
hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "comments"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "comments"))
|
||||||
hb.WriteElementClose("h1")
|
hb.WriteElementClose("h1")
|
||||||
// Notifications
|
// Comments
|
||||||
for _, c := range crd.comments {
|
for _, c := range crd.comments {
|
||||||
hb.WriteElementOpen("div", "class", "p")
|
hb.WriteElementOpen("div", "class", "p")
|
||||||
// ID, Target, Name
|
// ID, Target, Name
|
||||||
|
@ -1236,6 +1245,13 @@ func (a *goBlog) renderCommentsAdmin(hb *htmlbuilder.HtmlBuilder, rd *renderData
|
||||||
if c.Website != "" {
|
if c.Website != "" {
|
||||||
hb.WriteElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
}
|
}
|
||||||
|
if c.Original != "" {
|
||||||
|
hb.WriteElementOpen("br")
|
||||||
|
hb.WriteEscaped("Original: ")
|
||||||
|
hb.WriteElementOpen("a", "href", c.Website, "target", "_blank", "rel", "nofollow noopener noreferrer ugc")
|
||||||
|
hb.WriteEscaped(c.Original)
|
||||||
|
hb.WriteElementClose("a")
|
||||||
|
}
|
||||||
hb.WriteElementClose("p")
|
hb.WriteElementClose("p")
|
||||||
// Comment
|
// Comment
|
||||||
hb.WriteElementOpen("p")
|
hb.WriteElementOpen("p")
|
||||||
|
|
|
@ -186,6 +186,11 @@ func (db *database) deleteWebmentionId(id int) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *database) deleteWebmentionUUrl(uUrl string) error {
|
||||||
|
_, err := db.Exec("delete from webmentions where url = @url", sql.Named("url", uUrl))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (db *database) deleteWebmention(m *mention) error {
|
func (db *database) deleteWebmention(m *mention) error {
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
"delete from webmentions where lowerunescaped(source) in (lowerunescaped(@source), lowerunescaped(@newsource)) and lowerunescaped(target) in (lowerunescaped(@target), lowerunescaped(@newtarget))",
|
"delete from webmentions where lowerunescaped(source) in (lowerunescaped(@source), lowerunescaped(@newsource)) and lowerunescaped(target) in (lowerunescaped(@target), lowerunescaped(@newtarget))",
|
||||||
|
|
Loading…
Reference in New Issue