Home

Awesome

Waq

Waq is yet another ActivityPub server implementation written in OCaml.

A blog post about Waq is here. Its original Japanese version is there.

Note for those trying to deploy Waq

Although Waq can be deployed and used well for your daily microblogging, it currently lacks many of the features you would expect from a standard SNS, such as post privacy, custom emojis, profile editing, and so on. At the moment, I would not recommend using Waq as your primary SNS.

Quick start

Install docker beforehand. Then:

cd e2e
make start-ngrok
make create-cluster
make start-waq
make start-mastodon
make start-elk
make waq-port-forward &
make mastodon-port-forward &
make elk-port-forward &
cat _test_waq # Access this domain for Waq.
cat _test_mastodon # Access this domain for Mastodon.
cat _test_elk # Access this domain for Elk.

Create a demo user:

kubectl exec -n e2e deploy/waq-web -- /waq/waq user:register --username=demo --password=demo --display-name=demo --email=demo@example.com

When shutting down:

make clean-cluster
make stop-ngrok

How to run E2E tests

cd e2e
make start-ngrok && make create-cluster && make test

Technology stack

The OCaml libraries that Waq depends on (The full list is here).

Original O/R mapper (lib_sqlx)

To make it easy to construct typical SQL queries and handle their results, Waq has an original O/R mapper library lib_sqlx. It has a PPX driver that allows us to write SQL schemas as OCaml code (see lib/schema.ml for an example).

An example usage of lib_sqlx (working code is here):

open Sqlx

[%%sqlx.schemas
(* Define two RDB schemas (accounts and statuses) *)
[%%sqlx.schemas
module rec Account = struct
  name "accounts"

  class type t =
    object
      (* Table `accounts` has 3 columns ... *)
      val username : string
      val domain : string option
      val display_name : string

      (* ... and also has many `statuses` thorugh foreign key *)
      val statuses : Status.t list [@@foreign_key `account_id]
    end
end

and Status = struct
  name "statuses"

  class type t =
    object
      (* Table `statuses` has 4 columns *)
      val text : string
      val in_reply_to_id : ID.t option
      val reblog_of_id : ID.t option
      val account_id : Account.ID.t
    end
end]

module Db = struct
  include Engine.Make (Driver_pg)
end

(**)

(* Insert some records into table `statuses` for testing *)
let insert_some_statuses () : unit Lwt.t =
  (* Insert two accounts (`a1` and `a2`) *)
  let%lwt a1 =
    Db.e Account.(make ~username:"user1" ~display_name:"User 1" () |> save_one)
  in
  let%lwt a2 =
    Db.e
      Account.(
        make ~username:"user2" ~domain:"example.com" ~display_name:"User 2" ()
        |> save_one)
  in
  (* Insert two statuses published by the accounts `a1` and `a2`. The second status is a reply to the first one.  *)
  let%lwt s1 =
    Db.e Status.(make ~account_id:a1#id ~text:"Hello" () |> save_one)
  in
  let%lwt _ =
    Db.e
      Status.(
        make ~account_id:a2#id ~text:"World" ~in_reply_to_id:s1#id ()
        |> save_one)
  in
  Lwt.return_unit
  [@@warning "-8"]

(* Select all statuses that `username` has replied *)
let statuses_replied_by ~(username : string) : Status.t list Lwt.t =
  let%lwt acct =
    Db.e Account.(get_one ~username ~preload:[ `statuses [ `in_reply_to [] ] ])
  in
  acct#statuses |> List.filter_map (fun s -> s#in_reply_to) |> Lwt.return

License

MIT