2020-11-06 17:45:31 +00:00
package main
import (
"database/sql"
2020-11-16 13:18:14 +00:00
"errors"
2020-11-06 17:45:31 +00:00
"fmt"
"net/http"
"strings"
"time"
2021-06-18 12:32:03 +00:00
2021-06-28 20:17:18 +00:00
"go.goblog.app/app/pkgs/contenttype"
2020-11-06 17:45:31 +00:00
)
type webmentionStatus string
const (
webmentionStatusVerified webmentionStatus = "verified"
webmentionStatusApproved webmentionStatus = "approved"
2021-01-30 18:37:26 +00:00
webmentionPath = "/webmention"
2020-11-06 17:45:31 +00:00
)
type mention struct {
ID int
Source string
Target string
Created int64
Title string
Content string
Author string
2021-01-30 18:37:26 +00:00
Status webmentionStatus
2020-11-06 17:45:31 +00:00
}
2021-06-06 12:39:42 +00:00
func ( a * goBlog ) initWebmention ( ) {
2020-11-17 21:10:13 +00:00
// Add hooks
hookFunc := func ( p * post ) {
2021-07-14 13:44:57 +00:00
_ = a . sendWebmentions ( p )
2020-11-17 21:10:13 +00:00
}
2021-06-06 12:39:42 +00:00
a . pPostHooks = append ( a . pPostHooks , hookFunc )
a . pUpdateHooks = append ( a . pUpdateHooks , hookFunc )
a . pDeleteHooks = append ( a . pDeleteHooks , hookFunc )
2020-11-17 21:10:13 +00:00
// Start verifier
2021-06-06 12:39:42 +00:00
a . initWebmentionQueue ( )
2020-11-06 17:45:31 +00:00
}
2021-06-06 12:39:42 +00:00
func ( a * goBlog ) handleWebmention ( w http . ResponseWriter , r * http . Request ) {
2021-08-03 19:32:02 +00:00
m , err := a . extractMention ( r )
2020-11-06 17:45:31 +00:00
if err != nil {
2021-08-03 19:32:02 +00:00
a . debug ( "Error extracting webmention:" , err . Error ( ) )
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , err . Error ( ) , http . StatusBadRequest )
2020-11-06 17:45:31 +00:00
return
}
2021-06-30 06:04:30 +00:00
if ! strings . HasPrefix ( m . Target , a . cfg . Server . PublicAddress ) {
2021-08-03 19:32:02 +00:00
a . debug ( "Webmention target not allowed:" , m . Target )
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , "target not allowed" , http . StatusBadRequest )
2020-11-06 17:45:31 +00:00
return
}
2021-06-06 12:39:42 +00:00
if err = a . queueMention ( m ) ; err != nil {
2021-08-03 19:32:02 +00:00
a . debug ( "Failed to queue webmention" , err . Error ( ) )
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , err . Error ( ) , http . StatusInternalServerError )
2020-11-06 17:45:31 +00:00
return
}
w . WriteHeader ( http . StatusAccepted )
_ , _ = fmt . Fprint ( w , "Webmention accepted" )
2021-08-03 19:32:02 +00:00
a . debug ( "Accepted webmention:" , m . Source , m . Target )
2020-11-06 17:45:31 +00:00
}
2021-08-03 19:32:02 +00:00
func ( a * goBlog ) extractMention ( r * http . Request ) ( * mention , error ) {
if ct := r . Header . Get ( contentType ) ; ! strings . Contains ( ct , contenttype . WWWForm ) {
a . debug ( "New webmention request with wrong content type:" , ct )
2021-04-23 17:36:57 +00:00
return nil , errors . New ( "unsupported Content-Type" )
2020-11-16 13:18:14 +00:00
}
err := r . ParseForm ( )
if err != nil {
return nil , err
}
source := r . Form . Get ( "source" )
2020-11-25 11:36:14 +00:00
target := unescapedPath ( r . Form . Get ( "target" ) )
2020-11-16 13:18:14 +00:00
if source == "" || target == "" || ! isAbsoluteURL ( source ) || ! isAbsoluteURL ( target ) {
2021-08-03 19:32:02 +00:00
a . debug ( "Invalid webmention request, source:" , source , "target:" , target )
2021-04-23 17:36:57 +00:00
return nil , errors . New ( "invalid request" )
2020-11-16 13:18:14 +00:00
}
return & mention {
2020-11-25 11:36:14 +00:00
Source : source ,
Target : target ,
Created : time . Now ( ) . Unix ( ) ,
2020-11-16 13:18:14 +00:00
} , nil
}
2021-06-06 12:39:42 +00:00
func ( db * database ) webmentionExists ( source , target string ) bool {
2020-11-06 17:45:31 +00:00
result := 0
2021-08-09 11:09:45 +00:00
row , err := db . queryRow ( "select exists(select 1 from webmentions where lowerx(source) = lowerx(@source) and lowerx(target) = lowerx(@target))" , sql . Named ( "source" , source ) , sql . Named ( "target" , target ) )
2020-11-09 15:40:12 +00:00
if err != nil {
return false
}
if err = row . Scan ( & result ) ; err != nil {
2020-11-06 17:45:31 +00:00
return false
}
return result == 1
}
2021-06-06 12:39:42 +00:00
func ( a * goBlog ) createWebmention ( source , target string ) ( err error ) {
return a . queueMention ( & mention {
2020-11-25 11:36:14 +00:00
Source : source ,
Target : unescapedPath ( target ) ,
Created : time . Now ( ) . Unix ( ) ,
} )
2020-11-06 17:45:31 +00:00
}
2021-08-09 11:09:45 +00:00
func ( db * database ) insertWebmention ( m * mention , status webmentionStatus ) error {
_ , err := db . exec (
`
insert into webmentions ( source , target , created , status , title , content , author )
values ( @ source , @ target , @ created , @ status , @ title , @ content , @ author )
` ,
sql . Named ( "source" , m . Source ) ,
sql . Named ( "target" , m . Target ) ,
sql . Named ( "created" , m . Created ) ,
sql . Named ( "status" , status ) ,
sql . Named ( "title" , m . Title ) ,
sql . Named ( "content" , m . Content ) ,
sql . Named ( "author" , m . Author ) ,
)
return err
}
2021-06-06 12:39:42 +00:00
func ( db * database ) deleteWebmention ( id int ) error {
_ , err := db . exec ( "delete from webmentions where id = @id" , sql . Named ( "id" , id ) )
2020-11-06 17:45:31 +00:00
return err
}
2021-06-06 12:39:42 +00:00
func ( db * database ) approveWebmention ( id int ) error {
_ , err := db . exec ( "update webmentions set status = ? where id = ?" , webmentionStatusApproved , id )
2020-11-06 17:45:31 +00:00
return err
}
2021-06-06 12:39:42 +00:00
func ( a * goBlog ) reverifyWebmention ( id int ) error {
m , err := a . db . getWebmentions ( & webmentionsRequestConfig {
2021-05-23 18:11:48 +00:00
id : id ,
limit : 1 ,
} )
if err != nil {
return err
}
if len ( m ) > 0 {
2021-06-06 12:39:42 +00:00
err = a . queueMention ( m [ 0 ] )
2021-05-23 18:11:48 +00:00
}
2021-05-24 07:12:46 +00:00
return err
2021-05-23 18:11:48 +00:00
}
2020-11-06 17:45:31 +00:00
type webmentionsRequestConfig struct {
2021-01-30 18:37:26 +00:00
target string
status webmentionStatus
2021-05-24 08:09:37 +00:00
sourcelike string
2021-05-23 18:11:48 +00:00
id int
2021-01-30 18:37:26 +00:00
asc bool
offset , limit int
2020-11-06 17:45:31 +00:00
}
2021-01-30 18:37:26 +00:00
func buildWebmentionsQuery ( config * webmentionsRequestConfig ) ( query string , args [ ] interface { } ) {
2021-07-23 15:26:14 +00:00
var queryBuilder strings . Builder
queryBuilder . WriteString ( "select id, source, target, created, title, content, author, status from webmentions " )
2020-11-06 17:45:31 +00:00
if config != nil {
2021-07-23 15:26:14 +00:00
queryBuilder . WriteString ( "where 1" )
2021-05-24 08:09:37 +00:00
if config . target != "" {
2021-08-09 11:09:45 +00:00
queryBuilder . WriteString ( " and lowerx(target) = lowerx(@target)" )
2020-11-09 15:40:12 +00:00
args = append ( args , sql . Named ( "target" , config . target ) )
2021-05-24 08:09:37 +00:00
}
if config . status != "" {
2021-07-23 15:26:14 +00:00
queryBuilder . WriteString ( " and status = @status" )
2020-11-09 15:40:12 +00:00
args = append ( args , sql . Named ( "status" , config . status ) )
2021-05-24 08:09:37 +00:00
}
if config . sourcelike != "" {
2021-08-09 11:09:45 +00:00
queryBuilder . WriteString ( " and lowerx(source) like ('%' || lowerx(@sourcelike) || '%')" )
args = append ( args , sql . Named ( "sourcelike" , config . sourcelike ) )
2021-05-24 08:09:37 +00:00
}
if config . id != 0 {
2021-07-23 15:26:14 +00:00
queryBuilder . WriteString ( " and id = @id" )
2021-05-23 18:11:48 +00:00
args = append ( args , sql . Named ( "id" , config . id ) )
2020-11-06 17:45:31 +00:00
}
}
2021-07-23 15:26:14 +00:00
queryBuilder . WriteString ( " order by created " )
2020-11-11 15:58:44 +00:00
if config . asc {
2021-07-23 15:26:14 +00:00
queryBuilder . WriteString ( "asc" )
} else {
queryBuilder . WriteString ( "desc" )
2020-11-11 15:58:44 +00:00
}
2021-01-30 18:37:26 +00:00
if config . limit != 0 || config . offset != 0 {
2021-07-23 15:26:14 +00:00
queryBuilder . WriteString ( " limit @limit offset @offset" )
2021-01-30 18:37:26 +00:00
args = append ( args , sql . Named ( "limit" , config . limit ) , sql . Named ( "offset" , config . offset ) )
}
2021-07-23 15:26:14 +00:00
return queryBuilder . String ( ) , args
2021-01-30 18:37:26 +00:00
}
2021-06-06 12:39:42 +00:00
func ( db * database ) getWebmentions ( config * webmentionsRequestConfig ) ( [ ] * mention , error ) {
2021-01-30 18:37:26 +00:00
mentions := [ ] * mention { }
query , args := buildWebmentionsQuery ( config )
2021-06-06 12:39:42 +00:00
rows , err := db . query ( query , args ... )
2020-11-06 17:45:31 +00:00
if err != nil {
return nil , err
}
for rows . Next ( ) {
m := & mention { }
2021-01-30 18:37:26 +00:00
err = rows . Scan ( & m . ID , & m . Source , & m . Target , & m . Created , & m . Title , & m . Content , & m . Author , & m . Status )
2020-11-06 17:45:31 +00:00
if err != nil {
return nil , err
}
mentions = append ( mentions , m )
}
return mentions , nil
}
2021-01-30 18:37:26 +00:00
2021-06-18 12:32:03 +00:00
func ( db * database ) getWebmentionsByAddress ( address string ) [ ] * mention {
mentions , _ := db . getWebmentions ( & webmentionsRequestConfig {
target : address ,
status : webmentionStatusApproved ,
asc : true ,
} )
return mentions
}
2021-06-06 12:39:42 +00:00
func ( db * database ) countWebmentions ( config * webmentionsRequestConfig ) ( count int , err error ) {
2021-01-30 18:37:26 +00:00
query , params := buildWebmentionsQuery ( config )
query = "select count(*) from (" + query + ")"
2021-06-06 12:39:42 +00:00
row , err := db . queryRow ( query , params ... )
2021-01-30 18:37:26 +00:00
if err != nil {
return
}
err = row . Scan ( & count )
return
}