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;