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 <- 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("text/html"))
        
        // Get document
        let! res = client.SendAsync(reqMessage) |> Async.AwaitTask
        
        // Verify webmention
        let webmentions = 
            match res.IsSuccessStatusCode with 
            | true ->
                // Get document contents
                let body = 
                    async {
                        return! res.Content.ReadAsStringAsync() |> Async.AwaitTask
                    } |> Async.RunSynchronously

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

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

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

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

                // Get untagged mentions
                let mentions = 
                    getMentionUsingCssSelector doc "a" target

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

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

            | false -> 
                Error "Unable to get source"
        return webmentions            
    }

verifyWebmentions source target
|> Async.RunSynchronously

Sample Output

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