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-01-15 20:56:46 +00:00
if p . Status == statusPublished {
2021-06-06 12:39:42 +00:00
_ = a . sendWebmentions ( p )
2021-01-15 20:56:46 +00:00
}
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 ) {
2020-11-16 13:18:14 +00:00
m , err := extractMention ( r )
2020-11-06 17:45:31 +00:00
if err != nil {
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-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 {
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" )
}
2020-11-16 13:18:14 +00:00
func extractMention ( r * http . Request ) ( * mention , error ) {
2021-06-18 12:32:03 +00:00
if ! strings . Contains ( r . Header . Get ( contentType ) , contenttype . WWWForm ) {
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-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-06-29 21:19:58 +00:00
row , err := db . queryRow ( "select exists(select 1 from webmentions where lower(source) = lower(@source) and lower(target) = lower(@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-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 { } ) {
args = [ ] interface { } { }
2020-11-09 15:40:12 +00:00
filter := ""
2020-11-06 17:45:31 +00:00
if config != nil {
2021-05-24 08:09:37 +00:00
filter = "where 1 = 1"
if config . target != "" {
2021-06-29 21:19:58 +00:00
filter += " and lower(target) = lower(@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 != "" {
filter += " 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-06-29 21:19:58 +00:00
filter += " and lower(source) like @sourcelike"
args = append ( args , sql . Named ( "sourcelike" , "%" + strings . ToLower ( config . sourcelike ) + "%" ) )
2021-05-24 08:09:37 +00:00
}
if config . id != 0 {
filter += " 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
}
}
2020-11-11 15:58:44 +00:00
order := "desc"
if config . asc {
order = "asc"
}
2021-01-30 18:37:26 +00:00
query = "select id, source, target, created, title, content, author, status from webmentions " + filter + " order by created " + order
if config . limit != 0 || config . offset != 0 {
query += " limit @limit offset @offset"
args = append ( args , sql . Named ( "limit" , config . limit ) , sql . Named ( "offset" , config . offset ) )
}
return query , args
}
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
}