mirror of
https://github.com/writefreely/writefreely
synced 2024-09-20 05:41:52 +00:00
Support rel=me verification on blogs
This allows setting a URL, and then renders a <link> element in the head of the blog. It requires a database migration. Ref T744
This commit is contained in:
parent
e0c165ff1e
commit
264bef03b1
10 changed files with 169 additions and 11 deletions
|
@ -862,9 +862,6 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
|
|||
return ErrCollectionNotFound
|
||||
}
|
||||
|
||||
// Add collection properties
|
||||
c.Monetization = app.db.GetCollectionAttribute(c.ID, "monetization_pointer")
|
||||
|
||||
silenced, err := app.db.IsUserSilenced(u.ID)
|
||||
if err != nil {
|
||||
if err == ErrUserNotFound {
|
||||
|
|
|
@ -23,16 +23,19 @@ import (
|
|||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/writeas/activity/streams"
|
||||
"github.com/writeas/activityserve"
|
||||
"github.com/writeas/httpsig"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/activitypub"
|
||||
"github.com/writeas/web-core/activitystreams"
|
||||
"github.com/writeas/web-core/id"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/web-core/silobridge"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -60,6 +63,7 @@ type RemoteUser struct {
|
|||
ActorID string
|
||||
Inbox string
|
||||
SharedInbox string
|
||||
URL string
|
||||
Handle string
|
||||
}
|
||||
|
||||
|
@ -452,7 +456,7 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
|
|||
followerID = remoteUser.ID
|
||||
} else {
|
||||
// Add follower locally, since it wasn't found before
|
||||
res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox)
|
||||
res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox, url) VALUES (?, ?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox, fullActor.URL)
|
||||
if err != nil {
|
||||
// if duplicate key, res will be nil and panic on
|
||||
// res.LastInsertId below
|
||||
|
@ -764,8 +768,8 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
|
|||
|
||||
func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
|
||||
u := RemoteUser{ActorID: actorID}
|
||||
var handle sql.NullString
|
||||
err := app.db.QueryRow("SELECT id, inbox, shared_inbox, handle FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox, &handle)
|
||||
var urlVal, handle sql.NullString
|
||||
err := app.db.QueryRow("SELECT id, inbox, shared_inbox, url, handle FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox, &urlVal, &handle)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return nil, impart.HTTPError{http.StatusNotFound, "No remote user with that ID."}
|
||||
|
@ -774,6 +778,7 @@ func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
u.URL = urlVal.String
|
||||
u.Handle = handle.String
|
||||
|
||||
return &u, nil
|
||||
|
@ -783,7 +788,8 @@ func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
|
|||
// 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)
|
||||
var urlVal sql.NullString
|
||||
err := app.db.QueryRow("SELECT id, actor_id, inbox, shared_inbox, url FROM remoteusers WHERE handle = ?", handle).Scan(&u.ID, &u.ActorID, &u.Inbox, &u.SharedInbox, &urlVal)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return nil, ErrRemoteUserNotFound
|
||||
|
@ -791,6 +797,7 @@ func getRemoteUserFromHandle(app *App, handle string) (*RemoteUser, error) {
|
|||
log.Error("Couldn't get remote user %s: %v", handle, err)
|
||||
return nil, err
|
||||
}
|
||||
u.URL = urlVal.String
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
|
@ -824,6 +831,69 @@ func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser,
|
|||
return actor, remoteUser, nil
|
||||
}
|
||||
|
||||
func GetProfileURLFromHandle(app *App, handle string) (string, error) {
|
||||
handle = strings.TrimLeft(handle, "@")
|
||||
actorIRI := ""
|
||||
parts := strings.Split(handle, "@")
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("invalid handle format")
|
||||
}
|
||||
domain := parts[1]
|
||||
|
||||
// Check non-AP instances
|
||||
if siloProfileURL := silobridge.Profile(parts[0], domain); siloProfileURL != "" {
|
||||
return siloProfileURL, nil
|
||||
}
|
||||
|
||||
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("Couldn't update handle '%s' for user %s", handle, 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: %v", err)
|
||||
}
|
||||
if debugging {
|
||||
log.Info("Got remote actor: %s %s %s %s %s", actorIRI, remoteActor.GetInbox(), remoteActor.GetSharedInbox(), remoteActor.URL(), handle)
|
||||
}
|
||||
_, err = app.db.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox, url, handle) VALUES(?, ?, ?, ?, ?)", actorIRI, remoteActor.GetInbox(), remoteActor.GetSharedInbox(), remoteActor.URL(), handle)
|
||||
if err != nil {
|
||||
log.Error("Couldn't insert remote user: %v", err)
|
||||
return "", err
|
||||
}
|
||||
actorIRI = remoteActor.URL()
|
||||
}
|
||||
} else if remoteUser.URL == "" {
|
||||
log.Info("Remote user %s URL empty, fetching", remoteUser.ActorID)
|
||||
newRemoteActor, err := activityserve.NewRemoteActor(remoteUser.ActorID)
|
||||
if err != nil {
|
||||
log.Error("Couldn't fetch remote actor: %v", err)
|
||||
} else {
|
||||
_, err := app.db.Exec("UPDATE remoteusers SET url = ? WHERE actor_id = ?", newRemoteActor.URL(), remoteUser.ActorID)
|
||||
if err != nil {
|
||||
log.Error("Couldn't update handle '%s' for user %s", handle, actorIRI)
|
||||
} else {
|
||||
actorIRI = newRemoteActor.URL()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
actorIRI = remoteUser.URL
|
||||
}
|
||||
return actorIRI, nil
|
||||
}
|
||||
|
||||
// unmarshal actor normalizes the actor response to conform to
|
||||
// the type Person from github.com/writeas/web-core/activitysteams
|
||||
//
|
||||
|
|
|
@ -59,6 +59,7 @@ type (
|
|||
URL string `json:"url,omitempty"`
|
||||
|
||||
Monetization string `json:"monetization_pointer,omitempty"`
|
||||
Verification string `json:"verification_link"`
|
||||
|
||||
db *datastore
|
||||
hostName string
|
||||
|
@ -98,6 +99,7 @@ type (
|
|||
Script *sql.NullString `schema:"script" json:"script"`
|
||||
Signature *sql.NullString `schema:"signature" json:"signature"`
|
||||
Monetization *string `schema:"monetization_pointer" json:"monetization_pointer"`
|
||||
Verification *string `schema:"verification_link" json:"verification_link"`
|
||||
Visibility *int `schema:"visibility" json:"public"`
|
||||
Format *sql.NullString `schema:"format" json:"format"`
|
||||
}
|
||||
|
@ -1132,7 +1134,7 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
|
|||
}
|
||||
}
|
||||
|
||||
err = app.db.UpdateCollection(&c, collAlias)
|
||||
err = app.db.UpdateCollection(app, &c, collAlias)
|
||||
if err != nil {
|
||||
if err, ok := err.(impart.HTTPError); ok {
|
||||
if reqJSON {
|
||||
|
|
45
database.go
45
database.go
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/writeas/web-core/silobridge"
|
||||
wf_db "github.com/writefreely/writefreely/db"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -95,7 +96,7 @@ type writestore interface {
|
|||
GetCollection(alias string) (*Collection, error)
|
||||
GetCollectionForPad(alias string) (*Collection, error)
|
||||
GetCollectionByID(id int64) (*Collection, error)
|
||||
UpdateCollection(c *SubmittedCollection, alias string) error
|
||||
UpdateCollection(app *App, c *SubmittedCollection, alias string) error
|
||||
DeleteCollection(alias string, userID int64) error
|
||||
|
||||
UpdatePostPinState(pinned bool, postID string, collID, ownerID, pos int64) error
|
||||
|
@ -815,6 +816,7 @@ func (db *datastore) GetCollectionBy(condition string, value interface{}) (*Coll
|
|||
c.Format = format.String
|
||||
c.Public = c.IsPublic()
|
||||
c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer")
|
||||
c.Verification = db.GetCollectionAttribute(c.ID, "verification_link")
|
||||
|
||||
c.db = db
|
||||
|
||||
|
@ -851,7 +853,7 @@ func (db *datastore) GetCollectionFromDomain(host string) (*Collection, error) {
|
|||
return db.GetCollectionBy("host = ?", host)
|
||||
}
|
||||
|
||||
func (db *datastore) UpdateCollection(c *SubmittedCollection, alias string) error {
|
||||
func (db *datastore) UpdateCollection(app *App, c *SubmittedCollection, alias string) error {
|
||||
q := query.NewUpdate().
|
||||
SetStringPtr(c.Title, "title").
|
||||
SetStringPtr(c.Description, "description").
|
||||
|
@ -910,6 +912,44 @@ func (db *datastore) UpdateCollection(c *SubmittedCollection, alias string) erro
|
|||
}
|
||||
}
|
||||
|
||||
// Update Verification link value
|
||||
if c.Verification != nil {
|
||||
skipUpdate := false
|
||||
if *c.Verification != "" {
|
||||
// Strip away any excess spaces
|
||||
trimmed := strings.TrimSpace(*c.Verification)
|
||||
if strings.HasPrefix(trimmed, "@") && strings.Count(trimmed, "@") == 2 {
|
||||
// This looks like a fediverse handle, so resolve profile URL
|
||||
profileURL, err := GetProfileURLFromHandle(app, trimmed)
|
||||
if err != nil || profileURL == "" {
|
||||
log.Error("Couldn't find user %s: %v", trimmed, err)
|
||||
skipUpdate = true
|
||||
} else {
|
||||
c.Verification = &profileURL
|
||||
}
|
||||
} else {
|
||||
if !strings.HasPrefix(trimmed, "http") {
|
||||
trimmed = "https://" + trimmed
|
||||
}
|
||||
vu, err := url.Parse(trimmed)
|
||||
if err != nil {
|
||||
// Value appears invalid, so don't update
|
||||
skipUpdate = true
|
||||
} else {
|
||||
s := vu.String()
|
||||
c.Verification = &s
|
||||
}
|
||||
}
|
||||
}
|
||||
if !skipUpdate {
|
||||
err = db.SetCollectionAttribute(collID, "verification_link", *c.Verification)
|
||||
if err != nil {
|
||||
log.Error("Unable to insert verification_link value: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update Monetization value
|
||||
if c.Monetization != nil {
|
||||
skipUpdate := false
|
||||
|
@ -2811,6 +2851,7 @@ func handleFailedPostInsert(err error) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Deprecated: use GetProfileURLFromHandle() instead, which returns user-facing URL instead of actor_id
|
||||
func (db *datastore) GetProfilePageFromHandle(app *App, handle string) (string, error) {
|
||||
handle = strings.TrimLeft(handle, "@")
|
||||
actorIRI := ""
|
||||
|
|
2
go.mod
2
go.mod
|
@ -22,7 +22,7 @@ require (
|
|||
github.com/stretchr/testify v1.8.4
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
github.com/writeas/activity v0.1.2
|
||||
github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481
|
||||
github.com/writeas/activityserve v0.0.0-20230428180247-dc13a4f4d835
|
||||
github.com/writeas/go-strip-markdown/v2 v2.1.1
|
||||
github.com/writeas/go-webfinger v1.1.0
|
||||
github.com/writeas/httpsig v1.0.0
|
||||
|
|
2
go.sum
2
go.sum
|
@ -120,6 +120,8 @@ github.com/writeas/activity v0.1.2 h1:Y12B5lIrabfqKE7e7HFCWiXrlfXljr9tlkFm2mp7Dg
|
|||
github.com/writeas/activity v0.1.2/go.mod h1:mYYgiewmEM+8tlifirK/vl6tmB2EbjYaxwb+ndUw5T0=
|
||||
github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481 h1:BiSivIxLQFcKoUorpNN3rNwwFG5bITPnqUSyIccfdh0=
|
||||
github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481/go.mod h1:4akDJSl+sSp+QhrQKMqzAqdV1gJ1pPx6XPI77zgMM8o=
|
||||
github.com/writeas/activityserve v0.0.0-20230428180247-dc13a4f4d835 h1:bm/7gYo6y3GxtTa1qyUFyCk29CTnBAKt7z4D2MASYrw=
|
||||
github.com/writeas/activityserve v0.0.0-20230428180247-dc13a4f4d835/go.mod h1:4akDJSl+sSp+QhrQKMqzAqdV1gJ1pPx6XPI77zgMM8o=
|
||||
github.com/writeas/go-strip-markdown/v2 v2.1.1 h1:hAxUM21Uhznf/FnbVGiJciqzska6iLei22Ijc3q2e28=
|
||||
github.com/writeas/go-strip-markdown/v2 v2.1.1/go.mod h1:UvvgPJgn1vvN8nWuE5e7v/+qmDu3BSVnKAB6Gl7hFzA=
|
||||
github.com/writeas/go-webfinger v1.1.0 h1:MzNyt0ry/GMsRmJGftn2o9mPwqK1Q5MLdh4VuJCfb1Q=
|
||||
|
|
|
@ -67,6 +67,7 @@ var migrations = []Migration{
|
|||
New("optimize drafts retrieval", optimizeDrafts), // V8 -> V9
|
||||
New("support post signatures", supportPostSignatures), // V9 -> V10
|
||||
New("Widen oauth_users.access_token", widenOauthAcceesToken), // V10 -> V11
|
||||
New("support verifying fedi profile", fediverseVerifyProfile), // V11 -> V12
|
||||
}
|
||||
|
||||
// CurrentVer returns the current migration version the application is on
|
||||
|
|
33
migrations/v12.go
Normal file
33
migrations/v12.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright © 2023 Musing Studio 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 fediverseVerifyProfile(db *datastore) error {
|
||||
t, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = t.Exec(`ALTER TABLE remoteusers ADD COLUMN url ` + db.typeVarChar(255) + ` NULL` + db.after("shared_inbox"))
|
||||
if err != nil {
|
||||
t.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
err = t.Commit()
|
||||
if err != nil {
|
||||
t.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -3,6 +3,9 @@
|
|||
{{if .Monetization -}}
|
||||
<meta name="monetization" content="{{.DisplayMonetization}}" />
|
||||
{{- end}}
|
||||
{{if .Verification -}}
|
||||
<link rel="me" href="{{.Verification}}" />
|
||||
{{- end}}
|
||||
{{end}}
|
||||
|
||||
{{define "highlighting"}}
|
||||
|
|
|
@ -153,6 +153,15 @@ textarea.section.norm {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<h2>Verification</h2>
|
||||
<div class="section">
|
||||
<p class="explain">Verify that you own another site on the open web, fediverse, etc. For example, enter your Mastodon profile address here, then on Mastodon add a link back to this blog — it will show up as <a href="https://joinmastodon.org/verification" target="mastoverified">verified</a> there.</p>
|
||||
<input type="text" name="verification_link" style="width:100%" value="{{.Verification}}" placeholder="https://writing.exchange/@writefreely" />
|
||||
<p class="explain">This adds a <code>rel="me"</code> code in your blog's <code><head></code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .UserPage.StaticPage.AppCfg.Monetization}}
|
||||
<div class="option">
|
||||
<h2>Web Monetization</h2>
|
||||
|
|
Loading…
Reference in a new issue