about summary refs log tree commit diff
path: root/web/converse
diff options
context:
space:
mode:
authorVincent Ambo <mail@tazj.in>2021-04-05T20·09+0200
committertazjin <mail@tazj.in>2021-04-20T10·44+0000
commit929b38e8ae4d61340cbd70352663af8f6f2418cf (patch)
treeb2562c19bf3727bab14e11dabdffb3c8bd3569d0 /web/converse
parent54f59a5cc5835932c62c0f2d58712e30c248da4d (diff)
refactor(web/converse): Refactor first handlers to rouille r/2530
This commit starts the refactoring process towards dropping actix (and
tokio, ...). It builds, but at this commit, Converse does *not* work.

I decided to commit to avoid more ridiculous diffs.

Included changes:

* Added dependency on rouille.

* Refactored DbExecutor (formerly actix actor) to simply be a type
  with a few methods. Most actor messages still exist as they are
  being referred to by handlers.

* Started refactoring two of the handlers (and their related renderer
  functions) into Rouille's call scheme.

Important note: Rouille does not have safe session management out of
the box, and it will need to be implemented as this progresses.

Change-Id: I3e3f203e0705e561e1a3392e8f75dbe273d5fa81
Reviewed-on: https://cl.tvl.fyi/c/depot/+/2861
Tested-by: BuildkiteCI
Reviewed-by: tazjin <mail@tazj.in>
Diffstat (limited to 'web/converse')
-rw-r--r--web/converse/Cargo.lock349
-rw-r--r--web/converse/Cargo.toml1
-rw-r--r--web/converse/src/db.rs303
-rw-r--r--web/converse/src/errors.rs1
-rw-r--r--web/converse/src/handlers.rs53
-rw-r--r--web/converse/src/main.rs1
-rw-r--r--web/converse/src/render.rs119
7 files changed, 584 insertions, 243 deletions
diff --git a/web/converse/Cargo.lock b/web/converse/Cargo.lock
index 351ab1d261..b022bdd55e 100644
--- a/web/converse/Cargo.lock
+++ b/web/converse/Cargo.lock
@@ -81,8 +81,8 @@ dependencies = [
  "lazy_static",
  "lazycell",
  "log 0.4.14",
- "mime",
- "mime_guess",
+ "mime 0.3.16",
+ "mime_guess 2.0.3",
  "mio",
  "net2",
  "num_cpus",
@@ -135,6 +135,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
 [[package]]
+name = "adler32"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
+
+[[package]]
 name = "aead"
 version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -207,6 +213,24 @@ dependencies = [
 ]
 
 [[package]]
+name = "arrayref"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
+[[package]]
+name = "ascii"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97be891acc47ca214468e09425d02cef3af2c94d0d82081cd02061f996802f14"
+
+[[package]]
 name = "askama"
 version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -312,6 +336,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
 
 [[package]]
+name = "blake2b_simd"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "constant_time_eq",
+]
+
+[[package]]
 name = "block-buffer"
 version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -341,6 +376,16 @@ dependencies = [
 ]
 
 [[package]]
+name = "buf_redux"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
+dependencies = [
+ "memchr 2.3.4",
+ "safemem",
+]
+
+[[package]]
 name = "byteorder"
 version = "1.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -389,6 +434,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "chunked_transfer"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "498d20a7aaf62625b9bf26e637cf7736417cde1d0c99f1d04d1170229a85cf87"
+
+[[package]]
 name = "cipher"
 version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -439,6 +490,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "constant_time_eq"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
+
+[[package]]
 name = "converse"
 version = "0.1.0"
 dependencies = [
@@ -456,11 +513,12 @@ dependencies = [
  "hyper",
  "log 0.4.14",
  "md5",
- "mime_guess",
+ "mime_guess 2.0.3",
  "pq-sys",
  "pulldown-cmark",
  "r2d2",
  "rand 0.4.6",
+ "rouille",
  "serde",
  "serde_derive",
  "serde_json",
@@ -586,6 +644,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "crossbeam-utils"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49"
+dependencies = [
+ "autocfg 1.0.1",
+ "cfg-if 1.0.0",
+ "lazy_static",
+]
+
+[[package]]
 name = "crypto-mac"
 version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -635,6 +704,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "deflate"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4"
+dependencies = [
+ "adler32",
+ "byteorder",
+ "gzip-header",
+]
+
+[[package]]
 name = "diesel"
 version = "1.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -669,6 +749,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "dirs"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi 0.3.9",
+]
+
+[[package]]
 name = "dtoa"
 version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -798,6 +889,18 @@ dependencies = [
 ]
 
 [[package]]
+name = "filetime"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "redox_syscall 0.2.5",
+ "winapi 0.3.9",
+]
+
+[[package]]
 name = "flate2"
 version = "1.0.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -875,13 +978,24 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
 version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
 dependencies = [
  "cfg-if 1.0.0",
  "libc",
- "wasi",
+ "wasi 0.10.0+wasi-snapshot-preview1",
 ]
 
 [[package]]
