Webmentions Verification

Description

Sample script that shows how to perform Webmention verification per Webmentions specification.

Usage

dotnet fsi webmention-verification.fsx

Snippet

webmention-verification.fsx

// https://www.w3.org/TR/webmention/#webmention-verification

#r "nuget:FSharp.Data" #r "nuget: Microsoft.AspNetCore.WebUtilities, 2.2.0"

open System open System.Net open System.Net.Http open System.Net.Http.Headers open System.Collections.Generic open Microsoft.AspNetCore.WebUtilities open FSharp.Data

type WebmentionVerificationResult = | TaggedMention of {| Replies: bool; Likes: bool; Reposts: bool|} | UntaggedMention | Error of string

let getFormContent (request:HttpRequestMessage) = async { let! content = request.Content.ReadAsStringAsync() |> Async.AwaitTask let query = QueryHelpers.ParseQuery(content) let source = query["source"] |> Seq.head let target = query["target"] |> Seq.head

    return source,target
}

let cont =
dict [ ("source","https://raw.githubusercontent.com/lqdev/fsadvent-2021-webmentions/main/reply.html") ("target","https://webmention.rocks/test/1") ]

let buildSampleRequestMessage (content:IDictionary<string,string>) =

let reqMessage = new HttpRequestMessage()
reqMessage.Content &lt;- new FormUrlEncodedContent(content)

reqMessage

let req = buildSampleRequestMessage cont

// verification

let source,target = req |> getFormContent |> Async.RunSynchronously

let getMentionUsingCssSelector (doc:HtmlDocument) (selector:string) (target:string) = doc.CssSelect(selector) |> List.map(fun x -> x.AttributeValue("href")) |> List.filter(fun x -> x = target)

let hasMention (mentions:string list) = mentions |> List.isEmpty |> not

let verifyWebmentions (source:string) (target:string)= async { use client = new HttpClient() let reqMessage = new HttpRequestMessage(new HttpMethod("Get"), source) reqMessage.Headers.Accept.Clear()

    // Only accept text/html content
    reqMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(&quot;text/html&quot;))
    
    // Get document
    let! res = client.SendAsync(reqMessage) |&gt; Async.AwaitTask
    
    // Verify webmention
    let webmentions = 
        match res.IsSuccessStatusCode with 
        | true -&gt;
            // Get document contents
            let body = 
                async {
                    return! res.Content.ReadAsStringAsync() |&gt; Async.AwaitTask
                } |&gt; Async.RunSynchronously

            // Parse document
            let doc = HtmlDocument.Parse(body)

            // Get links tagged as replies using microformats
            let replies = 
                getMentionUsingCssSelector doc &quot;.u-in-reply-to&quot; target

            // Get links tagged as likes using microformats
            let likes = 
                getMentionUsingCssSelector doc &quot;.u-in-like-of&quot; target

            // Get links tagged as repost using microformats
            let shares = 
                getMentionUsingCssSelector doc &quot;.u-in-repost-of&quot; target

            // Get untagged mentions
            let mentions = 
                getMentionUsingCssSelector doc &quot;a&quot; target

            // Collect all tagged mentions
            let knownInteractions = 
                [replies;likes;shares] 
                |&gt; List.collect(id)

            // Choose tagged mentions before untagged mentions
            match knownInteractions.IsEmpty,mentions.IsEmpty with 
            | true,true -&gt; Error &quot;Target not mentioned&quot;
            | true,false | false,false -&gt; 
                TaggedMention 
                    {|
                        Replies = hasMention replies 
                        Likes = hasMention likes
                        Reposts = hasMention shares
                    |}
            | false,true -&gt; UntaggedMention 

        | false -&gt; 
            Error &quot;Unable to get source&quot;
    return webmentions            
}

verifyWebmentions source target |> Async.RunSynchronously

Sample Output

Interactions { 
    Likes = false
    Replies = true
    Shares = false }