2020-11-06 17:45:31 +00:00
package main
import (
"context"
"database/sql"
"fmt"
"log"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"time"
"github.com/go-chi/chi"
wmd "github.com/zerok/webmentiond/pkg/webmention"
"willnorris.com/go/webmention"
)
type webmentionStatus string
const (
webmentionStatusNew webmentionStatus = "new"
webmentionStatusRenew webmentionStatus = "renew"
webmentionStatusVerified webmentionStatus = "verified"
webmentionStatusApproved webmentionStatus = "approved"
)
type mention struct {
ID int
Source string
Target string
Created int64
Title string
Content string
Author string
Type string
}
func initWebmention ( ) {
startWebmentionVerifier ( )
}
func startWebmentionVerifier ( ) {
go func ( ) {
for {
verifyNextWebmention ( )
2020-11-10 18:43:01 +00:00
time . Sleep ( 30 * time . Second )
2020-11-06 17:45:31 +00:00
}
} ( )
}
func handleWebmention ( w http . ResponseWriter , r * http . Request ) {
m , err := wmd . ExtractMention ( r )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
return
}
if ! isAllowedHost ( httptest . NewRequest ( http . MethodGet , m . Target , nil ) , r . URL . Host , appConfig . Server . Domain ) {
http . Error ( w , "target not allowed" , http . StatusBadRequest )
return
}
if err = createWebmention ( m . Source , m . Target ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
w . WriteHeader ( http . StatusAccepted )
_ , _ = fmt . Fprint ( w , "Webmention accepted" )
}
func webmentionAdmin ( w http . ResponseWriter , r * http . Request ) {
verified , err := getWebmentions ( & webmentionsRequestConfig {
status : webmentionStatusVerified ,
} )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
approved , err := getWebmentions ( & webmentionsRequestConfig {
status : webmentionStatusApproved ,
} )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
render ( w , "webmentionadmin" , & renderData {
Data : map [ string ] [ ] * mention {
"Verified" : verified ,
"Approved" : approved ,
} ,
} )
}
func webmentionAdminDelete ( w http . ResponseWriter , r * http . Request ) {
id , err := strconv . Atoi ( chi . URLParam ( r , "id" ) )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
return
}
err = deleteWebmention ( id )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
http . Redirect ( w , r , "/webmention/admin" , http . StatusFound )
return
}
func webmentionAdminApprove ( w http . ResponseWriter , r * http . Request ) {
id , err := strconv . Atoi ( chi . URLParam ( r , "id" ) )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
return
}
err = approveWebmention ( id )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
http . Redirect ( w , r , "/webmention/admin" , http . StatusFound )
return
}
func webmentionExists ( source , target string ) bool {
result := 0
2020-11-09 15:40:12 +00:00
row , err := appDbQueryRow ( "select exists(select 1 from webmentions where source = ? and target = ?)" , source , target )
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
}
func verifyNextWebmention ( ) error {
m := & mention { }
oldStatus := ""
2020-11-09 15:40:12 +00:00
row , err := appDbQueryRow ( "select id, source, target, status from webmentions where (status = ? or status = ?) limit 1" , webmentionStatusNew , webmentionStatusRenew )
if err != nil {
return err
}
if err := row . Scan ( & m . ID , & m . Source , & m . Target , & oldStatus ) ; err == sql . ErrNoRows {
return nil
} else if err != nil {
2020-11-06 17:45:31 +00:00
return err
}
wmm := & wmd . Mention {
Source : m . Source ,
Target : m . Target ,
}
if err := wmd . Verify ( context . Background ( ) , wmm , func ( c * wmd . VerifyOptions ) {
c . MaxRedirects = 15
} ) ; err != nil {
// Invalid
return deleteWebmention ( m . ID )
}
if len ( wmm . Content ) > 500 {
wmm . Content = wmm . Content [ 0 : 497 ] + "…"
}
2020-11-09 17:54:34 +00:00
newStatus := webmentionStatusVerified
if strings . HasPrefix ( wmm . Source , appConfig . Server . PublicAddress ) {
// Approve if it's server-intern
newStatus = webmentionStatusApproved
}
_ , err = appDbExec ( "update webmentions set status = ?, title = ?, type = ?, content = ?, author = ? where id = ?" , newStatus , wmm . Title , wmm . Type , wmm . Content , wmm . AuthorName , m . ID )
2020-11-06 17:45:31 +00:00
if oldStatus == string ( webmentionStatusNew ) {
sendNotification ( fmt . Sprintf ( "New webmention from %s to %s" , m . Source , m . Target ) )
}
return err
}
func createWebmention ( source , target string ) ( err error ) {
if webmentionExists ( source , target ) {
2020-11-09 15:40:12 +00:00
_ , err = appDbExec ( "update webmentions set status = ? where source = ? and target = ?" , webmentionStatusRenew , source , target )
2020-11-06 17:45:31 +00:00
} else {
2020-11-09 15:40:12 +00:00
_ , err = appDbExec ( "insert into webmentions (source, target, created) values (?, ?, ?)" , source , target , time . Now ( ) . Unix ( ) )
2020-11-06 17:45:31 +00:00
}
return err
}
func deleteWebmention ( id int ) error {
2020-11-09 15:40:12 +00:00
_ , err := appDbExec ( "delete from webmentions where id = ?" , id )
2020-11-06 17:45:31 +00:00
return err
}
func approveWebmention ( id int ) error {
2020-11-09 15:40:12 +00:00
_ , err := appDbExec ( "update webmentions set status = ? where id = ?" , webmentionStatusApproved , id )
2020-11-06 17:45:31 +00:00
return err
}
type webmentionsRequestConfig struct {
target string
status webmentionStatus
2020-11-11 15:58:44 +00:00
asc bool
2020-11-06 17:45:31 +00:00
}
func getWebmentions ( config * webmentionsRequestConfig ) ( [ ] * mention , error ) {
mentions := [ ] * mention { }
var rows * sql . Rows
var err error
args := [ ] interface { } { }
2020-11-09 15:40:12 +00:00
filter := ""
2020-11-06 17:45:31 +00:00
if config != nil {
2020-11-09 15:40:12 +00:00
if config . target != "" && config . status != "" {
filter = "where target = @target and status = @status"
args = append ( args , sql . Named ( "target" , config . target ) , sql . Named ( "status" , config . status ) )
} else if config . target != "" {
filter = "where target = @target"
args = append ( args , sql . Named ( "target" , config . target ) )
} else if config . status != "" {
filter = "where status = @status"
args = append ( args , sql . Named ( "status" , config . status ) )
2020-11-06 17:45:31 +00:00
}
}
2020-11-11 15:58:44 +00:00
order := "desc"
if config . asc {
order = "asc"
}
rows , err = appDbQuery ( "select id, source, target, created, title, content, author, type from webmentions " + filter + " order by created " + order , args ... )
2020-11-06 17:45:31 +00:00
if err != nil {
return nil , err
}
for rows . Next ( ) {
m := & mention { }
err = rows . Scan ( & m . ID , & m . Source , & m . Target , & m . Created , & m . Title , & m . Content , & m . Author , & m . Type )
if err != nil {
return nil , err
}
mentions = append ( mentions , m )
}
return mentions , nil
}
2020-11-09 15:40:12 +00:00
func ( p * post ) sendWebmentions ( ) error {
url := appConfig . Server . PublicAddress + p . Path
recorder := httptest . NewRecorder ( )
// Render basic post data
render ( recorder , "postbasic" , & renderData {
blogString : p . Blog ,
Data : p ,
} )
discovered , err := webmention . DiscoverLinksFromReader ( recorder . Result ( ) . Body , url , ".h-entry" )
2020-11-06 17:45:31 +00:00
if err != nil {
return err
}
2020-11-09 15:40:12 +00:00
client := webmention . New ( nil )
2020-11-06 17:45:31 +00:00
for _ , link := range discovered {
2020-11-09 15:40:12 +00:00
if strings . HasPrefix ( link , appConfig . Server . PublicAddress ) {
// Save mention directly
createWebmention ( url , link )
continue
2020-11-06 17:45:31 +00:00
}
endpoint , err := client . DiscoverEndpoint ( link )
if err != nil || len ( endpoint ) < 1 {
continue
}
_ , err = client . SendWebmention ( endpoint , url , link )
if err != nil {
log . Println ( "Sending webmention to " + link + " failed" )
continue
}
log . Println ( "Sent webmention to " + link )
}
return nil
}