2020-11-16 11:05:34 +00:00
package main
import (
"bytes"
2020-11-16 17:34:29 +00:00
"database/sql"
2020-11-16 11:05:34 +00:00
"errors"
2020-11-16 17:34:29 +00:00
"fmt"
2020-11-16 11:05:34 +00:00
"io"
"net/http"
"net/url"
2020-11-16 17:34:29 +00:00
"strings"
"time"
2020-11-16 11:05:34 +00:00
2020-11-16 13:18:14 +00:00
"github.com/PuerkitoBio/goquery"
2020-11-16 11:05:34 +00:00
"willnorris.com/go/microformats"
)
2020-11-16 17:34:29 +00:00
func startWebmentionVerifier ( ) {
go func ( ) {
for {
time . Sleep ( 30 * time . Second )
verifyNextWebmention ( )
2020-11-16 11:05:34 +00:00
}
2020-11-16 17:34:29 +00:00
} ( )
}
func verifyNextWebmention ( ) error {
m := & mention { }
oldStatus := ""
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 {
2020-11-16 11:05:34 +00:00
return nil
2020-11-16 17:34:29 +00:00
} else if err != nil {
return err
2020-11-16 11:05:34 +00:00
}
2020-11-16 17:34:29 +00:00
if err := wmVerify ( m ) ; err != nil {
// Invalid
return deleteWebmention ( m . ID )
}
if len ( m . Content ) > 500 {
m . Content = m . Content [ 0 : 497 ] + "…"
}
newStatus := webmentionStatusVerified
if strings . HasPrefix ( m . Source , appConfig . Server . PublicAddress ) {
// Approve if it's server-intern
newStatus = webmentionStatusApproved
}
_ , err = appDbExec ( "update webmentions set status = ?, title = ?, content = ?, author = ? where id = ?" , newStatus , m . Title , m . Content , m . Author , m . ID )
if oldStatus == string ( webmentionStatusNew ) {
sendNotification ( fmt . Sprintf ( "New webmention from %s to %s" , m . Source , m . Target ) )
}
return err
}
func wmVerify ( m * mention ) error {
2020-11-16 13:18:14 +00:00
req , err := http . NewRequest ( http . MethodGet , m . Source , nil )
2020-11-16 11:05:34 +00:00
if err != nil {
return err
}
2020-11-16 17:34:29 +00:00
req . Header . Set ( userAgent , appUserAgent )
resp , err := http . DefaultClient . Do ( req )
2020-11-16 11:05:34 +00:00
if err != nil {
return err
}
defer resp . Body . Close ( )
2020-11-16 13:18:14 +00:00
return wmVerifyReader ( resp . Body , m )
2020-11-16 11:05:34 +00:00
}
2020-11-16 13:18:14 +00:00
func wmVerifyReader ( body io . Reader , m * mention ) error {
var linksBuffer , gqBuffer , mfBuffer bytes . Buffer
io . Copy ( io . MultiWriter ( & linksBuffer , & gqBuffer , & mfBuffer ) , body )
// Check if source mentions target
links , err := allLinksFromHTML ( & linksBuffer , m . Source )
2020-11-16 11:05:34 +00:00
if err != nil {
return err
}
2020-11-16 13:18:14 +00:00
hasLink := false
for _ , link := range links {
2020-11-17 19:01:02 +00:00
if unescapedPath ( link ) == unescapedPath ( m . Target ) {
2020-11-16 13:18:14 +00:00
hasLink = true
break
2020-11-16 11:05:34 +00:00
}
}
2020-11-16 13:18:14 +00:00
if ! hasLink {
return errors . New ( "target not found in source" )
2020-11-16 11:05:34 +00:00
}
2020-11-16 13:18:14 +00:00
// Set title
doc , err := goquery . NewDocumentFromReader ( & gqBuffer )
if err != nil {
return err
}
if title := doc . Find ( "title" ) ; title != nil {
m . Title = title . Text ( )
}
// Fill mention attributes
sourceURL , err := url . Parse ( m . Source )
if err != nil {
return err
}
mfFillMentionFromData ( m , microformats . Parse ( & mfBuffer , sourceURL ) )
2020-11-16 11:05:34 +00:00
return nil
}
2020-11-16 13:18:14 +00:00
func mfFillMentionFromData ( m * mention , mf * microformats . Data ) {
2020-11-16 11:05:34 +00:00
for _ , i := range mf . Items {
2020-11-16 13:18:14 +00:00
mfFillMention ( m , i )
2020-11-16 11:05:34 +00:00
}
}
2020-11-16 13:18:14 +00:00
func mfFillMention ( m * mention , mf * microformats . Microformat ) bool {
2020-11-16 11:05:34 +00:00
if mfHasType ( mf , "h-entry" ) {
if name , ok := mf . Properties [ "name" ] ; ok && len ( name ) > 0 {
if title , ok := name [ 0 ] . ( string ) ; ok {
2020-11-16 13:18:14 +00:00
m . Title = title
2020-11-16 11:05:34 +00:00
}
}
if contents , ok := mf . Properties [ "content" ] ; ok && len ( contents ) > 0 {
2020-11-16 14:46:16 +00:00
if content , ok := contents [ 0 ] . ( map [ string ] string ) ; ok {
if contentValue , ok := content [ "value" ] ; ok {
m . Content = contentValue
2020-11-16 11:05:34 +00:00
}
}
}
if authors , ok := mf . Properties [ "author" ] ; ok && len ( authors ) > 0 {
if author , ok := authors [ 0 ] . ( * microformats . Microformat ) ; ok {
if names , ok := author . Properties [ "name" ] ; ok && len ( names ) > 0 {
2020-11-16 13:18:14 +00:00
if name , ok := names [ 0 ] . ( string ) ; ok {
m . Author = name
2020-11-16 11:05:34 +00:00
}
}
}
}
return true
} else if len ( mf . Children ) > 0 {
2020-11-16 13:18:14 +00:00
for _ , mfc := range mf . Children {
if mfFillMention ( m , mfc ) {
2020-11-16 11:05:34 +00:00
return true
}
}
}
return false
}
func mfHasType ( mf * microformats . Microformat , typ string ) bool {
for _ , t := range mf . Type {
if typ == t {
return true
}
}
return false
}