@@ -901,6 +1015,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce"
 
 [[package]]
+name = "gzip-header"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0131feb3d3bb2a5a238d8a4d09f6353b7ebfdc52e77bccbf4ea6eaa751dde639"
+dependencies = [
+ "crc32fast",
+]
+
+[[package]]
 name = "h2"
 version = "0.1.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1004,7 +1127,7 @@ dependencies = [
  "iovec",
  "language-tags",
  "log 0.4.14",
- "mime",
+ "mime 0.3.16",
  "net2",
  "percent-encoding 1.0.1",
  "relay",
@@ -1013,7 +1136,7 @@ dependencies = [
  "tokio-io",
  "tokio-proto",
  "tokio-service",
- "unicase",
+ "unicase 2.6.0",
  "want",
 ]
 
@@ -1232,18 +1355,39 @@ dependencies = [
 
 [[package]]
 name = "mime"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0"
+dependencies = [
+ "log 0.3.9",
+]
+
+[[package]]
+name = "mime"
 version = "0.3.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
 
 [[package]]
 name = "mime_guess"
+version = "1.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "216929a5ee4dd316b1702eedf5e74548c123d370f47841ceaac38ca154690ca3"
+dependencies = [
+ "mime 0.2.6",
+ "phf",
+ "phf_codegen",
+ "unicase 1.4.2",
+]
+
+[[package]]
+name = "mime_guess"
 version = "2.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212"
 dependencies = [
- "mime",
- "unicase",
+ "mime 0.3.16",
+ "unicase 2.6.0",
 ]
 
 [[package]]
@@ -1309,6 +1453,24 @@ dependencies = [
 ]
 
 [[package]]
+name = "multipart"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adba94490a79baf2d6a23eac897157047008272fa3eecb3373ae6377b91eca28"
+dependencies = [
+ "buf_redux",
+ "httparse",
+ "log 0.4.14",
+ "mime 0.2.6",
+ "mime_guess 1.8.8",
+ "quick-error",
+ "rand 0.4.6",
+ "safemem",
+ "tempdir",
+ "twoway",
+]
+
+[[package]]
 name = "net2"
 version = "0.2.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1511,6 +1673,45 @@ dependencies = [
 ]
 
 [[package]]
