From a2c89301d5c63f573ac6680f9c11acda69043a78 Mon Sep 17 00:00:00 2001 From: Tyler Murphy <=> Date: Wed, 26 Jul 2023 01:04:39 -0400 Subject: [PATCH 1/5] start dms --- Cargo.lock | 197 +----------------------- Cargo.toml | 3 +- public/chat.html | 27 ++++ public/css/api.css | 6 +- public/css/main.css | 2 +- public/js/chat.js | 37 +++++ public/js/components.js | 18 ++- public/js/home.js | 21 +-- public/js/main.js | 8 +- public/js/people.js | 15 +- public/js/profile.js | 6 +- src/api/chat.rs | 324 ++++++++++++++++++++++++++++++++++++++++ src/api/mod.rs | 33 +--- src/database/chat.rs | 211 ++++++++++++++++++++++++++ src/database/mod.rs | 2 + src/public/docs.rs | 10 +- src/public/mod.rs | 29 +--- src/public/pages.rs | 5 + src/types/chat.rs | 98 ++++++++++++ src/types/mod.rs | 1 + 20 files changed, 763 insertions(+), 290 deletions(-) create mode 100644 public/chat.html create mode 100644 public/js/chat.js create mode 100644 src/api/chat.rs create mode 100644 src/database/chat.rs create mode 100644 src/types/chat.rs 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; From 2adbe0d9990bb74942377a07c05981b8a0e73c75 Mon Sep 17 00:00:00 2001 From: Tyler Murphy Date: Mon, 21 Aug 2023 15:16:48 -0400 Subject: [PATCH 2/5] websocket --- Cargo.lock | 203 +++++++++++++++++++++++++++++++++++++-------- Cargo.toml | 2 +- public/css/api.css | 4 + src/api/chat.rs | 193 ++++++++++++++++++++++++++++++++++++++++-- src/public/docs.rs | 4 + src/types/chat.rs | 35 +++++++- 6 files changed, 397 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 266e1fd..44ffe6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,13 +21,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.63" +version = "0.1.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff18d764974428cf3a9328e23fc5c986f5fbed46e6cd4cdf42544df5d297ec1" +checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.27", ] [[package]] @@ -38,12 +38,13 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.6.4" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5694b64066a2459918d8074c2ce0d5a88f409431994c2356617c8ae0c4721fc" +checksum = "a6a1de45611fdb535bfde7b7de4fd54f4fd2b17b1737c0a59b69bf9b92074b8c" dependencies = [ "async-trait", "axum-core", + "base64 0.21.2", "bitflags", "bytes", "futures-util", @@ -62,19 +63,20 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", + "sha1", "sync_wrapper", "tokio", + "tokio-tungstenite", "tower", - "tower-http", "tower-layer", "tower-service", ] [[package]] name = "axum-core" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cae3e661676ffbacb30f1a824089a8c9150e71017f7e1e38f2aa32009188d34" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", "bytes", @@ -93,6 +95,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + [[package]] name = "bit_field" version = "0.10.1" @@ -244,6 +252,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + [[package]] name = "digest" version = "0.10.6" @@ -336,41 +350,42 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-macro" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.27", ] [[package]] name = "futures-sink" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", "futures-macro", + "futures-sink", "futures-task", "pin-project-lite", "pin-utils", @@ -443,7 +458,7 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ - "base64", + "base64 0.13.1", "bitflags", "bytes", "headers-core", @@ -473,9 +488,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", @@ -513,9 +528,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.23" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", @@ -534,6 +549,16 @@ dependencies = [ "want", ] +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "image" version = "0.24.5" @@ -800,7 +825,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -841,18 +866,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.50" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.23" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] @@ -973,7 +998,7 @@ checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1082,12 +1107,43 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" +[[package]] +name = "thiserror" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + [[package]] name = "thread_local" version = "1.1.4" @@ -1144,6 +1200,21 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.25.0" @@ -1172,7 +1243,19 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec509ac96e9a0c43427c74f003127d953a265737636129424288d27cb5c4b12c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", ] [[package]] @@ -1241,7 +1324,6 @@ dependencies = [ "pin-project-lite", "tokio", "tokio-util", - "tower", "tower-layer", "tower-service", ] @@ -1279,7 +1361,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1323,6 +1405,25 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "tungstenite" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] + [[package]] name = "typenum" version = "1.16.0" @@ -1338,12 +1439,44 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "valuable" version = "0.1.0" @@ -1399,7 +1532,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.107", "wasm-bindgen-shared", ] @@ -1421,7 +1554,7 @@ checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index 9b460dc..b72713c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] tokio = { version = "1.25.0", features = ["full"] } -axum = { version = "0.6.4", features = ["headers", "query"] } +axum = { version = "0.6.12", features = ["headers", "query", "ws"] } tower-http = { version = "0.3.5", features = ["fs"] } tower-cookies = "0.8.0" tower = "0.4.13" diff --git a/public/css/api.css b/public/css/api.css index 6e5e612..a9f794a 100644 --- a/public/css/api.css +++ b/public/css/api.css @@ -123,6 +123,10 @@ h2 { background-color: #bfa354; } +.get { + background-color: #00cc00; +} + .delete { background-color: #cc0000; } diff --git a/src/api/chat.rs b/src/api/chat.rs index 0f3b92e..1e56c3e 100644 --- a/src/api/chat.rs +++ b/src/api/chat.rs @@ -1,13 +1,63 @@ -use axum::{response::Response, Router, routing::{post, patch, delete}}; +use std::collections::HashMap; + +use axum::{response::Response, Router, routing::{post, patch, delete, get}, extract::{ws::Message, WebSocketUpgrade}}; use serde::Deserialize; +use tokio::sync::{Mutex, mpsc::{Sender, self}}; use crate::{ public::docs::{EndpointDocumentation, EndpointMethod}, types::{ extract::{AuthorizedUser, Check, CheckResult, Database, Json}, http::ResponseCode, - chat::ChatRoom, user::User, + chat::{ChatRoom, ChatEvent}, user::User, }, }; +use std::collections::hash_map::Values; +use lazy_static::lazy_static; + +lazy_static!( + static ref CONNECTIONS: Mutex> = Mutex::new(HashMap::new()); +); + +struct ConnectionPool { + inner: HashMap>, + index: usize +} + +impl ConnectionPool { + fn new() -> Self { + Self { + inner: HashMap::new(), + index: 0 + } + } + + fn add(&mut self, send: Sender) -> usize { + let idx = self.index; + self.index += 1; + self.inner.insert(idx, send); + idx + } + + fn del(&mut self, idx: &usize) { + self.inner.remove(idx); + } + + fn values(&self) -> Values<'_, usize, Sender> { + self.inner.values() + } +} + +async fn send_event(event: ChatEvent, room: &ChatRoom) { + for user in &room.users { + let lock = CONNECTIONS.lock().await; + let Some(connection) = lock.get(&user) else { + continue + }; + for channel in connection.values() { + channel.send(event.clone()).await.ok(); + } + } +} pub const CHAT_LIST: EndpointDocumentation = EndpointDocumentation { uri: "/api/chat/list", @@ -80,11 +130,18 @@ async fn create ( Database(db): Database, Json(body): Json, ) -> Response { - let Ok(post) = ChatRoom::new(&db, vec![user.user_id], body.name) else { + let Ok(room) = 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 { + for user in &room.users { + send_event(ChatEvent::Add { + user_id: *user, + room_id: room.room_id + }, &room).await; + } + + let Ok(json) = serde_json::to_string(&room) else { return ResponseCode::InternalServerError.text("Failed to create room") }; @@ -145,6 +202,11 @@ async fn add ( if !success { return ResponseCode::BadRequest.text("User is already in the room") } + + send_event(ChatEvent::Add { + user_id: to_add.user_id, + room_id: room.room_id + }, &room).await; ResponseCode::Success.text("Successfully added user") } @@ -197,6 +259,11 @@ async fn leave ( if !success { return ResponseCode::BadRequest.text("You are currently not in this room (how did this happen?)") } + + send_event(ChatEvent::Leave { + user_id: user.user_id, + room_id: room.room_id + }, &room).await; ResponseCode::Success.text("Successfully left room") } @@ -250,9 +317,17 @@ async fn send ( 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 { + let Ok(msg) = room.send_message(&db, user.user_id, body.content) else { return ResponseCode::InternalServerError.text("Failed to send message") }; + + send_event(ChatEvent::Message { + user_id: msg.user_id, + room_id: msg.room_id, + message_id: msg.message_id, + content: msg.content, + date: msg.date + }, &room).await; ResponseCode::Created.text("Successfully sent message") } @@ -302,7 +377,7 @@ async fn load ( 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 { + let Ok(msgs) = room.load_old_chat_messages(&db, body.newest_msg, body.page) else { return ResponseCode::InternalServerError.text("Failed to load messages") }; @@ -313,6 +388,110 @@ async fn load ( ResponseCode::Created.json(&json) } +pub const CHAT_TYPING: EndpointDocumentation = EndpointDocumentation { + uri: "/api/chat/typing", + method: EndpointMethod::Post, + description: "Set if your typing in a given room", + body: Some( + r#" + { + "room_id": 69, + } + "#, + ), + responses: &[ + (201, "Successfully sent typing indicator"), + (400, "Body does not match parameters"), + (401, "Unauthorized"), + (500, "Failed to send typing indicator"), + ], + cookie: Some("auth"), +}; + +#[derive(Deserialize)] +struct TypingRequest { + room_id: u64, +} + +impl Check for TypingRequest { + fn check(&self) -> CheckResult { + Ok(()) + } +} + +async fn typing ( + 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") + }; + + send_event(ChatEvent::Typing { + user_id: user.user_id, + room_id: room.room_id, + }, &room).await; + + ResponseCode::Success.text("Successfully sent typing indicator") +} + +pub const CHAT_CONNECT: EndpointDocumentation = EndpointDocumentation { + uri: "/api/chat/connect", + method: EndpointMethod::Get, + description: "Start a websocket connection for chat events", + body: None, + responses: &[], + cookie: Some("auth"), +}; + +async fn connect ( + AuthorizedUser(user): AuthorizedUser, + ws: WebSocketUpgrade +) -> Response { + ws.on_upgrade(|mut ws| async move { + let user = user; + let (send, mut recv) = mpsc::channel::(20); + let id: usize; + { + let mut lock = CONNECTIONS.lock().await; + match lock.get_mut(&user.user_id) { + Some(pool) => { + id = pool.add(send); + }, + None => { + let mut pool = ConnectionPool::new(); + id = pool.add(send); + lock.insert(user.user_id, pool); + } + }; + } + loop { + tokio::select! { + m = ws.recv() => { + let Some(Ok(_)) = m else { + break; + }; + } + s = recv.recv() => { + let Some(msg) = s else { + break; + }; + if let Ok(string) = serde_json::to_string(&msg) { + ws.send(Message::Text(string)).await.ok(); + } + } + } + } + + let mut lock = CONNECTIONS.lock().await; + if let Some(conn) = lock.get_mut(&user.user_id) { + conn.del(&id); + }; + }) +} + pub fn router() -> Router { Router::new() .route("/create", post(create)) @@ -321,4 +500,6 @@ pub fn router() -> Router { .route("/leave", delete(leave)) .route("/send", post(send)) .route("/load", post(load)) + .route("/typing", post(typing)) + .route("/connect", get(connect)) } diff --git a/src/public/docs.rs b/src/public/docs.rs index 7417183..976638b 100644 --- a/src/public/docs.rs +++ b/src/public/docs.rs @@ -10,6 +10,7 @@ use crate::{ use super::console::beautify; pub enum EndpointMethod { + Get, Post, Put, Patch, @@ -19,6 +20,7 @@ pub enum EndpointMethod { impl ToString for EndpointMethod { fn to_string(&self) -> String { match self { + Self::Get => "GET".to_owned(), Self::Post => "POST".to_owned(), Self::Put => "PUT".to_owned(), Self::Patch => "PATCH".to_owned(), @@ -147,6 +149,8 @@ pub async fn init() { chat::CHAT_LEAVE, chat::CHAT_SEND, chat::CHAT_LOAD, + chat::CHAT_TYPING, + chat::CHAT_CONNECT, admin::ADMIN_AUTH, admin::ADMIN_QUERY, admin::ADMIN_POSTS, diff --git a/src/types/chat.rs b/src/types/chat.rs index dadcf10..ab3390c 100644 --- a/src/types/chat.rs +++ b/src/types/chat.rs @@ -1,4 +1,4 @@ -use serde::Serialize; +use serde::{Serialize, Deserialize}; use tracing::instrument; use crate::{types::http::{ResponseCode, Result}, database::Database}; @@ -18,6 +18,37 @@ pub struct ChatMessage { pub content: String } +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(tag = "type")] +pub enum ChatEvent { + #[serde(rename = "message")] + Message { + user_id: u64, + message_id: u64, + room_id: u64, + content: String, + date: u64 + }, + + #[serde(rename = "add")] + Add { + user_id: u64, + room_id: u64 + }, + + #[serde(rename = "leave")] + Leave { + user_id: u64, + room_id: u64 + }, + + #[serde(rename = "typing")] + Typing { + user_id: u64, + room_id: u64 + } +} + impl ChatRoom { #[instrument(skip(db))] @@ -87,7 +118,7 @@ impl ChatRoom { } #[instrument(skip(db))] - pub fn load_old_chat_messagegs(&self, db: &Database, newest_message: u64, page: u64) -> Result> { + pub fn load_old_chat_messages(&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")) From c4c26f42b6b06dbc75578314a483db66bf7ddb4c Mon Sep 17 00:00:00 2001 From: Tyler Murphy <=> Date: Mon, 21 Aug 2023 23:19:53 -0400 Subject: [PATCH 3/5] dms --- public/chat.html | 1 + public/css/chat.css | 172 +++++++++++++++++++++++++++++++++ public/css/home.css | 2 +- public/css/main.css | 6 +- public/js/api.js | 26 ++++- public/js/chat.js | 207 ++++++++++++++++++++++++++++++++++++++-- public/js/components.js | 163 ++++++++++++++++++++++++++++++- public/js/main.js | 6 +- src/api/chat.rs | 17 +++- src/database/chat.rs | 30 +++--- src/main.rs | 2 +- src/types/chat.rs | 4 +- 12 files changed, 598 insertions(+), 38 deletions(-) create mode 100644 public/css/chat.css diff --git a/public/chat.html b/public/chat.html index e293210..ecd2f5c 100644 --- a/public/chat.html +++ b/public/chat.html @@ -17,6 +17,7 @@ + diff --git a/public/css/chat.css b/public/css/chat.css new file mode 100644 index 0000000..1092a39 --- /dev/null +++ b/public/css/chat.css @@ -0,0 +1,172 @@ +.spacer { + margin-bottom: 3.5em !important; +} + +#cent { + display: flex; + width: 100%; + height: calc(100vh - 3.5em); + flex-direction: row; +} + +#sidebar { + height: 100%; + width: 20%; + min-width: 25em; + display: flex; + flex-direction: column; +} + +#sidebar>span { + display: block; + width: 100%; + text-align: center; + font-size: 1.5em; + margin: .5em 0; +} + +#center { + height: 100%; + width: calc(100vw - 20%); + max-width: calc(100vw - 25em); +} + +.room { + width: calc (100% - 1rem); + height: 3rem; + display: flex; + position: relative; + flex-direction: row; + padding: .5rem 1rem; +} + +.room:hover, .current { + background-color: var(--hover); +} + +.room-icon { + border-radius: 3rem; + height: 3rem; + width: 3rem; + display: flex; + justify-content: center; + align-items: center; + font-weight: 1000; + font-size: 1.5rem; +} + +.room-name { + display: flex; + height: 3rem; + margin-left: 1rem; + align-items: center; +} + +.roomDisplay { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + position: relative; +} + +.roomDisplayCenter { + display: flex; + width: calc(100% - 25em); + height: 100%; + flex-direction: column; +} + +.roomDisplayPeople { + width: 25em; + height: 100%; + overflow-y: scroll; + display: flex; + align-items: center; + flex-direction: column; +} + +.roomDisplayPeople>span { + display: block; + width: 100%; + text-align: center; + font-size: 1.5em; + margin: .5em 0em; +} + +.person { + width: 20em; +} + +.person img, .person .profile { + height: 7em; + width: 7em; +} + +.person .info { + margin: 5px; +} + +.person .ltext { + font-size: 18px; +} + +.person .gtext { + font-size: 12px; +} + +.roomDisplay .messages { + flex: 1; + margin-left: 1rem; + display: flex; + flex-direction: column-reverse; + overflow-y: scroll; +} + +.roomDisplay .messageContent { + flex-grow: 0; + width: auto; + margin-bottom: .5rem; + display: block; + height: fit-content; + overflow: none; +} + +.roomDisplay .messageContent[contenteditable]:empty::before { + content: "Send an unencrypted message"; + color: gray; +} + +.addUser[contenteditable]:empty::before { + content: "Type email to add user"; + color: gray; +} + +.addRoom[contenteditable]:empty::before { + content: "Type name to create room"; + color: gray; +} + +.message { + display: flex; + flex-direction: row; + height: fit-content; + padding: .5rem; +} + +.message-pfp { + flex-grow: 0; + height: 3rem; + width: 3rem; + margin-right: 1rem; +} + +.message-pfp img { + width: 100%; + height: 100%; + border-radius: 3rem; +} + +.message-content { + flex-grow: 0; +} diff --git a/public/css/home.css b/public/css/home.css index f467aba..f21d201 100644 --- a/public/css/home.css +++ b/public/css/home.css @@ -184,4 +184,4 @@ body { width: calc(100% - 20px); background-color: var(--secondary); font-family: sfpro; -} \ No newline at end of file +} diff --git a/public/css/main.css b/public/css/main.css index 352a3b4..c8ff1d7 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -14,6 +14,10 @@ body { --popup: #ffffffcc; } +textarea { + resize: none +} + body { background-color: var(--primary); width: 100%; @@ -130,7 +134,7 @@ footer { background-color: var(--primary); } -input { +input, .input { flex: 1; font-family: sfpro; background-color: var(--primary); diff --git a/public/js/api.js b/public/js/api.js index 5a55460..9ef5e4c 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -141,4 +141,28 @@ export const updateavatar = async (file) => { export const updatebanner = async (file) => { return await fileRequest('/users/banner', file, 'PUT') -} \ No newline at end of file +} + +export const chatlist = async () => { + return await request('/chat/list', {}) +} + +export const chatcreate = async (name) => { + return await request('/chat/create', {name}) +} + +export const chatadd = async (email, room_id) => { + return await request('/chat/add', {email, room_id}, 'PATCH') +} + +export const chatleave = async (room_id) => { + return await request('/chat/leave', {room_id}, 'DELETE') +} + +export const chatsend = async (content, room_id) => { + return await request('/chat/send', {content, room_id}) +} + +export const chatload = async (newest_msg, page, room_id) => { + return await request('/chat/load', {newest_msg, page, room_id}) +} diff --git a/public/js/chat.js b/public/js/chat.js index 41ba2ba..33f21cb 100644 --- a/public/js/chat.js +++ b/public/js/chat.js @@ -1,21 +1,202 @@ -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' +import { body, div, span, p, parse } from './main.js' +import { loadself, chatlist, chatload, loadusers, chatcreate } from './api.js' +import { createRoomDisplay, header, parseMessage, parseRoom, parseUser, createSingleLineInput } from './components.js' -function render() { +async function getUser(user_id) { + if (data.users[user_id]) { + return data.users[user_id] + } else { + let request = (await loadusers([user_id])) + if (request.status != 200) { + location.href = 'login' + } else { + data.users[user_id] = request.json[0] + return request.json[0] + } + } +} +async function parseMessageImpl(message) { + let user = await getUser(message.user_id) + return parseMessage(message, user) +} + +async function onRoomClick(room) { + for (const room of Object.values(data.rooms)) { + room.display.style.display = 'none' + room.button.classList.remove('current') + } + room.display.style.display = '' + room.button.classList.add('current') +} + +async function render() { let new_body = body({}, ...header(false, false, true, data.self.user_id), + div({id: 'cent'}, + div({id: 'sidebar'}, + span({class: 'ltext'}, + parse("Rooms") + ), + createSingleLineInput( + { + type: 'text', + name: 'addRoom', + class: 'addRoom input', + style: 'flex-grow: 0; width: 80%; margin-left: auto; margin-right: auto;' + }, + async (text) => { + let result = (await chatcreate(text)) + if (result.status != 201) { + alert(result.msg) + return false + } else { + return true + } + } + ), + ), + div({id: 'center'}) + ) ) document.body.replaceWith(new_body) - } const data = { self: {}, - users: [] + users: [], + rooms: {}, +} + +async function loadRoomPage(room_id) { + let room = data.rooms[room_id] + let request = (await chatload ( + room.newest_msg, + room.page, + room_id + )) + + if (request.json == undefined) { + alert(request.msg) + return + } + + for (const msg of request.json) { + room.messages.push(msg) + } + + room.page++ +} + +async function loadRoom(room_id) { + let room = data.rooms[room_id] + + let request = (await loadusers(room.users)) + if (request.status != 200) { + location.href = '/login' + } else { + for (const user of request.json) { + data.users[user.user_id] = user + } + } + + room.page = 0 + room.messages = [] + if (room.newest_msg == undefined || room.newest_msg < 0) + room.newest_msg = Number.MAX_SAFE_INTEGER + await loadRoomPage(room_id) + room.newest_msg = Math.max( + ...room.messages.map(m => m.message_id) + ) + room.page = 0 + + room.display = createRoomDisplay(room) + + let displays = document.getElementById("center") + displays.appendChild(room.display) + + let button = parseRoom(room, onRoomClick) + + if (displays.children.length > 1) { + room.display.style.display = 'none' + } else { + button.classList.add('current') + } + + let sidebar = document.getElementById("sidebar") + sidebar.appendChild(button) + room.button = button + + let messages = room.display.getElementsByClassName('messages')[0] + for (const message of room.messages) { + messages.appendChild(await parseMessageImpl(message)) + } + + if (!room.people) room.people = room.people = {} + + let people = room.display.getElementsByClassName("roomDisplayPeople")[0] + for (const user_id of room.users) { + if (room.people[user_id]) continue + let user = await getUser(user_id) + let el = parseUser(user) + people.appendChild(el) + room.people[user_id] = el + } + +} + +async function onMessage(message) { + let event = JSON.parse(message.data) + switch (event.type) { + case "message": { + let room = data.rooms[event.room_id] + if (!room) return + let display = room.display + let messages = display.getElementsByClassName('messages')[0] + messages.prepend(await parseMessageImpl(event)) + break; + } + case "add": { + let room = data.rooms[event.room.room_id] + if (!room) { + // we are the user being added + data.rooms[event.room.room_id] = event.room + loadRoom(event.room.room_id) + } else { + let display = room.display + let people = display.getElementsByClassName('roomDisplayPeople')[0] + if (!room.people[event.user_id]) { + let user = await getUser(event.user_id) + let el = parseUser(user) + people.appendChild(el) + room.people[event.user_id] = el + } + } + break; + } + case "leave": { + let room = data.rooms[event.room_id] + if (!room) return + if (room.people[event.user_id]) { + room.people[event.user_id].remove() + delete room.people[event.user_id] + } + if (event.user_id == data.self.user_id) { + room.display.remove() + room.button.remove() + } + break; + } + case "typing": { + break; + } + default: { + console.warn("unhandled event: " + message.data) + break; + } + } } async function init() { @@ -31,6 +212,20 @@ async function init() { data.users[data.self.user_id] = data.self render() + + let rooms = (await chatlist()); + if (rooms.json === undefined) { + alert(rooms.msg) + } else { + for (const room of rooms.json) { + data.rooms[room.room_id] = room + loadRoom(room.room_id) + } + } + + let socket = new WebSocket(window.location.protocol.replace("http", "ws") + "//" + location.host + "/api/chat/connect") + socket.addEventListener("message", onMessage); + } diff --git a/public/js/components.js b/public/js/components.js index f247875..38af787 100644 --- a/public/js/components.js +++ b/public/js/components.js @@ -1,5 +1,5 @@ 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'; +import { postlike, postcomment, loadcommentspage, chatsend, chatadd, chatleave } from './api.js'; window.parse = parse; @@ -268,3 +268,164 @@ export function parseUser(user) { ) ) } + +const stringToColor = (str) => { + let hash = 0; + str.split('').forEach(char => { + hash = char.charCodeAt(0) + ((hash << 5) - hash) + }) + let color = '#' + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xff + color += value.toString(16).padStart(2, '0') + } + return color +} + +export function parseRoom(room, callback) { + + let dspName = room.name[0].toUpperCase() + let color = stringToColor(room.room_id + room.name + room.room_id) + + return ( + div({class: 'room', onclick: () => { + callback(room) + }}, + div({class: 'room-icon ltext', style: `background-color: ${color}`}, + span({}, parse(dspName)) + ), + div({class: 'room-name ltext'}, + span({}, parse(room.name)) + ), + div({ + class: 'close', + onclick: async () => { + let request = (await chatleave(room.room_id)) + + if (request.status != 200) { + alert(request.msg) + } + } + }) + ) + ) +} + +export function parseRooms(rooms, callback) { + let ret = [] + + for (const room of rooms) { + ret.push(parseRoom(room, callback)) + } + + return ret +} + +export function createMultiLineInput(attributes, onSubmit) { + let area = span({ + ...attributes, + role: 'textbox', + contenteditable: '', + onkeydown: async (event) => { + if (event.keyCode == 13 && !event.shiftKey) { + event.preventDefault() + let text = area.innerHTML.trim() + text = text.replaceAll("\n", "
") + if (text.length < 1) return + if (await onSubmit(text)) { + area.textContent = '' + } + } + }, + }) + return area +} + +export function createSingleLineInput(attributes, onSubmit) { + let area = span({ + ...attributes, + role: 'textbox', + contenteditable: '', + onkeydown: async (event) => { + if (event.keyCode == 13 && !event.shiftKey) { + event.preventDefault() + let text = area.innerHTML.trim() + text = text.replaceAll("\n", "
") + if (text.length < 1) return + if (await onSubmit(text)) { + area.textContent = '' + } + } else if (event.keyCode == 13) { + event.preventDefault() + } + }, + }) + return area +} + +export function createRoomDisplay(room) { + return ( + div({class: 'roomDisplay'}, + div({class: 'roomDisplayCenter'}, + div({class: 'messages'}), + createMultiLineInput( + { + type: 'text', + name: 'messageContent', + class: 'messageContent input', + }, + async (text) => { + let result = (await chatsend(text, room.room_id)) + + if (result.status != 201) { + alert(result.msg) + return false + } else { + return true + } + } + ), + ), + div({class: 'roomDisplayPeople'}, + span({class: 'ltext'}, + parse("People"), + ), + createSingleLineInput( + { + type: 'text', + name: 'addUser', + class: 'addUser input', + style: 'flex-grow: 0; width: 80%' + }, + async (text) => { + let result = (await chatadd(text, room.room_id)) + if (result.status != 200) { + alert(result.msg) + return false + } else { + return true + } + } + ) + ) + ) + ) +} + +export function parseMessage(message, user) { + return ( + div({class: 'message'}, + a({class: 'message-pfp', href: `/profile?id=${message.user_id}`}, + pfp(message.user_id) + ), + div({class: 'message-content'}, + span({class: 'message-name ltext'}, + parse(user.firstname + ' ' + user.lastname) + ), + p({class: 'message-text ltext'}, + parse(message.content) + ) + ) + ) + ) +} diff --git a/public/js/main.js b/public/js/main.js index 80ee48b..a20ef10 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -103,10 +103,6 @@ export function parse(html) { return document.createRange().createContextualFragment(html); } -export function pfpl(id) { - -} - export function pfp(id) { return img('pfp', {src: `/image/avatar?user_id=${id}`}) } @@ -133,7 +129,7 @@ export function parseMonth(month) { } export function parseDate(date) { - return parseMonth(date.getUTCMonth()) + ' ' + (date.getUTCDate()-1) + ', ' + date.getUTCFullYear() + ' ' + date.toLocaleTimeString(); + return parseMonth(date.getUTCMonth()) + ' ' + date.getUTCDate() + ', ' + date.getUTCFullYear() + ' ' + date.toLocaleTimeString(); } export function crawl(key, object) { diff --git a/src/api/chat.rs b/src/api/chat.rs index 1e56c3e..02bdfbd 100644 --- a/src/api/chat.rs +++ b/src/api/chat.rs @@ -6,7 +6,7 @@ use tokio::sync::{Mutex, mpsc::{Sender, self}}; use crate::{ public::docs::{EndpointDocumentation, EndpointMethod}, types::{ - extract::{AuthorizedUser, Check, CheckResult, Database, Json}, + extract::{AuthorizedUser, Check, CheckResult, Database, Json, Log}, http::ResponseCode, chat::{ChatRoom, ChatEvent}, user::User, }, @@ -75,7 +75,8 @@ pub const CHAT_LIST: EndpointDocumentation = EndpointDocumentation { async fn list ( AuthorizedUser(user): AuthorizedUser, - Database(db): Database + Database(db): Database, + _: Log ) -> Response { let Ok(rooms) = ChatRoom::from_user_id(&db, user.user_id) else { return ResponseCode::InternalServerError.text("Failed to retrieve rooms") @@ -137,7 +138,7 @@ async fn create ( for user in &room.users { send_event(ChatEvent::Add { user_id: *user, - room_id: room.room_id + room: room.clone() }, &room).await; } @@ -191,10 +192,14 @@ async fn add ( 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 { + let Ok(mut 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") }; + if room.users.contains(&to_add.user_id) { + return ResponseCode::BadRequest.text("User is already in the room") + } + let Ok(success) = room.add_user(&db, to_add.user_id) else { return ResponseCode::InternalServerError.text("Failed to add user") }; @@ -202,10 +207,12 @@ async fn add ( if !success { return ResponseCode::BadRequest.text("User is already in the room") } + + room.users.push(to_add.user_id); send_event(ChatEvent::Add { user_id: to_add.user_id, - room_id: room.room_id + room: room.clone() }, &room).await; ResponseCode::Success.text("Successfully added user") diff --git a/src/database/chat.rs b/src/database/chat.rs index 7364211..99ec86c 100644 --- a/src/database/chat.rs +++ b/src/database/chat.rs @@ -61,9 +61,10 @@ impl Database { ); ", )?; + let row = stmt.query_map([user_id], |row| { - let room_id = row.get(0)?; - let name = row.get(1)?; + let room_id: u64 = row.get(0)?; + let name: String = row.get(1)?; let mut stmt2 = self.0.prepare( " @@ -72,20 +73,19 @@ impl Database { " )?; - let mut users = Vec::new(); - let _ = stmt2.query_map([room_id], |row2| { - let user_id = row2.get(0)?; - users.push(user_id); - Ok(()) - })?; + let users = stmt2.query_map([room_id], |row2| { + Ok(row2.get(0)?) + })?.into_iter().flatten().collect(); let room = ChatRoom { room_id, users, name }; + Ok(room) })?; + Ok(row.into_iter().flatten().collect()) } @@ -158,8 +158,8 @@ impl Database { 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 user_id = row.get(1)?; + let room_id = row.get(2)?; let date = row.get(3)?; let content = row.get(4)?; @@ -182,17 +182,17 @@ impl Database { " SELECT * FROM chat_messages WHERE room_id = ? - AND message_id < newest_message - ORDER BY message_id ASC + AND message_id < ? + ORDER BY message_id DESC LIMIT ? OFFSET ? " )?; - let messages = stmt.query_map((room_id, 20, 20 * page), |row| { + let messages = stmt.query_map((room_id, newest_message, 20, 20 * page), |row| { let message_id = row.get(0)?; - let room_id = row.get(1)?; - let user_id = row.get(2)?; + let user_id = row.get(1)?; + let room_id = row.get(2)?; let date = row.get(3)?; let content = row.get(4)?; diff --git a/src/main.rs b/src/main.rs index 817f8ac..cc8a61e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,7 +46,7 @@ async fn main() { tracing_subscriber::registry() .with( fmt_layer - .with_filter(LevelFilter::TRACE) + .with_filter(LevelFilter::INFO) .with_filter(filter_fn(|metadata| { metadata.target().starts_with("xssbook") })), diff --git a/src/types/chat.rs b/src/types/chat.rs index ab3390c..8413f77 100644 --- a/src/types/chat.rs +++ b/src/types/chat.rs @@ -2,7 +2,7 @@ use serde::{Serialize, Deserialize}; use tracing::instrument; use crate::{types::http::{ResponseCode, Result}, database::Database}; -#[derive(Serialize, Clone, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug)] pub struct ChatRoom { pub room_id: u64, pub users: Vec, @@ -33,7 +33,7 @@ pub enum ChatEvent { #[serde(rename = "add")] Add { user_id: u64, - room_id: u64 + room: ChatRoom }, #[serde(rename = "leave")] From 909d47f3316b3593a375bfb52535003c6bc9dd4b Mon Sep 17 00:00:00 2001 From: Tyler Murphy <=> Date: Mon, 21 Aug 2023 23:43:01 -0400 Subject: [PATCH 4/5] fuck you you stupid browsers stop making thing safe >:( --- public/js/components.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/public/js/components.js b/public/js/components.js index 38af787..0bf0ed1 100644 --- a/public/js/components.js +++ b/public/js/components.js @@ -330,6 +330,13 @@ export function createMultiLineInput(attributes, onSubmit) { if (event.keyCode == 13 && !event.shiftKey) { event.preventDefault() let text = area.innerHTML.trim() + .replaceAll("&", '&') + .replaceAll("<", '<') + .replaceAll(">", '>') + .replaceAll(""", '"') + .replaceAll("'", "'") + + text = text.replaceAll("\n", "
") if (text.length < 1) return if (await onSubmit(text)) { From fabc1e51cb6b21b19f00f155adb0ee329c9abdb2 Mon Sep 17 00:00:00 2001 From: Tyler Murphy <=> Date: Tue, 22 Aug 2023 00:15:10 -0400 Subject: [PATCH 5/5] finish dms --- public/css/chat.css | 10 ++++++- public/js/chat.js | 60 +++++++++++++++++++++++------------------ public/js/components.js | 17 ++++++++++-- 3 files changed, 58 insertions(+), 29 deletions(-) diff --git a/public/css/chat.css b/public/css/chat.css index 1092a39..d654679 100644 --- a/public/css/chat.css +++ b/public/css/chat.css @@ -53,7 +53,8 @@ align-items: center; font-weight: 1000; font-size: 1.5rem; -} + +right: 0;} .room-name { display: flex; @@ -71,6 +72,7 @@ } .roomDisplayCenter { + position: relative; display: flex; width: calc(100% - 25em); height: 100%; @@ -170,3 +172,9 @@ .message-content { flex-grow: 0; } + +.loadMessages { + position: absolute; + right: 0; + cursor: pointer; +} diff --git a/public/js/chat.js b/public/js/chat.js index 33f21cb..ed92bb6 100644 --- a/public/js/chat.js +++ b/public/js/chat.js @@ -70,12 +70,12 @@ const data = { rooms: {}, } -async function loadRoomPage(room_id) { - let room = data.rooms[room_id] +async function loadRoomPage(room) { + let request = (await chatload ( room.newest_msg, room.page, - room_id + room.room_id )) if (request.json == undefined) { @@ -83,36 +83,38 @@ async function loadRoomPage(room_id) { return } + let messages = room.display.getElementsByClassName('messages')[0] for (const msg of request.json) { room.messages.push(msg) + messages.appendChild(await parseMessageImpl(msg)) } room.page++ + + return request.json.length > 0 } async function loadRoom(room_id) { let room = data.rooms[room_id] - let request = (await loadusers(room.users)) - if (request.status != 200) { - location.href = '/login' - } else { - for (const user of request.json) { - data.users[user.user_id] = user + let batch = [] + for (const user_id of room.users) { + if (data.users[user_id]) continue + batch.push(user_id) + } + + if (batch.length > 1) { + let request = (await loadusers(batch)) + if (request.status != 200) { + location.href = '/login' + } else { + for (const user of request.json) { + data.users[user.user_id] = user + } } } - room.page = 0 - room.messages = [] - if (room.newest_msg == undefined || room.newest_msg < 0) - room.newest_msg = Number.MAX_SAFE_INTEGER - await loadRoomPage(room_id) - room.newest_msg = Math.max( - ...room.messages.map(m => m.message_id) - ) - room.page = 0 - - room.display = createRoomDisplay(room) + room.display = createRoomDisplay(room, loadRoomPage) let displays = document.getElementById("center") displays.appendChild(room.display) @@ -125,15 +127,20 @@ async function loadRoom(room_id) { button.classList.add('current') } + room.page = 0 + room.messages = [] + if (room.newest_msg == undefined || room.newest_msg < 0) + room.newest_msg = Number.MAX_SAFE_INTEGER + await loadRoomPage(room) + room.newest_msg = Math.min( + ...room.messages.map(m => m.message_id) + ) + room.page = 0 + let sidebar = document.getElementById("sidebar") sidebar.appendChild(button) room.button = button - let messages = room.display.getElementsByClassName('messages')[0] - for (const message of room.messages) { - messages.appendChild(await parseMessageImpl(message)) - } - if (!room.people) room.people = room.people = {} let people = room.display.getElementsByClassName("roomDisplayPeople")[0] @@ -186,6 +193,7 @@ async function onMessage(message) { if (event.user_id == data.self.user_id) { room.display.remove() room.button.remove() + delete data.rooms[event.room_id] } break; } @@ -202,13 +210,13 @@ async function onMessage(message) { async function init() { let request = (await loadself()); - data.self = request.json if (request.json == undefined) { location.href = '/login' return } + data.self = request.json data.users[data.self.user_id] = data.self render() diff --git a/public/js/components.js b/public/js/components.js index 0bf0ed1..88895c1 100644 --- a/public/js/components.js +++ b/public/js/components.js @@ -1,4 +1,4 @@ -import { div, a, pfp, span, i, parse, parseDate, p, form, input, svg, path, parseMonth, g } from './main.js' +import { div, a, pfp, span, i, parse, parseDate, p, form, input, svg, path, parseMonth, g, button } from './main.js' import { postlike, postcomment, loadcommentspage, chatsend, chatadd, chatleave } from './api.js'; window.parse = parse; @@ -370,10 +370,23 @@ export function createSingleLineInput(attributes, onSubmit) { return area } -export function createRoomDisplay(room) { +export function createRoomDisplay(room, loadMessageCallback) { + let buttonEl = button({ + class: 'loadMessages input', + style: 'flex-grow: 0', + onclick: async () => { + if (!await loadMessageCallback(room)) { + buttonEl.remove() + } + } + }, + parse('Load Previous') + ) + return ( div({class: 'roomDisplay'}, div({class: 'roomDisplayCenter'}, + buttonEl, div({class: 'messages'}), createMultiLineInput( {