diff --git a/Cargo.lock b/Cargo.lock
index 7f8df04..266e1fd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -244,19 +244,6 @@ dependencies = [
"typenum",
]
-[[package]]
-name = "dashmap"
-version = "5.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
-dependencies = [
- "cfg-if",
- "hashbrown",
- "lock_api",
- "once_cell",
- "parking_lot_core",
-]
-
[[package]]
name = "digest"
version = "0.10.6"
@@ -338,31 +325,6 @@ dependencies = [
"percent-encoding",
]
-[[package]]
-name = "forwarded-header-value"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
-dependencies = [
- "nonempty",
- "thiserror",
-]
-
-[[package]]
-name = "futures"
-version = "0.3.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-io",
- "futures-sink",
- "futures-task",
- "futures-util",
-]
-
[[package]]
name = "futures-channel"
version = "0.3.26"
@@ -370,7 +332,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5"
dependencies = [
"futures-core",
- "futures-sink",
]
[[package]]
@@ -379,23 +340,6 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608"
-[[package]]
-name = "futures-executor"
-version = "0.3.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e"
-dependencies = [
- "futures-core",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
-name = "futures-io"
-version = "0.3.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531"
-
[[package]]
name = "futures-macro"
version = "0.3.26"
@@ -419,25 +363,15 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366"
-[[package]]
-name = "futures-timer"
-version = "3.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
-
[[package]]
name = "futures-util"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1"
dependencies = [
- "futures-channel",
"futures-core",
- "futures-io",
"futures-macro",
- "futures-sink",
"futures-task",
- "memchr",
"pin-project-lite",
"pin-utils",
"slab",
@@ -462,7 +396,7 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasi",
"wasm-bindgen",
]
@@ -476,24 +410,6 @@ dependencies = [
"weezl",
]
-[[package]]
-name = "governor"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c390a940a5d157878dd057c78680a33ce3415bcd05b4799509ea44210914b4d5"
-dependencies = [
- "cfg-if",
- "dashmap",
- "futures",
- "futures-timer",
- "no-std-compat",
- "nonzero_ext",
- "parking_lot",
- "quanta",
- "rand",
- "smallvec",
-]
-
[[package]]
name = "half"
version = "2.2.1"
@@ -709,15 +625,6 @@ dependencies = [
"cfg-if",
]
-[[package]]
-name = "mach"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
-dependencies = [
- "libc",
-]
-
[[package]]
name = "matchit"
version = "0.7.0"
@@ -772,7 +679,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
dependencies = [
"libc",
"log",
- "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasi",
"windows-sys",
]
@@ -785,24 +692,6 @@ dependencies = [
"getrandom",
]
-[[package]]
-name = "no-std-compat"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
-
-[[package]]
-name = "nonempty"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
-
-[[package]]
-name = "nonzero_ext"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
-
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@@ -959,22 +848,6 @@ dependencies = [
"unicode-ident",
]
-[[package]]
-name = "quanta"
-version = "0.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8"
-dependencies = [
- "crossbeam-utils",
- "libc",
- "mach",
- "once_cell",
- "raw-cpuid",
- "wasi 0.10.2+wasi-snapshot-preview1",
- "web-sys",
- "winapi",
-]
-
[[package]]
name = "quote"
version = "1.0.23"
@@ -1014,15 +887,6 @@ dependencies = [
"getrandom",
]
-[[package]]
-name = "raw-cpuid"
-version = "10.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6823ea29436221176fe662da99998ad3b4db2c7f31e7b6f5fe43adccd6320bb"
-dependencies = [
- "bitflags",
-]
-
[[package]]
name = "rayon"
version = "1.6.1"
@@ -1224,26 +1088,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8"
-[[package]]
-name = "thiserror"
-version = "1.0.38"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
-dependencies = [
- "thiserror-impl",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.38"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
[[package]]
name = "thread_local"
version = "1.1.4"
@@ -1414,26 +1258,6 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
-[[package]]
-name = "tower_governor"
-version = "0.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c6be418f6d18863291f0a7fa1da1de71495a19a54b5fb44969136f731a47e86"
-dependencies = [
- "axum",
- "forwarded-header-value",
- "futures",
- "futures-core",
- "governor",
- "http",
- "pin-project",
- "thiserror",
- "tokio",
- "tower",
- "tower-layer",
- "tracing",
-]
-
[[package]]
name = "tracing"
version = "0.1.37"
@@ -1548,12 +1372,6 @@ dependencies = [
"try-lock",
]
-[[package]]
-name = "wasi"
-version = "0.10.2+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
-
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@@ -1614,16 +1432,6 @@ version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
-[[package]]
-name = "web-sys"
-version = "0.3.60"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
-dependencies = [
- "js-sys",
- "wasm-bindgen",
-]
-
[[package]]
name = "weezl"
version = "0.1.7"
@@ -1726,7 +1534,6 @@ dependencies = [
"tower",
"tower-cookies",
"tower-http",
- "tower_governor",
"tracing",
"tracing-subscriber",
]
diff --git a/Cargo.toml b/Cargo.toml
index 244a7e9..9b460dc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,7 +7,6 @@ edition = "2021"
tokio = { version = "1.25.0", features = ["full"] }
axum = { version = "0.6.4", features = ["headers", "query"] }
tower-http = { version = "0.3.5", features = ["fs"] }
-tower_governor = "0.0.4"
tower-cookies = "0.8.0"
tower = "0.4.13"
tracing = "0.1.37"
@@ -19,4 +18,4 @@ rusqlite = { version = "0.28.0", features = ["bundled"] }
rand = "0.8.5"
time = "0.3.17"
lazy_static = "1.4"
-image = "0.24.5"
\ No newline at end of file
+image = "0.24.5"
diff --git a/public/chat.html b/public/chat.html
new file mode 100644
index 0000000..e293210
--- /dev/null
+++ b/public/chat.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+ XSSBook - Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/css/api.css b/public/css/api.css
index 8358538..6e5e612 100644
--- a/public/css/api.css
+++ b/public/css/api.css
@@ -123,6 +123,10 @@ h2 {
background-color: #bfa354;
}
+.delete {
+ background-color: #cc0000;
+}
+
.key {
margin-left: 40px;
-}
\ No newline at end of file
+}
diff --git a/public/css/main.css b/public/css/main.css
index d5ac0bf..352a3b4 100644
--- a/public/css/main.css
+++ b/public/css/main.css
@@ -431,4 +431,4 @@ form {
transform: rotate(360deg);
}
}
-
\ No newline at end of file
+
diff --git a/public/js/chat.js b/public/js/chat.js
new file mode 100644
index 0000000..41ba2ba
--- /dev/null
+++ b/public/js/chat.js
@@ -0,0 +1,37 @@
+import { div, pfp, p, parse, button, body, a, textarea, span, crawl } from './main.js'
+import { loadself, loadpostspage, createpost, loadusers } from './api.js'
+import { parsePost, header } from './components.js'
+
+function render() {
+
+ let new_body =
+ body({},
+ ...header(false, false, true, data.self.user_id),
+ )
+
+ document.body.replaceWith(new_body)
+
+}
+
+const data = {
+ self: {},
+ users: []
+}
+
+async function init() {
+
+ let request = (await loadself());
+ data.self = request.json
+
+ if (request.json == undefined) {
+ location.href = '/login'
+ return
+ }
+
+ data.users[data.self.user_id] = data.self
+
+ render()
+}
+
+
+init()
diff --git a/public/js/components.js b/public/js/components.js
index 7e2a268..f247875 100644
--- a/public/js/components.js
+++ b/public/js/components.js
@@ -1,9 +1,9 @@
-import { div, a, pfp, span, i, parse, parseDate, p, form, input, svg, path, parseMonth } from './main.js'
+import { div, a, pfp, span, i, parse, parseDate, p, form, input, svg, path, parseMonth, g } from './main.js'
import { postlike, postcomment, loadcommentspage } from './api.js';
window.parse = parse;
-export function header(home, people, user_id) {
+export function header(home, people, chat, user_id) {
return [
div({id: 'header'},
span({class: 'logo'},
@@ -21,6 +21,16 @@ export function header(home, people, user_id) {
svg({viewBox: '0 0 28 28', fill: 'currentColor', height: '28', width: '28'},
path({d: "M10.5 4.5c-2.272 0-2.75 1.768-2.75 3.25C7.75 9.542 8.983 11 10.5 11s2.75-1.458 2.75-3.25c0-1.482-.478-3.25-2.75-3.25zm0 8c-2.344 0-4.25-2.131-4.25-4.75C6.25 4.776 7.839 3 10.5 3s4.25 1.776 4.25 4.75c0 2.619-1.906 4.75-4.25 4.75zm9.5-6c-1.41 0-2.125.841-2.125 2.5 0 1.378.953 2.5 2.125 2.5 1.172 0 2.125-1.122 2.125-2.5 0-1.659-.715-2.5-2.125-2.5zm0 6.5c-1.999 0-3.625-1.794-3.625-4 0-2.467 1.389-4 3.625-4 2.236 0 3.625 1.533 3.625 4 0 2.206-1.626 4-3.625 4zm4.622 8a.887.887 0 00.878-.894c0-2.54-2.043-4.606-4.555-4.606h-1.86c-.643 0-1.265.148-1.844.413a6.226 6.226 0 011.76 4.336V21h5.621zm-7.122.562v-1.313a4.755 4.755 0 00-4.749-4.749H8.25A4.755 4.755 0 003.5 20.249v1.313c0 .518.421.938.937.938h12.125c.517 0 .938-.42.938-.938zM20.945 14C24.285 14 27 16.739 27 20.106a2.388 2.388 0 01-2.378 2.394h-5.81a2.44 2.44 0 01-2.25 1.5H4.437A2.44 2.44 0 012 21.562v-1.313A6.256 6.256 0 018.25 14h4.501a6.2 6.2 0 013.218.902A5.932 5.932 0 0119.084 14h1.861z"})
)
+ ),
+ a({id: 'chat', class: chat ? 'selected' : '', href: '/chat', 'aria-label': 'xssbook chat page'},
+ svg({viewBox: '0 0 512 512', fill: 'currentColor', height: '28', width: '28'},
+ g({transform: "translate(0.000000,512.000000) scale(0.100000,-0.100000)", fill: "#fffffff", stroke: "none"},
+ path({d: "M1731 4799 c-240 -27 -467 -93 -687 -199 -992 -481 -1340 -1619 -768 -2512 l43 -66 -150 -469 c-82 -257 -149 -481 -149 -496 0 -73 75 -147 150 -147 31 0 215 89 725 350 l230 118 90 -35 c109 -42 279 -87 395 -104 83 -12 86 -14 147 -70 172 -159 313 -256 514 -354 507 -245 1103 -270 1644 -68 l81 30 449 -229 c291 -148 464 -232 491 -235 80 -10 164 63 164 143 0 15 -67 238 -149 496 l-150 469 43 67 c330 511 364 1151 90 1689 -268 524 -818 913 -1421 1003 -43 7 -83 15 -89 18 -7 4 -54 45 -106 92 -143 128 -266 212 -443 299 -215 107 -352 152 -580 191 -139 25 -430 34 -564 19z m407 -300 c123 -13 261 -43 377 -80 100 -33 300 -127 385 -182 l54 -35 -39 -7 c-273 -43 -442 -94 -645 -191 -911 -439 -1295 -1442 -887 -2317 25 -53 41 -97 36 -97 -6 0 -67 22 -136 50 -78 31 -141 50 -166 50 -32 0 -104 -33 -363 -165 -178 -91 -325 -165 -327 -165 -3 0 42 145 100 323 57 177 104 340 105 362 1 47 -6 63 -84 178 -107 157 -180 326 -220 510 -29 135 -31 396 -5 530 119 596 612 1070 1253 1206 186 40 380 50 562 30z m1220 -600 c223 -24 404 -78 607 -179 436 -217 742 -607 832 -1059 24 -119 24 -384 0 -504 -39 -194 -130 -405 -244 -563 -31 -43 -60 -94 -64 -112 -9 -42 -4 -61 114 -429 52 -161 93 -293 91 -293 -2 0 -149 74 -327 165 -263 134 -331 165 -365 165 -26 0 -82 -17 -149 -44 -528 -216 -1130 -170 -1608 124 -163 100 -335 258 -452 417 -115 155 -211 374 -250 570 -24 122 -24 384 0 506 106 530 514 974 1062 1155 239 79 508 108 753 81z"}),
+ path({d: "M2488 2539 c-43 -22 -78 -81 -78 -129 0 -50 35 -107 80 -130 75 -38 157 -14 198 58 27 49 28 91 2 142 -37 73 -127 99 -202 59z"}),
+ path({d: "M3088 2539 c-43 -22 -78 -81 -78 -129 0 -50 35 -107 80 -130 75 -38 157 -14 198 58 27 49 28 91 2 142 -37 73 -127 99 -202 59z"}),
+ path({d: "M3688 2539 c-43 -22 -78 -81 -78 -129 0 -50 35 -107 80 -130 49 -25 90 -25 138 -1 43 22 82 84 82 131 0 47 -39 109 -82 131 -47 24 -93 24 -140 -2z"})
+ )
+ )
)
),
a({class: 'pfp', id: 'profile', href: '/profile', 'aria-label': 'your xssbook profile'},
@@ -246,7 +256,7 @@ export function parseUser(user) {
parse('Joined ' + parseDate(new Date(user.date)))
),
span({class: 'gtext'},
- parse('Gender :' + user.gender)
+ parse('Gender:' + user.gender)
),
span({class: 'gtext'},
parse('Birthday: ' + parseMonth(user.month) + ' ' + user.day + ', ' + user.year)
@@ -257,4 +267,4 @@ export function parseUser(user) {
)
)
)
-}
\ No newline at end of file
+}
diff --git a/public/js/home.js b/public/js/home.js
index 9f0398f..7d64eab 100644
--- a/public/js/home.js
+++ b/public/js/home.js
@@ -36,7 +36,7 @@ function render() {
let new_body =
body({},
- ...header(true, false, data.self.user_id),
+ ...header(true, false, false, data.self.user_id),
div({id: 'create'},
div({class: 'create'},
a({class: 'pfp', href: '/profile'},
@@ -44,7 +44,7 @@ function render() {
),
button({class: 'pfp'},
p({class: 'gtext', onclick: () => document.getElementById('popup').classList.remove('hidden')},
- parse(`What' on your mind, ${data.self.firstname}`)
+ parse(`What's on your mind, ${data.self.firstname}`)
)
)
)
@@ -113,7 +113,9 @@ async function load() {
const posts = (await loadpostspage(page)).json
if (posts.length === 0) {
- document.getElementById('load').remove()
+ let load = document.getElementById('load')
+ if (load)
+ load.remove()
return []
} else {
page++
@@ -134,17 +136,6 @@ async function load() {
async function init() {
let request = (await loadself());
-
- if (request.status === 429) {
- let new_body =
- body({},
- ...header(true, false)
- )
-
- document.body.replaceWith(new_body)
- throw new Error("Rate limited");
- }
-
data.self = request.json
if (request.json == undefined) {
@@ -161,4 +152,4 @@ async function init() {
}
-init()
\ No newline at end of file
+init()
diff --git a/public/js/main.js b/public/js/main.js
index b49474c..80ee48b 100644
--- a/public/js/main.js
+++ b/public/js/main.js
@@ -83,6 +83,10 @@ export function path(attrs, ...children) {
return createElementNS("path", attrs, ...children)
}
+export function g(attrs, ...children) {
+ return createElementNS("g", attrs, ...children)
+}
+
export function svg(attrs, ...children) {
return createElementNS("svg", attrs, ...children)
}
@@ -129,7 +133,7 @@ export function parseMonth(month) {
}
export function parseDate(date) {
- return parseMonth(date.getUTCMonth()) + ' ' + date.getUTCDate() + ', ' + date.getUTCFullYear() + ' ' + date.toLocaleTimeString();
+ return parseMonth(date.getUTCMonth()) + ' ' + (date.getUTCDate()-1) + ', ' + date.getUTCFullYear() + ' ' + date.toLocaleTimeString();
}
export function crawl(key, object) {
@@ -142,4 +146,4 @@ export function crawl(key, object) {
}
}
return data
-}
\ No newline at end of file
+}
diff --git a/public/js/people.js b/public/js/people.js
index 99890d9..431359c 100644
--- a/public/js/people.js
+++ b/public/js/people.js
@@ -6,7 +6,7 @@ function render() {
let new_body =
body({},
- ...header(false, true, data.self.user_id),
+ ...header(false, true, false, data.self.user_id),
div({id: 'users'},
...data.users.map(u => parseUser(u))
),
@@ -49,17 +49,6 @@ async function load() {
async function init() {
let request = (await loadself());
-
- if (request.status === 429) {
- let new_body =
- body({},
- ...header(true, false, data.self.user_id)
- )
-
- document.body.replaceWith(new_body)
- throw new Error("Rate limited");
- }
-
if (request.json == undefined) {
location.href = '/login'
return
@@ -73,4 +62,4 @@ async function init() {
render()
}
-init()
\ No newline at end of file
+init()
diff --git a/public/js/profile.js b/public/js/profile.js
index a9990fb..b000425 100644
--- a/public/js/profile.js
+++ b/public/js/profile.js
@@ -107,7 +107,7 @@ async function render() {
let new_body =
body({},
- ...header(false, false, data.self.user_id),
+ ...header(false, false, false, data.self.user_id),
div({id: 'top'},
div({id: 'banner'},
div({class: 'bg'},
@@ -223,7 +223,7 @@ async function render() {
parse('Email: ' + data.user.email)
),
span({class: 'gtext bold'},
- parse('Gender ' + data.user.gender)
+ parse('Gender: ' + data.user.gender)
),
span({class: 'gtext bold'},
parse('Birthday: ' + parseMonth(data.user.month) + ' ' + data.user.day + ', ' + data.user.year)
@@ -362,4 +362,4 @@ async function init() {
render()
}
-init()
\ No newline at end of file
+init()
diff --git a/src/api/chat.rs b/src/api/chat.rs
new file mode 100644
index 0000000..0f3b92e
--- /dev/null
+++ b/src/api/chat.rs
@@ -0,0 +1,324 @@
+use axum::{response::Response, Router, routing::{post, patch, delete}};
+use serde::Deserialize;
+use crate::{
+ public::docs::{EndpointDocumentation, EndpointMethod},
+ types::{
+ extract::{AuthorizedUser, Check, CheckResult, Database, Json},
+ http::ResponseCode,
+ chat::ChatRoom, user::User,
+ },
+};
+
+pub const CHAT_LIST: EndpointDocumentation = EndpointDocumentation {
+ uri: "/api/chat/list",
+ method: EndpointMethod::Post,
+ description: "Returns the rooms you are in",
+ body: None,
+ responses: &[
+ (201, "Returns rooms in a list"),
+ (400, "Body does not match parameters"),
+ (401, "Unauthorized"),
+ (500, "Failed to retrieve rooms"),
+ ],
+ cookie: Some("auth"),
+};
+
+async fn list (
+ AuthorizedUser(user): AuthorizedUser,
+ Database(db): Database
+) -> Response {
+ let Ok(rooms) = ChatRoom::from_user_id(&db, user.user_id) else {
+ return ResponseCode::InternalServerError.text("Failed to retrieve rooms")
+ };
+
+ let Ok(json) = serde_json::to_string(&rooms) else {
+ return ResponseCode::InternalServerError.text("Failed to retrieve rooms")
+ };
+
+ ResponseCode::Success.json(&json)
+}
+
+pub const CHAT_CREATE: EndpointDocumentation = EndpointDocumentation {
+ uri: "/api/chat/create",
+ method: EndpointMethod::Post,
+ description: "Creates a new room",
+ body: Some(
+ r#"
+ {
+ "name" : "Funny memes"
+ }
+ "#,
+ ),
+ responses: &[
+ (201, "Successfully created room"),
+ (400, "Body does not match parameters"),
+ (401, "Unauthorized"),
+ (500, "Failed to create room"),
+ ],
+ cookie: Some("auth"),
+};
+
+#[derive(Deserialize)]
+struct RoomCreateRequest {
+ name: String,
+}
+
+impl Check for RoomCreateRequest {
+ fn check(&self) -> CheckResult {
+ Self::assert_length(
+ &self.name,
+ 1,
+ 255,
+ "Room names must be between 1-255 characters long",
+ )?;
+ Ok(())
+ }
+}
+
+async fn create (
+ AuthorizedUser(user): AuthorizedUser,
+ Database(db): Database,
+ Json(body): Json,
+) -> Response {
+ let Ok(post) = ChatRoom::new(&db, vec![user.user_id], body.name) else {
+ return ResponseCode::InternalServerError.text("Failed to create room")
+ };
+
+ let Ok(json) = serde_json::to_string(&post) else {
+ return ResponseCode::InternalServerError.text("Failed to create room")
+ };
+
+ ResponseCode::Created.json(&json)
+}
+
+pub const CHAT_ADD: EndpointDocumentation = EndpointDocumentation {
+ uri: "/api/chat/add",
+ method: EndpointMethod::Patch,
+ description: "Adds a user to a room",
+ body: Some(
+ r#"
+ {
+ "room_id": 69,
+ "email" : "joebide@house.gov"
+ }
+ "#,
+ ),
+ responses: &[
+ (201, "Successfully added user"),
+ (400, "Body does not match parameters"),
+ (401, "Unauthorized"),
+ (500, "Failed to add user"),
+ ],
+ cookie: Some("auth"),
+};
+
+#[derive(Deserialize)]
+struct AddUserRequest {
+ room_id: u64,
+ email: String,
+}
+
+impl Check for AddUserRequest {
+ fn check(&self) -> CheckResult {
+ Ok(())
+ }
+}
+
+async fn add (
+ AuthorizedUser(user): AuthorizedUser,
+ Database(db): Database,
+ Json(body): Json,
+) -> Response {
+
+ let Ok(to_add) = User::from_email(&db, &body.email) else {
+ return ResponseCode::BadRequest.text("User does not exist")
+ };
+
+ let Ok(room) = ChatRoom::from_user_and_room_id(&db, user.user_id, body.room_id) else {
+ return ResponseCode::BadRequest.text("Room doesnt exist or you are not in it")
+ };
+
+ let Ok(success) = room.add_user(&db, to_add.user_id) else {
+ return ResponseCode::InternalServerError.text("Failed to add user")
+ };
+
+ if !success {
+ return ResponseCode::BadRequest.text("User is already in the room")
+ }
+
+ ResponseCode::Success.text("Successfully added user")
+}
+
+pub const CHAT_LEAVE: EndpointDocumentation = EndpointDocumentation {
+ uri: "/api/chat/leave",
+ method: EndpointMethod::Delete,
+ description: "Leaves a room",
+ body: Some(
+ r#"
+ {
+ "room_id": 69
+ }
+ "#,
+ ),
+ responses: &[
+ (201, "Successfully left room"),
+ (400, "Body does not match parameters"),
+ (401, "Unauthorized"),
+ (500, "Failed to leave a room"),
+ ],
+ cookie: Some("auth"),
+};
+
+#[derive(Deserialize)]
+struct LeaveRoomRequest {
+ room_id: u64,
+}
+
+impl Check for LeaveRoomRequest {
+ fn check(&self) -> CheckResult {
+ Ok(())
+ }
+}
+
+async fn leave (
+ AuthorizedUser(user): AuthorizedUser,
+ Database(db): Database,
+ Json(body): Json,
+) -> Response {
+
+ let Ok(room) = ChatRoom::from_user_and_room_id(&db, user.user_id, body.room_id) else {
+ return ResponseCode::BadRequest.text("Room doesnt exist or you are not in it")
+ };
+
+ let Ok(success) = room.remove_user(&db, user.user_id) else {
+ return ResponseCode::InternalServerError.text("Failed to leave room")
+ };
+
+ if !success {
+ return ResponseCode::BadRequest.text("You are currently not in this room (how did this happen?)")
+ }
+
+ ResponseCode::Success.text("Successfully left room")
+}
+
+pub const CHAT_SEND: EndpointDocumentation = EndpointDocumentation {
+ uri: "/api/chat/send",
+ method: EndpointMethod::Post,
+ description: "Send a message to a room",
+ body: Some(
+ r#"
+ {
+ "room_id": 420,
+ "content" : "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ }
+ "#,
+ ),
+ responses: &[
+ (201, "Successfully sent message"),
+ (400, "Body does not match parameters"),
+ (401, "Unauthorized"),
+ (500, "Failed to send message"),
+ ],
+ cookie: Some("auth"),
+};
+
+#[derive(Deserialize)]
+struct SendMessageRequest {
+ room_id: u64,
+ content: String
+}
+
+impl Check for SendMessageRequest {
+ fn check(&self) -> CheckResult {
+ Self::assert_length(
+ &self.content,
+ 1,
+ 500,
+ "Messages must be between 1-500 length"
+ )?;
+ Ok(())
+ }
+}
+
+async fn send (
+ AuthorizedUser(user): AuthorizedUser,
+ Database(db): Database,
+ Json(body): Json,
+) -> Response {
+
+ let Ok(room) = ChatRoom::from_user_and_room_id(&db, user.user_id, body.room_id) else {
+ return ResponseCode::BadRequest.text("Room doesnt exist or you are not in it")
+ };
+
+ let Ok(_msg) = room.send_message(&db, user.user_id, body.content) else {
+ return ResponseCode::InternalServerError.text("Failed to send message")
+ };
+
+ ResponseCode::Created.text("Successfully sent message")
+}
+
+pub const CHAT_LOAD: EndpointDocumentation = EndpointDocumentation {
+ uri: "/api/chat/load",
+ method: EndpointMethod::Post,
+ description: "Get a page of historic room messages starting before given message id",
+ body: Some(
+ r#"
+ {
+ "room_id": 69,
+ "newest_msg": 400,
+ "page": 3
+ }
+ "#,
+ ),
+ responses: &[
+ (201, "Successfully sent message"),
+ (400, "Body does not match parameters"),
+ (401, "Unauthorized"),
+ (500, "Failed to send message"),
+ ],
+ cookie: Some("auth"),
+};
+
+#[derive(Deserialize)]
+struct LoadMessagesRequest {
+ room_id: u64,
+ newest_msg: u64,
+ page: u64
+}
+
+impl Check for LoadMessagesRequest {
+ fn check(&self) -> CheckResult {
+ Ok(())
+ }
+}
+
+async fn load (
+ AuthorizedUser(user): AuthorizedUser,
+ Database(db): Database,
+ Json(body): Json,
+) -> Response {
+
+ let Ok(room) = ChatRoom::from_user_and_room_id(&db, user.user_id, body.room_id) else {
+ return ResponseCode::BadRequest.text("Room doesnt exist or you are not in it")
+ };
+
+ let Ok(msgs) = room.load_old_chat_messagegs(&db, body.newest_msg, body.page) else {
+ return ResponseCode::InternalServerError.text("Failed to load messages")
+ };
+
+ let Ok(json) = serde_json::to_string(&msgs) else {
+ return ResponseCode::InternalServerError.text("Failed to load messages")
+ };
+
+ ResponseCode::Created.json(&json)
+}
+
+pub fn router() -> Router {
+ Router::new()
+ .route("/create", post(create))
+ .route("/list", post(list))
+ .route("/add", patch(add))
+ .route("/leave", delete(leave))
+ .route("/send", post(send))
+ .route("/load", post(load))
+}
diff --git a/src/api/mod.rs b/src/api/mod.rs
index 8b631c8..0c01ea0 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -1,32 +1,20 @@
use crate::types::extract::{RouterURI, self};
-use axum::{
- error_handling::HandleErrorLayer,
- BoxError, Extension, Router, middleware,
-};
-use tower::ServiceBuilder;
-use tower_governor::{
- errors::display_error, governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor,
- GovernorLayer,
-};
+pub mod chat;
pub mod admin;
pub mod auth;
pub mod posts;
pub mod users;
pub use auth::RegistrationRequet;
+use axum::{Extension, Router, middleware};
pub fn router() -> Router {
- let governor_conf = Box::new(
- GovernorConfigBuilder::default()
- .burst_size(15)
- .per_second(1)
- .key_extractor(SmartIpKeyExtractor)
- .finish()
- .expect("Failed to create rate limiter"),
- );
-
Router::new()
+ .nest(
+ "/chat",
+ chat::router().layer(Extension(RouterURI("/api/chat"))),
+ )
.nest(
"/admin",
admin::router().layer(Extension(RouterURI("/api/admin"))),
@@ -43,14 +31,5 @@ pub fn router() -> Router {
"/posts",
posts::router().layer(Extension(RouterURI("/api/posts"))),
)
- .layer(
- ServiceBuilder::new()
- .layer(HandleErrorLayer::new(|e: BoxError| async move {
- display_error(e)
- }))
- .layer(GovernorLayer {
- config: Box::leak(governor_conf),
- }),
- )
.layer(middleware::from_fn(extract::connect))
}
diff --git a/src/database/chat.rs b/src/database/chat.rs
new file mode 100644
index 0000000..7364211
--- /dev/null
+++ b/src/database/chat.rs
@@ -0,0 +1,211 @@
+use std::time::{SystemTime, UNIX_EPOCH, Duration};
+
+use tracing::instrument;
+
+use crate::types::chat::{ChatRoom, ChatMessage};
+
+use super::Database;
+
+impl Database {
+
+ #[instrument(skip(self))]
+ pub fn init_chat(&self) -> Result<(), rusqlite::Error> {
+ let sql = "
+ CREATE TABLE IF NOT EXISTS chat_rooms (
+ room_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name VARCHAR(255) NOT NULL
+ );
+ ";
+ self.0.execute(sql, ())?;
+
+ let sql2 = "
+ CREATE TABLE IF NOT EXISTS chat_users (
+ room_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL,
+ FOREIGN KEY(room_id) REFERENCES chat_rooms(room_id),
+ FOREIGN KEY(user_id) REFERENCES users(user_id),
+ PRIMARY KEY (room_id, user_id)
+ );
+ ";
+ self.0.execute(sql2, ())?;
+
+ let sql3 = "
+ CREATE TABLE IF NOT EXISTS chat_messages (
+ message_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ room_id INTEGER NOT NULL,
+ date INTEGER NOT NULL,
+ content VARCHAR(500) NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(user_id),
+ FOREIGN KEY(room_id) REFERENCES chat_rooms(room_id)
+ );
+ ";
+ self.0.execute(sql3, ())?;
+
+ let sql4 = "CREATE INDEX IF NOT EXISTS chat_message_ids ON chat_messages(room_id);";
+ self.0.execute(sql4, ())?;
+
+ Ok(())
+ }
+
+ #[instrument(skip(self))]
+ pub fn get_rooms(&self, user_id: u64) -> Result, rusqlite::Error> {
+ tracing::trace!("Retrieving rooms");
+ let mut stmt = self.0.prepare(
+ "
+ SELECT * FROM chat_rooms
+ WHERE room_id IN (
+ SELECT room_id
+ FROM chat_users
+ WHERE user_id = ?
+ );
+ ",
+ )?;
+ let row = stmt.query_map([user_id], |row| {
+ let room_id = row.get(0)?;
+ let name = row.get(1)?;
+
+ let mut stmt2 = self.0.prepare(
+ "
+ SELECT user_id FROM chat_users
+ WHERE room_id = ?;
+ "
+ )?;
+
+ let mut users = Vec::new();
+ let _ = stmt2.query_map([room_id], |row2| {
+ let user_id = row2.get(0)?;
+ users.push(user_id);
+ Ok(())
+ })?;
+
+ let room = ChatRoom {
+ room_id,
+ users,
+ name
+ };
+ Ok(room)
+ })?;
+ Ok(row.into_iter().flatten().collect())
+ }
+
+ #[instrument(skip(self))]
+ pub fn create_room(&self, users: Vec, name: String) -> Result {
+ tracing::trace!("Creating new room");
+ let mut stmt = self.0.prepare(
+ "INSERT INTO chat_rooms (name) VALUES (?) RETURNING *;"
+ )?;
+ let mut room = stmt.query_row([name], |row| {
+ let room_id = row.get(0)?;
+ let name = row.get(1)?;
+ Ok(ChatRoom {
+ room_id,
+ users: Vec::new(),
+ name
+ })
+ })?;
+
+ let mut stmt2 = self.0.prepare(
+ "INSERT INTO chat_users (room_id, user_id) VALUES (?, ?);"
+ )?;
+
+ for user_id in users {
+ stmt2.execute([room.room_id, user_id])?;
+ room.users.push(user_id);
+ }
+
+ Ok(room)
+ }
+
+ #[instrument(skip(self))]
+ pub fn add_user_to_room(&self, room_id: u64, user_id: u64) -> Result {
+ tracing::trace!("Adding user to room");
+ let mut stmt = self.0.prepare(
+ "INSERT OR REPLACE INTO chat_users (room_id, user_id) VALUES(?,?);"
+ )?;
+
+ let changes = stmt.execute([room_id, user_id])?;
+
+ Ok(changes == 1)
+ }
+
+ #[instrument(skip(self))]
+ pub fn remove_user_from_room(&self, room_id: u64, user_id: u64) -> Result {
+ tracing::trace!("Removing user from room");
+ let mut stmt = self.0.prepare(
+ "DELETE FROM chat_users WHERE room_id = ? AND user_id = ?;"
+ )?;
+
+ let changes = stmt.execute([room_id, user_id])?;
+
+ Ok(changes == 1)
+ }
+
+ #[instrument(skip(self))]
+ pub fn create_message(&self, room_id: u64, user_id: u64, content: String) -> Result {
+ tracing::trace!("Creating new chat message");
+ let date = u64::try_from(
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap_or(Duration::ZERO)
+ .as_millis(),
+ )
+ .unwrap_or(0);
+
+ let mut stmt = self.0.prepare(
+ "INSERT INTO chat_messages (user_id, room_id, date, content) VALUES (?,?,?,?) RETURNING *;"
+ )?;
+
+ let msg = stmt.query_row((user_id, room_id, date, content), |row| {
+ let message_id = row.get(0)?;
+ let room_id = row.get(1)?;
+ let user_id = row.get(2)?;
+ let date = row.get(3)?;
+ let content = row.get(4)?;
+
+ Ok(ChatMessage {
+ message_id,
+ room_id,
+ user_id,
+ date,
+ content
+ })
+ })?;
+
+ Ok(msg)
+ }
+
+ #[instrument(skip(self))]
+ pub fn load_old_chat_messages(&self, room_id: u64, newest_message: u64, page: u64) -> Result, rusqlite::Error> {
+ tracing::trace!("Loading old chat messages");
+ let mut stmt = self.0.prepare(
+ "
+ SELECT * FROM chat_messages
+ WHERE room_id = ?
+ AND message_id < newest_message
+ ORDER BY message_id ASC
+ LIMIT ?
+ OFFSET ?
+ "
+ )?;
+
+ let messages = stmt.query_map((room_id, 20, 20 * page), |row| {
+ let message_id = row.get(0)?;
+ let room_id = row.get(1)?;
+ let user_id = row.get(2)?;
+ let date = row.get(3)?;
+ let content = row.get(4)?;
+
+ Ok(ChatMessage {
+ message_id,
+ room_id,
+ user_id,
+ date,
+ content
+ })
+ })?;
+
+ Ok(messages.into_iter().flatten().collect())
+ }
+
+}
diff --git a/src/database/mod.rs b/src/database/mod.rs
index 67e05c6..7d0928f 100644
--- a/src/database/mod.rs
+++ b/src/database/mod.rs
@@ -1,6 +1,7 @@
use rusqlite::Connection;
use tracing::instrument;
+pub mod chat;
pub mod comments;
pub mod friends;
pub mod likes;
@@ -32,5 +33,6 @@ pub fn init() -> Result<(), rusqlite::Error> {
db.init_likes()?;
db.init_comments()?;
db.init_friends()?;
+ db.init_chat()?;
Ok(())
}
diff --git a/src/public/docs.rs b/src/public/docs.rs
index 397e696..7417183 100644
--- a/src/public/docs.rs
+++ b/src/public/docs.rs
@@ -3,7 +3,7 @@ use lazy_static::lazy_static;
use tokio::sync::Mutex;
use crate::{
- api::{admin, auth, posts, users},
+ api::{admin, auth, posts, users, chat},
types::http::ResponseCode,
};
@@ -13,6 +13,7 @@ pub enum EndpointMethod {
Post,
Put,
Patch,
+ Delete
}
impl ToString for EndpointMethod {
@@ -21,6 +22,7 @@ impl ToString for EndpointMethod {
Self::Post => "POST".to_owned(),
Self::Put => "PUT".to_owned(),
Self::Patch => "PATCH".to_owned(),
+ Self::Delete => "DELETE".to_owned(),
}
}
}
@@ -139,6 +141,12 @@ pub async fn init() {
users::USERS_FOLLOW,
users::USERS_FOLLOW_STATUS,
users::USERS_FRIENDS,
+ chat::CHAT_LIST,
+ chat::CHAT_CREATE,
+ chat::CHAT_ADD,
+ chat::CHAT_LEAVE,
+ chat::CHAT_SEND,
+ chat::CHAT_LOAD,
admin::ADMIN_AUTH,
admin::ADMIN_QUERY,
admin::ADMIN_POSTS,
diff --git a/src/public/mod.rs b/src/public/mod.rs
index bb75ef0..bd40fda 100644
--- a/src/public/mod.rs
+++ b/src/public/mod.rs
@@ -1,17 +1,12 @@
use axum::{
body::Body,
- error_handling::HandleErrorLayer,
headers::HeaderName,
http::{HeaderValue, Request, StatusCode},
- response::{IntoResponse, Response},
+ response::{Response, IntoResponse},
routing::get,
- BoxError, Router,
-};
-use tower::{ServiceBuilder, ServiceExt};
-use tower_governor::{
- errors::display_error, governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor,
- GovernorLayer,
+ Router,
};
+use tower::ServiceExt;
use tower_http::services::ServeFile;
use crate::types::http::ResponseCode;
@@ -23,15 +18,6 @@ pub mod file;
pub mod pages;
pub fn router() -> Router {
- let governor_conf = Box::new(
- GovernorConfigBuilder::default()
- .burst_size(30)
- .per_second(1)
- .key_extractor(SmartIpKeyExtractor)
- .finish()
- .expect("Failed to create rate limiter"),
- );
-
Router::new()
.nest("/", pages::router())
.route("/favicon.ico", get(file::favicon))
@@ -42,15 +28,6 @@ pub fn router() -> Router {
.route("/image/*path", get(file::image))
.route("/image/avatar", get(file::avatar))
.route("/image/banner", get(file::banner))
- .layer(
- ServiceBuilder::new()
- .layer(HandleErrorLayer::new(|e: BoxError| async move {
- display_error(e)
- }))
- .layer(GovernorLayer {
- config: Box::leak(governor_conf),
- }),
- )
}
pub async fn serve(path: &str) -> Response {
diff --git a/src/public/pages.rs b/src/public/pages.rs
index a7789b2..0eef51b 100644
--- a/src/public/pages.rs
+++ b/src/public/pages.rs
@@ -66,6 +66,10 @@ async fn forgot(UserAgent(agent): UserAgent, _: Log) -> Response {
Redirect::to("https://www.youtube.com/watch?v=dQw4w9WgXcQ").into_response()
}
+async fn chat() -> Response {
+ super::serve("/chat.html").await
+}
+
pub fn router() -> Router {
Router::new()
.route("/", get(root))
@@ -79,4 +83,5 @@ pub fn router() -> Router {
.route("/admin", get(admin))
.route("/docs", get(api))
.route("/forgot", get(forgot))
+ .route("/chat", get(chat))
}
diff --git a/src/types/chat.rs b/src/types/chat.rs
new file mode 100644
index 0000000..dadcf10
--- /dev/null
+++ b/src/types/chat.rs
@@ -0,0 +1,98 @@
+use serde::Serialize;
+use tracing::instrument;
+use crate::{types::http::{ResponseCode, Result}, database::Database};
+
+#[derive(Serialize, Clone, Debug)]
+pub struct ChatRoom {
+ pub room_id: u64,
+ pub users: Vec,
+ pub name: String
+}
+
+#[derive(Serialize, Clone, Debug)]
+pub struct ChatMessage {
+ pub message_id: u64,
+ pub user_id: u64,
+ pub room_id: u64,
+ pub date: u64,
+ pub content: String
+}
+
+impl ChatRoom {
+
+ #[instrument(skip(db))]
+ pub fn new(db: &Database, users: Vec, name: String) -> Result {
+ let Ok(room) = db.create_room(users, name) else {
+ tracing::error!("Failed to create room");
+ return Err(ResponseCode::InternalServerError.text("Failed to create room"))
+ };
+
+ Ok(room)
+ }
+
+ #[instrument(skip(db))]
+ pub fn from_user_id(db: &Database, user_id: u64) -> Result> {
+ let Ok(rooms) = db.get_rooms(user_id) else {
+ tracing::error!("Failed to get rooms");
+ return Err(ResponseCode::InternalServerError.text("Failed to get rooms"))
+ };
+
+ Ok(rooms)
+ }
+
+ #[instrument(skip(db))]
+ pub fn from_user_and_room_id(db: &Database, user_id: u64, room_id: u64) -> Result {
+ let Ok(rooms) = db.get_rooms(user_id) else {
+ tracing::error!("Failed to get room");
+ return Err(ResponseCode::InternalServerError.text("Failed to get room"))
+ };
+
+ for room in rooms {
+ if room.room_id == room_id {
+ return Ok(room);
+ }
+ }
+
+ return Err(ResponseCode::BadRequest.text("Room doesnt exist or you are not in it"))
+ }
+
+ #[instrument(skip(db))]
+ pub fn add_user(&self, db: &Database, user_id: u64) -> Result {
+ let Ok(success) = db.add_user_to_room(self.room_id, user_id) else {
+ tracing::error!("Failed to add user to room");
+ return Err(ResponseCode::InternalServerError.text("Failed to add user to room"))
+ };
+
+ Ok(success)
+ }
+
+ #[instrument(skip(db))]
+ pub fn remove_user(&self, db: &Database, user_id: u64) -> Result {
+ let Ok(success) = db.remove_user_from_room(self.room_id, user_id) else {
+ tracing::error!("Failed to remove user from room");
+ return Err(ResponseCode::InternalServerError.text("Failed to remove user from room"))
+ };
+
+ Ok(success)
+ }
+
+ #[instrument(skip(db))]
+ pub fn send_message(&self, db: &Database, user_id: u64, content: String) -> Result {
+ let Ok(msg) = db.create_message(self.room_id, user_id, content) else {
+ tracing::error!("Failed to create messgae");
+ return Err(ResponseCode::InternalServerError.text("Failed to create message"))
+ };
+
+ Ok(msg)
+ }
+
+ #[instrument(skip(db))]
+ pub fn load_old_chat_messagegs(&self, db: &Database, newest_message: u64, page: u64) -> Result> {
+ let Ok(msgs) = db.load_old_chat_messages(self.room_id, newest_message, page) else {
+ tracing::error!("Failed to load messgaes");
+ return Err(ResponseCode::InternalServerError.text("Failed to load messages"))
+ };
+
+ Ok(msgs)
+ }
+}
diff --git a/src/types/mod.rs b/src/types/mod.rs
index 1ee2d08..a325ff9 100644
--- a/src/types/mod.rs
+++ b/src/types/mod.rs
@@ -1,3 +1,4 @@
+pub mod chat;
pub mod comment;
pub mod extract;
pub mod http;