+name = "phf"
+version = "0.7.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18"
+dependencies = [
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.7.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.7.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662"
+dependencies = [
+ "phf_shared",
+ "rand 0.6.5",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.7.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
+dependencies = [
+ "siphasher",
+ "unicase 1.4.2",
+]
+
+[[package]]
 name = "pkg-config"
 version = "0.3.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1737,7 +1938,7 @@ version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.2",
 ]
 
 [[package]]
@@ -1836,6 +2037,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "redox_users"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
+dependencies = [
+ "getrandom 0.1.16",
+ "redox_syscall 0.1.57",
+ "rust-argon2",
+]
+
+[[package]]
 name = "regex"
 version = "1.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1862,6 +2074,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
 name = "resolv-conf"
 version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1872,6 +2093,43 @@ dependencies = [
 ]
 
 [[package]]
+name = "rouille"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112568052ec17fa26c6c11c40acbb30d3ad244bf3d6da0be181f5e7e42e5004f"
+dependencies = [
+ "base64 0.9.3",
+ "brotli2",
+ "chrono",
+ "deflate",
+ "filetime",
+ "multipart",
+ "num_cpus",
+ "rand 0.5.6",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sha1",
+ "term",
+ "threadpool",
+ "time",
+ "tiny_http",
+ "url",
+]
+
+[[package]]
+name = "rust-argon2"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
+dependencies = [
+ "base64 0.13.0",
+ "blake2b_simd",
+ "constant_time_eq",
+ "crossbeam-utils 0.8.3",
+]
+
+[[package]]
 name = "rustc-demangle"
 version = "0.1.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2019,6 +2277,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "siphasher"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
+
+[[package]]
 name = "slab"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2161,6 +2425,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b157868d8ac1f56b64604539990685fa7611d8fa9e5476cf0c02cf34d32917c5"
 
 [[package]]
+name = "tempdir"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
+dependencies = [
+ "rand 0.4.6",
+ "remove_dir_all",
+]
+
+[[package]]
+name = "term"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42"
+dependencies = [
+ "byteorder",
+ "dirs",
+ "winapi 0.3.9",
+]
+
+[[package]]
 name = "termcolor"
 version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2179,16 +2464,39 @@ dependencies = [
 ]
 
 [[package]]
+name = "threadpool"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
+dependencies = [
+ "num_cpus",
+]
+
+[[package]]
 name = "time"
-version = "0.1.43"
+version = "0.1.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
+checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
 dependencies = [
  "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
  "winapi 0.3.9",
 ]
 
 [[package]]
+name = "tiny_http"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1661fa0a44c95d01604bd05c66732a446c657efb62b5164a7a083a3b552b4951"
+dependencies = [
+ "ascii",
+ "chrono",
+ "chunked_transfer",
+ "log 0.4.14",
+ "url",
+]
+
+[[package]]
 name = "tinyvec"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2553,6 +2861,15 @@ checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
 
 [[package]]
 name = "unicase"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33"
+dependencies = [
+ "version_check 0.1.5",
+]
+
+[[package]]
+name = "unicase"
 version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
@@ -2717,9 +3034,15 @@ dependencies = [
 
 [[package]]
 name = "wasi"
-version = "0.10.2+wasi-snapshot-preview1"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
 
 [[package]]
 name = "widestring"
diff --git a/web/converse/Cargo.toml b/web/converse/Cargo.toml
index c49f1c4c6a..c77101144d 100644
--- a/web/converse/Cargo.toml
+++ b/web/converse/Cargo.toml
@@ -30,6 +30,7 @@ tokio-timer = "0.2"
 url = "1.7"
 url_serde = "0.2"
 curl = "*" # bounded by crimp
+rouille = "3.0"
 
 [build-dependencies]
 pulldown-cmark = "0.1"
diff --git a/web/converse/src/db.rs b/web/converse/src/db.rs
index 1d61595898..ae186bdf4e 100644
--- a/web/converse/src/db.rs
+++ b/web/converse/src/db.rs
@@ -16,7 +16,8 @@
 // along with this program. If not, see
 // <https://www.gnu.org/licenses/>.
 
-//! This module implements the database connection actor.
+//! This module implements the database executor, which holds the
+//! database connection and performs queries on it.
 
 use actix::prelude::*;
 use diesel::{self, sql_query};
@@ -26,23 +27,32 @@ use diesel::r2d2::{Pool, ConnectionManager};
 use crate::models::*;
 use crate::errors::{ConverseError, Result};
 
-/// The DB actor itself. Several of these will be run in parallel by
-/// `SyncArbiter`.
-pub struct DbExecutor(pub Pool<ConnectionManager<PgConnection>>);
-
-impl Actor for DbExecutor {
-    type Context = SyncContext<Self>;
-}
+/// Raw PostgreSQL query used to perform full-text search on posts
+/// with a supplied phrase. For now, the query language is hardcoded
+/// to English and only "plain" queries (i.e. no searches for exact
+/// matches or more advanced query syntax) are supported.
+const SEARCH_QUERY: &'static str = r#"
+WITH search_query (query) AS (VALUES (plainto_tsquery('english', $1)))
+SELECT post_id,
+       thread_id,
+       author,
+       title,
+       ts_headline('english', body, query) AS headline
+  FROM search_index, search_query
+  WHERE document @@ query
+  ORDER BY ts_rank(document, query) DESC
+  LIMIT 50
+"#;
 
-/// Message used to request a list of threads.
-/// TODO: This should support page numbers.
-pub struct ListThreads;
-message!(ListThreads, Result<Vec<ThreadIndex>>);
+const REFRESH_QUERY: &'static str = "REFRESH MATERIALIZED VIEW search_index";
 
-impl Handler<ListThreads> for DbExecutor {
-    type Result = <ListThreads as Message>::Result;
+pub struct DbExecutor(pub Pool<ConnectionManager<PgConnection>>);
 
-    fn handle(&mut self, _: ListThreads, _: &mut Self::Context) -> Self::Result {
+impl DbExecutor {
+    /// Request a list of threads.
+    //
+    // TODO(tazjin): This should support pagination.
+    pub fn list_threads(&self) -> Result<Vec<ThreadIndex>> {
         use crate::schema::thread_index::dsl::*;
 
         let conn = self.0.get()?;
@@ -50,38 +60,25 @@ impl Handler<ListThreads> for DbExecutor {
             .load::<ThreadIndex>(&conn)?;
         Ok(results)
     }
-}
-
-/// Message used to look up a user based on their email-address. If
-/// the user does not exist, it is created.
-pub struct LookupOrCreateUser {
-    pub email: String,
-    pub name: String,
-}
-
-message!(LookupOrCreateUser, Result<User>);
-
-impl Handler<LookupOrCreateUser> for DbExecutor {
-    type Result = <LookupOrCreateUser as Message>::Result;
 
-    fn handle(&mut self,
-              msg: LookupOrCreateUser,
-              _: &mut Self::Context) -> Self::Result {
+    /// Look up a user based on their email-address. If the user does
+    /// not exist, it is created.
+    pub fn lookup_or_create_user(&self, user_email: &str, user_name: &str) -> Result<User> {
         use crate::schema::users;
         use crate::schema::users::dsl::*;
 
         let conn = self.0.get()?;
 
         let opt_user = users
-            .filter(email.eq(&msg.email))
+            .filter(email.eq(email))
             .first(&conn).optional()?;
 
         if let Some(user) = opt_user {
             Ok(user)
         } else {
             let new_user = NewUser {
-                email: msg.email,
-                name: msg.name,
+                email: user_email.to_string(),
+                name: user_name.to_string(),
             };
 
             let user: User = diesel::insert_into(users::table)
@@ -93,23 +90,15 @@ impl Handler<LookupOrCreateUser> for DbExecutor {
             Ok(user)
         }
     }
-}
 
-/// Message used to fetch a specific thread. Returns the thread and
-/// its posts.
-pub struct GetThread(pub i32);
-message!(GetThread, Result<(Thread, Vec<SimplePost>)>);
-
-impl Handler<GetThread> for DbExecutor {
-    type Result = <GetThread as Message>::Result;
-
-    fn handle(&mut self, msg: GetThread, _: &mut Self::Context) -> Self::Result {
+    /// Fetch a specific thread and return it with its posts.
+    pub fn get_thread(&self, thread_id: i32) -> Result<(Thread, Vec<SimplePost>)> {
         use crate::schema::threads::dsl::*;
         use crate::schema::simple_posts::dsl::id;
 
         let conn = self.0.get()?;
         let thread_result: Thread = threads
-            .find(msg.0).first(&conn)?;
+            .find(thread_id).first(&conn)?;
 
         let post_list = SimplePost::belonging_to(&thread_result)
             .order_by(id.asc())
@@ -117,59 +106,28 @@ impl Handler<GetThread> for DbExecutor {
 
         Ok((thread_result, post_list))
     }
-}
-
-/// Message used to fetch a specific post.
-#[derive(Deserialize, Debug)]
-pub struct GetPost { pub id: i32 }
-
-message!(GetPost, Result<SimplePost>);
 
-impl Handler<GetPost> for DbExecutor {
-    type Result = <GetPost as Message>::Result;
-
-    fn handle(&mut self, msg: GetPost, _: &mut Self::Context) -> Self::Result {
+    /// Fetch a specific post.
+    pub fn get_post(&self, post_id: i32) -> Result<SimplePost> {
         use crate::schema::simple_posts::dsl::*;
         let conn = self.0.get()?;
-        Ok(simple_posts.find(msg.id).first(&conn)?)
+        Ok(simple_posts.find(post_id).first(&conn)?)
     }
-}
-
-/// Message used to update the content of a post.
-#[derive(Deserialize)]
-pub struct UpdatePost {
-    pub post_id: i32,
-    pub post: String,
-}
 
-message!(UpdatePost, Result<Post>);
-
-impl Handler<UpdatePost> for DbExecutor {
-    type Result = Result<Post>;
-
-    fn handle(&mut self, msg: UpdatePost, _: &mut Self::Context) -> Self::Result {
+    /// Update the content of a post.
+    pub fn update_post(&self, post_id: i32, post_text: String) -> Result<Post> {
         use crate::schema::posts::dsl::*;
         let conn = self.0.get()?;
-        let updated = diesel::update(posts.find(msg.post_id))
-            .set(body.eq(msg.post))
+        let updated = diesel::update(posts.find(post_id))
+            .set(body.eq(post_text))
             .get_result(&conn)?;
 
         Ok(updated)
     }
-}
-
-/// Message used to create a new thread
-pub struct CreateThread {
-    pub new_thread: NewThread,
-    pub post: String,
-}
-message!(CreateThread, Result<Thread>);
-
-impl Handler<CreateThread> for DbExecutor {
-    type Result = <CreateThread as Message>::Result;
 
-    fn handle(&mut self, msg: CreateThread, _: &mut Self::Context) -> Self::Result {
-        use crate::schema::threads;
+    /// Create a new thread.
+    pub fn create_thread(&self, new_thread: NewThread, post_text: String) -> Result<Thread> {
+                use crate::schema::threads;
         use crate::schema::posts;
 
         let conn = self.0.get()?;
@@ -177,14 +135,14 @@ impl Handler<CreateThread> for DbExecutor {
         conn.transaction::<Thread, ConverseError, _>(|| {
             // First insert the thread structure itself
             let thread: Thread = diesel::insert_into(threads::table)
-                .values(&msg.new_thread)
+                .values(&new_thread)
                 .get_result(&conn)?;
 
             // ... then create the first post in the thread.
             let new_post = NewPost {
                 thread_id: thread.id,
-                body: msg.post,
-                user_id: msg.new_thread.user_id,
+                body: post_text,
+                user_id: new_thread.user_id,
             };
 
             diesel::insert_into(posts::table)
@@ -194,16 +152,9 @@ impl Handler<CreateThread> for DbExecutor {
             Ok(thread)
         })
     }
-}
-
-/// Message used to create a new reply
-pub struct CreatePost(pub NewPost);
-message!(CreatePost, Result<Post>);
-
-impl Handler<CreatePost> for DbExecutor {
-    type Result = <CreatePost as Message>::Result;
 
-    fn handle(&mut self, msg: CreatePost, _: &mut Self::Context) -> Self::Result {
+    /// Create a new post.
+    pub fn create_post(&self, new_post: NewPost) -> Result<Post> {
         use crate::schema::posts;
 
         let conn = self.0.get()?;
@@ -211,20 +162,136 @@ impl Handler<CreatePost> for DbExecutor {
         let closed: bool = {
             use crate::schema::threads::dsl::*;
             threads.select(closed)
-                .find(msg.0.thread_id)
+                .find(new_post.thread_id)
                 .first(&conn)?
         };
 
         if closed {
             return Err(ConverseError::ThreadClosed {
-                id: msg.0.thread_id
+                id: new_post.thread_id
             })
         }
 
         Ok(diesel::insert_into(posts::table)
-           .values(&msg.0)
+           .values(&new_post)
            .get_result(&conn)?)
     }
+
+    /// Search for posts.
+    pub fn search_posts(&self, query: String) -> Result<Vec<SearchResult>> {
+        let conn = self.0.get()?;
+
+        let search_results = sql_query(SEARCH_QUERY)
+            .bind::<Text, _>(query)
+            .get_results::<SearchResult>(&conn)?;
+
+        Ok(search_results)
+    }
+
+    /// Trigger a refresh of the view used for full-text searching.
+    pub fn refresh_search_view(&self) -> Result<()> {
+        let conn = self.0.get()?;
+        debug!("Refreshing search_index view in DB");
+        sql_query(REFRESH_QUERY).execute(&conn)?;
+        Ok(())
+    }
+}
+
+
+// Old actor implementation:
+
+impl Actor for DbExecutor {
+    type Context = SyncContext<Self>;
+}
+
+/// Message used to look up a user based on their email-address. If
+/// the user does not exist, it is created.
+pub struct LookupOrCreateUser {
+    pub email: String,
+    pub name: String,
+}
+
+message!(LookupOrCreateUser, Result<User>);
+
+impl Handler<LookupOrCreateUser> for DbExecutor {
+    type Result = <LookupOrCreateUser as Message>::Result;
+
+    fn handle(&mut self,
+              _: LookupOrCreateUser,
+              _: &mut Self::Context) -> Self::Result {
+        unimplemented!()
+    }
+}
+
+/// Message used to fetch a specific thread. Returns the thread and
+/// its posts.
+pub struct GetThread(pub i32);
+message!(GetThread, Result<(Thread, Vec<SimplePost>)>);
+
+impl Handler<GetThread> for DbExecutor {
+    type Result = <GetThread as Message>::Result;
+
+    fn handle(&mut self, _: GetThread, _: &mut Self::Context) -> Self::Result {
+        unimplemented!()
+    }
+}
+
+/// Message used to fetch a specific post.
+#[derive(Deserialize, Debug)]
+pub struct GetPost { pub id: i32 }
+
+message!(GetPost, Result<SimplePost>);
+
+impl Handler<GetPost> for DbExecutor {
+    type Result = <GetPost as Message>::Result;
+
+    fn handle(&mut self, _: GetPost, _: &mut Self::Context) -> Self::Result {
+        unimplemented!()
+    }
+}
+
+/// Message used to update the content of a post.
+#[derive(Deserialize)]
+pub struct UpdatePost {
+    pub post_id: i32,
+    pub post: String,
+}
+
+message!(UpdatePost, Result<Post>);
+
+impl Handler<UpdatePost> for DbExecutor {
+    type Result = Result<Post>;
+
+    fn handle(&mut self, _: UpdatePost, _: &mut Self::Context) -> Self::Result {
+        unimplemented!()
+    }
+}
+
+/// Message used to create a new thread
+pub struct CreateThread {
+    pub new_thread: NewThread,
+    pub post: String,
+}
+message!(CreateThread, Result<Thread>);
+
+impl Handler<CreateThread> for DbExecutor {
+    type Result = <CreateThread as Message>::Result;
+
+    fn handle(&mut self, _: CreateThread, _: &mut Self::Context) -> Self::Result {
+        unimplemented!()
+    }
+}
+
+/// Message used to create a new reply
+pub struct CreatePost(pub NewPost);
+message!(CreatePost, Result<Post>);
+
+impl Handler<CreatePost> for DbExecutor {
+    type Result = <CreatePost as Message>::Result;
+
+    fn handle(&mut self, _: CreatePost, _: &mut Self::Context) -> Self::Result {
+        unimplemented!()
+    }
 }
 
 /// Message used to search for posts
@@ -232,34 +299,11 @@ impl Handler<CreatePost> for DbExecutor {
 pub struct SearchPosts { pub query: String }
 message!(SearchPosts, Result<Vec<SearchResult>>);
 
-/// Raw PostgreSQL query used to perform full-text search on posts
-/// with a supplied phrase. For now, the query language is hardcoded
-/// to English and only "plain" queries (i.e. no searches for exact
-/// matches or more advanced query syntax) are supported.
-const SEARCH_QUERY: &'static str = r#"
-WITH search_query (query) AS (VALUES (plainto_tsquery('english', $1)))
-SELECT post_id,
-       thread_id,
-       author,
-       title,
-       ts_headline('english', body, query) AS headline
-  FROM search_index, search_query
-  WHERE document @@ query
-  ORDER BY ts_rank(document, query) DESC
-  LIMIT 50
-"#;
-
 impl Handler<SearchPosts> for DbExecutor {
     type Result = <SearchPosts as Message>::Result;
 
-    fn handle(&mut self, msg: SearchPosts, _: &mut Self::Context) -> Self::Result {
-        let conn = self.0.get()?;
-
-        let search_results = sql_query(SEARCH_QUERY)
-            .bind::<Text, _>(msg.query)
-            .get_results::<SearchResult>(&conn)?;
-
-        Ok(search_results)
+    fn handle(&mut self, _: SearchPosts, _: &mut Self::Context) -> Self::Result {
+        unimplemented!()
     }
 }
 
@@ -268,15 +312,10 @@ impl Handler<SearchPosts> for DbExecutor {
 pub struct RefreshSearchView;
 message!(RefreshSearchView, Result<()>);
 
-const REFRESH_QUERY: &'static str = "REFRESH MATERIALIZED VIEW search_index";
-
 impl Handler<RefreshSearchView> for DbExecutor {
     type Result = Result<()>;
 
     fn handle(&mut self, _: RefreshSearchView, _: &mut Self::Context) -> Self::Result {
-        let conn = self.0.get()?;
-        debug!("Refreshing search_index view in DB");
-        sql_query(REFRESH_QUERY).execute(&conn)?;
-        Ok(())
+        unimplemented!()
     }
 }
diff --git a/web/converse/src/errors.rs b/web/converse/src/errors.rs
index b079f41c4f..32507c51b0 100644
--- a/web/converse/src/errors.rs
+++ b/web/converse/src/errors.rs
@@ -34,6 +34,7 @@ use r2d2;
 use tokio_timer;
 
 pub type Result<T> = result::Result<T, ConverseError>;
+pub type ConverseResult<T> = result::Result<T, ConverseError>;
 
 #[derive(Debug, Fail)]
 pub enum ConverseError {
diff --git a/web/converse/src/handlers.rs b/web/converse/src/handlers.rs
index c4448c7483..0759cec5c1 100644
--- a/web/converse/src/handlers.rs
+++ b/web/converse/src/handlers.rs
@@ -30,12 +30,14 @@ use actix_web::middleware::identity::RequestIdentity;
 use actix_web::middleware::{Started, Middleware};
 use actix_web;
 use crate::db::*;
-use crate::errors::ConverseError;
+use crate::errors::{ConverseResult, ConverseError};
 use futures::Future;
 use crate::models::*;
 use crate::oidc::*;
 use crate::render::*;
 
+use rouille::{Request, Response};
+
 type ConverseResponse = Box<dyn Future<Item=HttpResponse, Error=ConverseError>>;
 
 const HTML: &'static str = "text/html";
@@ -54,15 +56,14 @@ pub struct AppState {
     pub renderer: Addr<Renderer>,
 }
 
-pub fn forum_index(state: State<AppState>) -> ConverseResponse {
-    state.db.send(ListThreads)
-        .flatten()
-        .and_then(move |res| state.renderer.send(IndexPage {
-            threads: res
-        }).from_err())
-        .flatten()
-        .map(|res| HttpResponse::Ok().content_type(HTML).body(res))
-        .responder()
+/// Serve the forum's index page.
+pub fn forum_index_rouille(db: &DbExecutor) -> ConverseResult<Response> {
+    let threads = db.list_threads()?;
+    Ok(Response::html(index_page(threads)?))
+}
+
+pub fn forum_index(_: State<AppState>) -> ConverseResponse {
+    unimplemented!()
 }
 
 /// Returns the ID of the currently logged in user. If there is no ID
@@ -78,23 +79,23 @@ pub fn get_user_id(req: &HttpRequest<AppState>) -> i32 {
     }
 }
 
-/// This handler retrieves and displays a single forum thread.
-pub fn forum_thread(state: State<AppState>,
-                    req: HttpRequest<AppState>,
-                    thread_id: Path<i32>) -> ConverseResponse {
-    let id = thread_id.into_inner();
-    let user = get_user_id(&req);
+pub fn get_user_id_rouille(_req: &Request) -> i32 {
+    // TODO(tazjin): Implement session support in rouille somehow.
+    ANONYMOUS
+}
 
-    state.db.send(GetThread(id))
-        .flatten()
-        .and_then(move |res| state.renderer.send(ThreadPage {
-            current_user: user,
-            thread: res.0,
-            posts: res.1,
-        }).from_err())
-        .flatten()
-        .map(|res| HttpResponse::Ok().content_type(HTML).body(res))
-        .responder()
+pub fn forum_thread_rouille(req: &Request, db: &DbExecutor, thread_id: i32)
+                            -> ConverseResult<Response> {
+    let user = get_user_id_rouille(&req);
+    let thread = db.get_thread(thread_id)?;
+    Ok(Response::html(thread_page(user, thread.0, thread.1)?))
+}
+
+/// This handler retrieves and displays a single forum thread.
+pub fn forum_thread(_: State<AppState>,
+                    _: HttpRequest<AppState>,
+                    _: Path<i32>) -> ConverseResponse {
+    unimplemented!()
 }
 
 /// This handler presents the user with the "New Thread" form.
diff --git a/web/converse/src/main.rs b/web/converse/src/main.rs
index d17626c17f..6d6e9ac710 100644
--- a/web/converse/src/main.rs
+++ b/web/converse/src/main.rs
@@ -30,6 +30,7 @@ extern crate log;
 #[macro_use]
 extern crate serde_derive;
 
+extern crate rouille;
 extern crate actix;
 extern crate actix_web;
 extern crate chrono;
diff --git a/web/converse/src/render.rs b/web/converse/src/render.rs
index cfc08377a6..749e77ef50 100644
--- a/web/converse/src/render.rs
+++ b/web/converse/src/render.rs
@@ -47,12 +47,6 @@ impl fmt::Display for FormattedDate {
     }
 }
 
-/// Message used to render the index page.
-pub struct IndexPage {
-    pub threads: Vec<ThreadIndex>,
-}
-message!(IndexPage, Result<String>);
-
 #[derive(Debug)]
 struct IndexThread {
     id: i32,
@@ -70,39 +64,6 @@ struct IndexPageTemplate {
     threads: Vec<IndexThread>,
 }
 
-impl Handler<IndexPage> for Renderer {
-    type Result = Result<String>;
-
-    fn handle(&mut self, msg: IndexPage, _: &mut Self::Context) -> Self::Result {
-        let threads: Vec<IndexThread> = msg.threads
-            .into_iter()
-            .map(|thread| IndexThread {
-                id: thread.thread_id,
-                title: thread.title, // escape_html(&thread.title),
-                sticky: thread.sticky,
-                closed: thread.closed,
-                posted: FormattedDate(thread.posted),
-                author_name: thread.thread_author,
-                post_author: thread.post_author,
-            })
-            .collect();
-
-        let tpl = IndexPageTemplate {
-            threads
-        };
-
-        tpl.render().map_err(|e| e.into())
-    }
-}
-
-/// Message used to render a thread.
-pub struct ThreadPage {
-    pub current_user: i32,
-    pub thread: Thread,
-    pub posts: Vec<SimplePost>,
-}
-message!(ThreadPage, Result<String>);
-
 // "Renderable" structures with data transformations applied.
 #[derive(Debug)]
 struct RenderablePost {
@@ -130,39 +91,6 @@ fn md5_hex(input: &[u8]) -> String {
     format!("{:x}", md5::compute(input))
 }
 
-fn prepare_thread(comrak: &ComrakOptions, page: ThreadPage) -> RenderableThreadPage {
-    let user = page.current_user;
-
-    let posts = page.posts.into_iter().map(|post| {
-        let editable = user != 1 && post.user_id == user;
-
-        RenderablePost {
-            id: post.id,
-            body: markdown_to_html(&post.body, comrak),
-            posted: FormattedDate(post.posted),
-            author_name: post.author_name.clone(),
-            author_gravatar: md5_hex(post.author_email.as_bytes()),
-            editable,
-        }
-    }).collect();
-
-    RenderableThreadPage {
-        posts,
-        closed: page.thread.closed,
-        id: page.thread.id,
-        title: page.thread.title,
-    }
-}
-
-impl Handler<ThreadPage> for Renderer {
-    type Result = Result<String>;
-
-    fn handle(&mut self, msg: ThreadPage, _: &mut Self::Context) -> Self::Result {
-        let renderable = prepare_thread(&self.comrak, msg);
-        renderable.render().map_err(|e| e.into())
-    }
-}
-
 /// The different types of editing modes supported by the editing
 /// template:
 #[derive(Debug, PartialEq)]
@@ -263,3 +191,50 @@ impl Handler<SearchResultPage> for Renderer {
         msg.render().map_err(|e| e.into())
     }
 }
+
+// TODO: actor-free implementation below
+
+/// Render the index page for the given thread list.
+pub fn index_page(threads: Vec<ThreadIndex>) -> Result<String> {
+    let threads: Vec<IndexThread> = threads
+        .into_iter()
+        .map(|thread| IndexThread {
+            id: thread.thread_id,
+            title: thread.title, // escape_html(&thread.title),
+            sticky: thread.sticky,
+            closed: thread.closed,
+            posted: FormattedDate(thread.posted),
+            author_name: thread.thread_author,
+            post_author: thread.post_author,
+        })
+        .collect();
+
+    let tpl = IndexPageTemplate { threads };
+    tpl.render().map_err(|e| e.into())
+}
+
+// Render the page of a given thread.
+pub fn thread_page(user: i32, thread: Thread, posts: Vec<SimplePost>) -> Result<String> {
+    let posts = posts.into_iter().map(|post| {
+        let editable = user != 1 && post.user_id == user;
+
+        let comrak = ComrakOptions::default(); // TODO(tazjin): cheddar
+        RenderablePost {
+            id: post.id,
+            body: markdown_to_html(&post.body, &comrak),
+            posted: FormattedDate(post.posted),
+            author_name: post.author_name.clone(),
+            author_gravatar: md5_hex(post.author_email.as_bytes()),
+            editable,
+        }
+    }).collect();
+
+    let renderable = RenderableThreadPage {
+        posts,
+        closed: thread.closed,
+        id: thread.id,
+        title: thread.title,
+    };
+
+    Ok(renderable.render()?)
+}